new version with saga
This commit is contained in:
498
internal/acl/manger.go
Normal file
498
internal/acl/manger.go
Normal file
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/acl/manager.go
|
||||
// Назначение: Глобальный менеджер ACL для всей СУБД.
|
||||
// Управляет пользователями, ролями и разрешениями на уровне БД и коллекций.
|
||||
// Реализован с использованием sync.Map для wait-free доступа.
|
||||
|
||||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// PermissionType определяет тип разрешения
|
||||
type PermissionType string
|
||||
|
||||
const (
|
||||
PermRead PermissionType = "read"
|
||||
PermWrite PermissionType = "write"
|
||||
PermDelete PermissionType = "delete"
|
||||
PermAdmin PermissionType = "admin"
|
||||
)
|
||||
|
||||
// User представляет пользователя системы
|
||||
type User struct {
|
||||
ID string `msgpack:"id"`
|
||||
Username string `msgpack:"username"`
|
||||
Password string `msgpack:"password"` // В реальной системе - хеш
|
||||
Roles []string `msgpack:"roles"`
|
||||
CreatedAt int64 `msgpack:"created_at"`
|
||||
LastLogin int64 `msgpack:"last_login"`
|
||||
Active bool `msgpack:"active"`
|
||||
}
|
||||
|
||||
// Session представляет активную сессию пользователя
|
||||
type Session struct {
|
||||
ID string `msgpack:"id"`
|
||||
Username string `msgpack:"username"`
|
||||
Roles []string `msgpack:"roles"`
|
||||
CreatedAt int64 `msgpack:"created_at"`
|
||||
ExpiresAt int64 `msgpack:"expires_at"`
|
||||
}
|
||||
|
||||
// Role представляет роль с набором разрешений
|
||||
type Role struct {
|
||||
Name string `msgpack:"name"`
|
||||
Permissions []string `msgpack:"permissions"` // "database.collection:read" формат
|
||||
}
|
||||
|
||||
// ACLManager управляет доступом к БД
|
||||
type ACLManager struct {
|
||||
users sync.Map // map[string]*User
|
||||
roles sync.Map // map[string]*Role
|
||||
sessions sync.Map // map[string]*Session - sessionID -> Session
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewACLManager создаёт новый менеджер ACL
|
||||
func NewACLManager() *ACLManager {
|
||||
m := &ACLManager{}
|
||||
|
||||
// Создаём роль администратора по умолчанию
|
||||
adminRole := &Role{
|
||||
Name: "admin",
|
||||
Permissions: []string{"*:*"},
|
||||
}
|
||||
m.roles.Store("admin", adminRole)
|
||||
|
||||
// Создаём пользователя admin по умолчанию
|
||||
adminUser := &User{
|
||||
ID: uuid.New().String(),
|
||||
Username: "admin",
|
||||
Password: "admin", // В продакшене использовать хеш!
|
||||
Roles: []string{"admin"},
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Active: true,
|
||||
}
|
||||
m.users.Store("admin", adminUser)
|
||||
|
||||
// Создаём роль guest с ограниченными правами
|
||||
guestRole := &Role{
|
||||
Name: "guest",
|
||||
Permissions: []string{},
|
||||
}
|
||||
m.roles.Store("guest", guestRole)
|
||||
|
||||
// Создаём пользователя guest по умолчанию
|
||||
guestUser := &User{
|
||||
ID: uuid.New().String(),
|
||||
Username: "guest",
|
||||
Password: "guest",
|
||||
Roles: []string{"guest"},
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Active: true,
|
||||
}
|
||||
m.users.Store("guest", guestUser)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// CreateUser создаёт нового пользователя
|
||||
func (m *ACLManager) CreateUser(username, password string, roles []string) error {
|
||||
if _, exists := m.users.Load(username); exists {
|
||||
return fmt.Errorf("user %s already exists", username)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
ID: uuid.New().String(),
|
||||
Username: username,
|
||||
Password: password,
|
||||
Roles: roles,
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Active: true,
|
||||
}
|
||||
|
||||
m.users.Store(username, user)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate аутентифицирует пользователя и создаёт сессию
|
||||
func (m *ACLManager) Authenticate(username, password string) (string, error) {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
if !user.Active {
|
||||
return "", fmt.Errorf("user is disabled")
|
||||
}
|
||||
|
||||
if user.Password != password {
|
||||
return "", fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
// Обновляем время последнего входа
|
||||
user.LastLogin = time.Now().UnixMilli()
|
||||
m.users.Store(username, user)
|
||||
|
||||
// Создаём сессию (24 часа)
|
||||
sessionID := uuid.New().String()
|
||||
now := time.Now().Unix()
|
||||
session := &Session{
|
||||
ID: sessionID,
|
||||
Username: username,
|
||||
Roles: user.Roles,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now + 86400, // 24 часа
|
||||
}
|
||||
m.sessions.Store(sessionID, session)
|
||||
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// Logout завершает сессию
|
||||
func (m *ACLManager) Logout(sessionID string) {
|
||||
m.sessions.Delete(sessionID)
|
||||
}
|
||||
|
||||
// CheckSession проверяет, активна ли сессия
|
||||
func (m *ACLManager) CheckSession(sessionID string) bool {
|
||||
val, ok := m.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
session := val.(*Session)
|
||||
// Проверка на expiry
|
||||
if time.Now().Unix() > session.ExpiresAt {
|
||||
m.sessions.Delete(sessionID)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetUsername возвращает имя пользователя по ID сессии
|
||||
func (m *ACLManager) GetUsername(sessionID string) string {
|
||||
val, ok := m.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
session := val.(*Session)
|
||||
return session.Username
|
||||
}
|
||||
|
||||
// GetUserRoles возвращает роли пользователя по ID сессии
|
||||
func (m *ACLManager) GetUserRoles(sessionID string) []string {
|
||||
val, ok := m.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
session := val.(*Session)
|
||||
return session.Roles
|
||||
}
|
||||
|
||||
// CheckPermission проверяет разрешение для сессии
|
||||
func (m *ACLManager) CheckPermission(sessionID, database, collection, operation string) bool {
|
||||
val, ok := m.sessions.Load(sessionID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
session := val.(*Session)
|
||||
|
||||
for _, roleName := range session.Roles {
|
||||
roleVal, ok := m.roles.Load(roleName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
role := roleVal.(*Role)
|
||||
for _, perm := range role.Permissions {
|
||||
if m.matchPermission(perm, database, collection, operation) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchPermission проверяет соответствие разрешения
|
||||
func (m *ACLManager) matchPermission(perm, database, collection, operation string) bool {
|
||||
// Формат: "database.collection:operation" или "*:*" для всех
|
||||
// или "database.*:read" для всех коллекций в БД
|
||||
|
||||
parts := splitPermission(perm)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
resource := parts[0] // "database.collection" или "database.*"
|
||||
op := parts[1] // "read", "write", "delete", "admin"
|
||||
|
||||
// Проверка операции
|
||||
if op != "*" && op != operation && operation != "admin" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Администратор имеет доступ ко всему
|
||||
if op == "admin" || op == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Проверка ресурса
|
||||
if resource == "*:*" {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceParts := splitResource(resource)
|
||||
if len(resourceParts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
dbPattern := resourceParts[0]
|
||||
collPattern := resourceParts[1]
|
||||
|
||||
if dbPattern != "*" && dbPattern != database {
|
||||
return false
|
||||
}
|
||||
|
||||
if collPattern != "*" && collPattern != collection {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GrantPermission выдаёт разрешение роли
|
||||
func (m *ACLManager) GrantPermission(roleName, permission string) error {
|
||||
val, ok := m.roles.Load(roleName)
|
||||
if !ok {
|
||||
return fmt.Errorf("role not found")
|
||||
}
|
||||
|
||||
role := val.(*Role)
|
||||
role.Permissions = append(role.Permissions, permission)
|
||||
m.roles.Store(roleName, role)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokePermission отзывает разрешение у роли
|
||||
func (m *ACLManager) RevokePermission(roleName, permission string) error {
|
||||
val, ok := m.roles.Load(roleName)
|
||||
if !ok {
|
||||
return fmt.Errorf("role not found")
|
||||
}
|
||||
|
||||
role := val.(*Role)
|
||||
newPermissions := make([]string, 0, len(role.Permissions))
|
||||
for _, p := range role.Permissions {
|
||||
if p != permission {
|
||||
newPermissions = append(newPermissions, p)
|
||||
}
|
||||
}
|
||||
role.Permissions = newPermissions
|
||||
m.roles.Store(roleName, role)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateRole создаёт новую роль
|
||||
func (m *ACLManager) CreateRole(name string) error {
|
||||
if _, exists := m.roles.Load(name); exists {
|
||||
return fmt.Errorf("role %s already exists", name)
|
||||
}
|
||||
|
||||
role := &Role{
|
||||
Name: name,
|
||||
Permissions: []string{},
|
||||
}
|
||||
|
||||
m.roles.Store(name, role)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRole удаляет роль
|
||||
func (m *ACLManager) DeleteRole(name string) error {
|
||||
if _, exists := m.roles.LoadAndDelete(name); !exists {
|
||||
return fmt.Errorf("role not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddUserRole добавляет роль пользователю
|
||||
func (m *ACLManager) AddUserRole(username, roleName string) error {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
// Проверяем, есть ли уже такая роль
|
||||
for _, r := range user.Roles {
|
||||
if r == roleName {
|
||||
return fmt.Errorf("user already has role %s", roleName)
|
||||
}
|
||||
}
|
||||
user.Roles = append(user.Roles, roleName)
|
||||
m.users.Store(username, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUserRole удаляет роль у пользователя
|
||||
func (m *ACLManager) RemoveUserRole(username, roleName string) error {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
newRoles := make([]string, 0, len(user.Roles))
|
||||
for _, r := range user.Roles {
|
||||
if r != roleName {
|
||||
newRoles = append(newRoles, r)
|
||||
}
|
||||
}
|
||||
user.Roles = newRoles
|
||||
m.users.Store(username, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableUser отключает пользователя
|
||||
func (m *ACLManager) DisableUser(username string) error {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
user.Active = false
|
||||
m.users.Store(username, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableUser включает пользователя
|
||||
func (m *ACLManager) EnableUser(username string) error {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
user.Active = true
|
||||
m.users.Store(username, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser удаляет пользователя
|
||||
func (m *ACLManager) DeleteUser(username string) error {
|
||||
if _, exists := m.users.LoadAndDelete(username); !exists {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangePassword изменяет пароль пользователя
|
||||
func (m *ACLManager) ChangePassword(username, newPassword string) error {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
user.Password = newPassword
|
||||
m.users.Store(username, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUsers возвращает список всех пользователей
|
||||
func (m *ACLManager) ListUsers() []string {
|
||||
users := make([]string, 0)
|
||||
m.users.Range(func(key, value interface{}) bool {
|
||||
users = append(users, key.(string))
|
||||
return true
|
||||
})
|
||||
return users
|
||||
}
|
||||
|
||||
// ListRoles возвращает список всех ролей
|
||||
func (m *ACLManager) ListRoles() []string {
|
||||
roles := make([]string, 0)
|
||||
m.roles.Range(func(key, value interface{}) bool {
|
||||
roles = append(roles, key.(string))
|
||||
return true
|
||||
})
|
||||
return roles
|
||||
}
|
||||
|
||||
// GetUserInfo возвращает информацию о пользователе
|
||||
func (m *ACLManager) GetUserInfo(username string) (*User, error) {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
// Возвращаем копию, чтобы избежать модификации извне
|
||||
return &User{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Roles: user.Roles,
|
||||
CreatedAt: user.CreatedAt,
|
||||
LastLogin: user.LastLogin,
|
||||
Active: user.Active,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRolePermissions возвращает разрешения роли
|
||||
func (m *ACLManager) GetRolePermissions(roleName string) ([]string, error) {
|
||||
val, ok := m.roles.Load(roleName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("role not found")
|
||||
}
|
||||
|
||||
role := val.(*Role)
|
||||
return role.Permissions, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func splitPermission(perm string) []string {
|
||||
for i := 0; i < len(perm); i++ {
|
||||
if perm[i] == ':' {
|
||||
return []string{perm[:i], perm[i+1:]}
|
||||
}
|
||||
}
|
||||
return []string{perm, ""}
|
||||
}
|
||||
|
||||
func splitResource(resource string) []string {
|
||||
for i := 0; i < len(resource); i++ {
|
||||
if resource[i] == '.' {
|
||||
return []string{resource[:i], resource[i+1:]}
|
||||
}
|
||||
}
|
||||
return []string{resource, "*"}
|
||||
}
|
||||
1705
internal/api/http.go
Normal file
1705
internal/api/http.go
Normal file
File diff suppressed because it is too large
Load Diff
204
internal/api/rate_limiter.go
Normal file
204
internal/api/rate_limiter.go
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/api/rate_limiter.go (НОВЫЙ ФАЙЛ)
|
||||
// Назначение: Rate limiting для API
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RateLimiterConfig содержит конфигурацию rate limiter'а
|
||||
type RateLimiterConfig struct {
|
||||
RequestsPerSecond int
|
||||
BurstSize int
|
||||
CleanupInterval time.Duration
|
||||
MaxEntries int
|
||||
}
|
||||
|
||||
// DefaultRateLimiterConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultRateLimiterConfig() *RateLimiterConfig {
|
||||
return &RateLimiterConfig{
|
||||
RequestsPerSecond: 100,
|
||||
BurstSize: 20,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
MaxEntries: 10000,
|
||||
}
|
||||
}
|
||||
|
||||
// TokenBucket реализует алгоритм token bucket
|
||||
type TokenBucket struct {
|
||||
tokens float64
|
||||
maxTokens float64
|
||||
refillRate float64
|
||||
lastRefill time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewTokenBucket создаёт новый token bucket
|
||||
func NewTokenBucket(ratePerSecond float64, burst int) *TokenBucket {
|
||||
return &TokenBucket{
|
||||
tokens: float64(burst),
|
||||
maxTokens: float64(burst),
|
||||
refillRate: ratePerSecond,
|
||||
lastRefill: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Allow проверяет, разрешён ли запрос
|
||||
func (tb *TokenBucket) Allow() bool {
|
||||
tb.mu.Lock()
|
||||
defer tb.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(tb.lastRefill).Seconds()
|
||||
tb.tokens += elapsed * tb.refillRate
|
||||
if tb.tokens > tb.maxTokens {
|
||||
tb.tokens = tb.maxTokens
|
||||
}
|
||||
tb.lastRefill = now
|
||||
|
||||
if tb.tokens >= 1.0 {
|
||||
tb.tokens -= 1.0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RateLimiter управляет rate limiting для API
|
||||
type RateLimiter struct {
|
||||
config *RateLimiterConfig
|
||||
limiters sync.Map
|
||||
rejected atomic.Uint64
|
||||
allowed atomic.Uint64
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewRateLimiter создаёт новый rate limiter
|
||||
func NewRateLimiter(config *RateLimiterConfig) *RateLimiter {
|
||||
if config == nil {
|
||||
config = DefaultRateLimiterConfig()
|
||||
}
|
||||
|
||||
rl := &RateLimiter{
|
||||
config: config,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
rl.wg.Add(1)
|
||||
go rl.cleanupLoop()
|
||||
|
||||
return rl
|
||||
}
|
||||
|
||||
// getLimiter возвращает или создаёт лимитер для ключа
|
||||
func (rl *RateLimiter) getLimiter(key string) *TokenBucket {
|
||||
if val, ok := rl.limiters.Load(key); ok {
|
||||
return val.(*TokenBucket)
|
||||
}
|
||||
|
||||
limiter := NewTokenBucket(float64(rl.config.RequestsPerSecond), rl.config.BurstSize)
|
||||
actual, _ := rl.limiters.LoadOrStore(key, limiter)
|
||||
return actual.(*TokenBucket)
|
||||
}
|
||||
|
||||
// Allow проверяет, разрешён ли запрос для ключа
|
||||
func (rl *RateLimiter) Allow(key string) bool {
|
||||
limiter := rl.getLimiter(key)
|
||||
allowed := limiter.Allow()
|
||||
|
||||
if allowed {
|
||||
rl.allowed.Add(1)
|
||||
} else {
|
||||
rl.rejected.Add(1)
|
||||
}
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
||||
// Middleware возвращает middleware для HTTP
|
||||
func (rl *RateLimiter) Middleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Используем IP + User-Agent как ключ
|
||||
key := r.RemoteAddr
|
||||
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
||||
key = forwarded
|
||||
}
|
||||
|
||||
if !rl.Allow(key) {
|
||||
w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", rl.config.RequestsPerSecond))
|
||||
w.Header().Set("X-RateLimit-Remaining", "0")
|
||||
w.Header().Set("Retry-After", "1")
|
||||
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupLoop периодически очищает старые лимитеры
|
||||
func (rl *RateLimiter) cleanupLoop() {
|
||||
defer rl.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(rl.config.CleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
rl.cleanup()
|
||||
case <-rl.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup удаляет старые лимитеры
|
||||
func (rl *RateLimiter) cleanup() {
|
||||
count := 0
|
||||
rl.limiters.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
if count > rl.config.MaxEntries {
|
||||
rl.limiters.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику rate limiter'а
|
||||
func (rl *RateLimiter) GetStats() map[string]interface{} {
|
||||
count := 0
|
||||
rl.limiters.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
|
||||
return map[string]interface{}{
|
||||
"active_limiters": count,
|
||||
"allowed_requests": rl.allowed.Load(),
|
||||
"rejected_requests": rl.rejected.Load(),
|
||||
"requests_per_second": rl.config.RequestsPerSecond,
|
||||
"burst_size": rl.config.BurstSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает rate limiter
|
||||
func (rl *RateLimiter) Stop() {
|
||||
close(rl.stopChan)
|
||||
rl.wg.Wait()
|
||||
}
|
||||
1413
internal/api/static/app.js
Normal file
1413
internal/api/static/app.js
Normal file
File diff suppressed because it is too large
Load Diff
1572
internal/api/static/style.css
Normal file
1572
internal/api/static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
3106
internal/api/webui.go
Normal file
3106
internal/api/webui.go
Normal file
File diff suppressed because it is too large
Load Diff
425
internal/api/webui_credentials.go
Normal file
425
internal/api/webui_credentials.go
Normal file
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/api/webui_credentials.go
|
||||
// Назначение: Управление учётными данными для веб-интерфейса
|
||||
// Хранит логин/пароль в скрытом файле .credentials в директории futriis
|
||||
// Поддерживает множественных пользователей-администраторов
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserCredential представляет учётные данные одного пользователя
|
||||
type UserCredential struct {
|
||||
Password string `json:"password"` // хранится в виде хеша
|
||||
Avatar string `json:"avatar,omitempty"` // base64 encoded avatar
|
||||
IsAdmin bool `json:"is_admin"` // является ли администратором
|
||||
CreatedAt int64 `json:"created_at"` // время создания
|
||||
LastLogin int64 `json:"last_login,omitempty"` // время последнего входа
|
||||
}
|
||||
|
||||
// Credentials структура для хранения всех учётных данных
|
||||
type Credentials struct {
|
||||
Users map[string]*UserCredential `json:"users"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// CredentialManager управляет учётными данными веб-интерфейса
|
||||
type CredentialManager struct {
|
||||
mu sync.RWMutex
|
||||
credentials *Credentials
|
||||
credFile string
|
||||
currentUser string
|
||||
}
|
||||
|
||||
// NewCredentialManager создаёт новый менеджер учётных данных
|
||||
func NewCredentialManager() *CredentialManager {
|
||||
// Определяем путь к директории futriis
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
execPath = "."
|
||||
}
|
||||
futriisDir := filepath.Dir(execPath)
|
||||
|
||||
// Ищем директорию futriis (поднимаемся вверх, если нужно)
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(futriisDir, "futriis")); err == nil {
|
||||
futriisDir = filepath.Join(futriisDir, "futriis")
|
||||
break
|
||||
}
|
||||
parent := filepath.Dir(futriisDir)
|
||||
if parent == futriisDir {
|
||||
// Не нашли директорию futriis, создаём в текущей
|
||||
futriisDir = filepath.Join(execPath, "futriis")
|
||||
os.MkdirAll(futriisDir, 0700)
|
||||
break
|
||||
}
|
||||
futriisDir = parent
|
||||
}
|
||||
|
||||
credFile := filepath.Join(futriisDir, ".credentials")
|
||||
|
||||
return &CredentialManager{
|
||||
credentials: &Credentials{
|
||||
Users: make(map[string]*UserCredential),
|
||||
Settings: make(map[string]interface{}),
|
||||
},
|
||||
credFile: credFile,
|
||||
}
|
||||
}
|
||||
|
||||
// hashPassword создаёт хеш пароля
|
||||
func (cm *CredentialManager) hashPassword(password string) string {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return base64.StdEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// Load загружает учётные данные из файла
|
||||
func (cm *CredentialManager) Load() error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(cm.credFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return cm.createDefaultFile()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var creds Credentials
|
||||
if err := json.Unmarshal(data, &creds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.credentials = &creds
|
||||
if cm.credentials.Users == nil {
|
||||
cm.credentials.Users = make(map[string]*UserCredential)
|
||||
}
|
||||
if cm.credentials.Settings == nil {
|
||||
cm.credentials.Settings = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDefaultFile создаёт файл с учётными данными по умолчанию
|
||||
func (cm *CredentialManager) createDefaultFile() error {
|
||||
// Создаём пользователя admin по умолчанию
|
||||
cm.credentials.Users["admin"] = &UserCredential{
|
||||
Password: cm.hashPassword("admin"),
|
||||
IsAdmin: true,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
cm.credentials.Settings["version"] = "1.0"
|
||||
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// CreateDefault создаёт учётные данные по умолчанию (если файл не существует)
|
||||
func (cm *CredentialManager) CreateDefault() error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
// Проверяем, существует ли файл
|
||||
if _, err := os.Stat(cm.credFile); err == nil {
|
||||
return nil // файл уже существует
|
||||
}
|
||||
|
||||
cm.credentials.Users["admin"] = &UserCredential{
|
||||
Password: cm.hashPassword("admin"),
|
||||
IsAdmin: true,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
cm.credentials.Settings["version"] = "1.0"
|
||||
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// save сохраняет учётные данные в файл
|
||||
func (cm *CredentialManager) save() error {
|
||||
data, err := json.MarshalIndent(cm.credentials, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Устанавливаем права доступа 0600 (только владелец может читать/писать)
|
||||
return os.WriteFile(cm.credFile, data, 0600)
|
||||
}
|
||||
|
||||
// Validate проверяет учётные данные
|
||||
func (cm *CredentialManager) Validate(username, password string) bool {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return user.Password == cm.hashPassword(password)
|
||||
}
|
||||
|
||||
// IsAdmin проверяет, является ли пользователь администратором
|
||||
func (cm *CredentialManager) IsAdmin(username string) bool {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return user.IsAdmin
|
||||
}
|
||||
|
||||
// CreateUser создаёт нового пользователя (только для администраторов)
|
||||
func (cm *CredentialManager) CreateUser(username, password string, isAdmin bool) error {
|
||||
if username == "" {
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
}
|
||||
|
||||
if len(password) < 4 {
|
||||
return fmt.Errorf("password must be at least 4 characters")
|
||||
}
|
||||
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if cm.credentials == nil {
|
||||
cm.credentials = &Credentials{
|
||||
Users: make(map[string]*UserCredential),
|
||||
Settings: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := cm.credentials.Users[username]; exists {
|
||||
return fmt.Errorf("user %s already exists", username)
|
||||
}
|
||||
|
||||
cm.credentials.Users[username] = &UserCredential{
|
||||
Password: cm.hashPassword(password),
|
||||
IsAdmin: isAdmin,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// DeleteUser удаляет пользователя (только для администраторов)
|
||||
func (cm *CredentialManager) DeleteUser(username string) error {
|
||||
if username == "admin" {
|
||||
return fmt.Errorf("cannot delete default admin user")
|
||||
}
|
||||
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return fmt.Errorf("no users found")
|
||||
}
|
||||
|
||||
if _, exists := cm.credentials.Users[username]; !exists {
|
||||
return fmt.Errorf("user %s not found", username)
|
||||
}
|
||||
|
||||
delete(cm.credentials.Users, username)
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// ListUsers возвращает список всех пользователей (только для администраторов)
|
||||
func (cm *CredentialManager) ListUsers() []map[string]interface{} {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
users := make([]map[string]interface{}, 0)
|
||||
|
||||
for username, user := range cm.credentials.Users {
|
||||
users = append(users, map[string]interface{}{
|
||||
"username": username,
|
||||
"is_admin": user.IsAdmin,
|
||||
"created_at": user.CreatedAt,
|
||||
"last_login": user.LastLogin,
|
||||
"has_avatar": user.Avatar != "",
|
||||
})
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// ChangePassword изменяет пароль пользователя
|
||||
func (cm *CredentialManager) ChangePassword(username, currentPassword, newPassword string) error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return fmt.Errorf("credentials not loaded")
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
if user.Password != cm.hashPassword(currentPassword) {
|
||||
return fmt.Errorf("current password is incorrect")
|
||||
}
|
||||
|
||||
if len(newPassword) < 4 {
|
||||
return fmt.Errorf("new password must be at least 4 characters")
|
||||
}
|
||||
|
||||
user.Password = cm.hashPassword(newPassword)
|
||||
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// AdminChangePassword изменяет пароль пользователя (без проверки старого, только для админов)
|
||||
func (cm *CredentialManager) AdminChangePassword(username, newPassword string) error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return fmt.Errorf("credentials not loaded")
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
if len(newPassword) < 4 {
|
||||
return fmt.Errorf("password must be at least 4 characters")
|
||||
}
|
||||
|
||||
user.Password = cm.hashPassword(newPassword)
|
||||
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// SetAvatar устанавливает аватар для пользователя
|
||||
func (cm *CredentialManager) SetAvatar(username, avatarBase64 string) error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return fmt.Errorf("credentials not loaded")
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user.Avatar = avatarBase64
|
||||
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// GetAvatar возвращает аватар пользователя
|
||||
func (cm *CredentialManager) GetAvatar(username string) (string, error) {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return "", fmt.Errorf("credentials not loaded")
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
return user.Avatar, nil
|
||||
}
|
||||
|
||||
// DeleteAvatar удаляет аватар пользователя
|
||||
func (cm *CredentialManager) DeleteAvatar(username string) error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return fmt.Errorf("credentials not loaded")
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user.Avatar = ""
|
||||
|
||||
return cm.save()
|
||||
}
|
||||
|
||||
// UpdateLastLogin обновляет время последнего входа пользователя
|
||||
func (cm *CredentialManager) UpdateLastLogin(username string) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if cm.credentials == nil || cm.credentials.Users == nil {
|
||||
return
|
||||
}
|
||||
|
||||
user, exists := cm.credentials.Users[username]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
user.LastLogin = time.Now().Unix()
|
||||
cm.save() // игнорируем ошибку, т.к. это не критично
|
||||
}
|
||||
|
||||
// GetCurrentUsername возвращает имя текущего пользователя
|
||||
func (cm *CredentialManager) GetCurrentUsername() string {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
if cm.credentials == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Возвращаем последнего вошедшего пользователя или admin по умолчанию
|
||||
if cm.currentUser != "" {
|
||||
return cm.currentUser
|
||||
}
|
||||
|
||||
return "admin"
|
||||
}
|
||||
|
||||
// SetCurrentUsername устанавливает имя текущего пользователя
|
||||
func (cm *CredentialManager) SetCurrentUsername(username string) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
cm.currentUser = username
|
||||
}
|
||||
|
||||
// GetCredentialsFile возвращает путь к файлу с учётными данными
|
||||
func (cm *CredentialManager) GetCredentialsFile() string {
|
||||
return cm.credFile
|
||||
}
|
||||
204
internal/autoscaling/autoscaler.go
Normal file
204
internal/autoscaling/autoscaler.go
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/api/rate_limiter.go (НОВЫЙ ФАЙЛ)
|
||||
// Назначение: Rate limiting для API
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RateLimiterConfig содержит конфигурацию rate limiter'а
|
||||
type RateLimiterConfig struct {
|
||||
RequestsPerSecond int
|
||||
BurstSize int
|
||||
CleanupInterval time.Duration
|
||||
MaxEntries int
|
||||
}
|
||||
|
||||
// DefaultRateLimiterConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultRateLimiterConfig() *RateLimiterConfig {
|
||||
return &RateLimiterConfig{
|
||||
RequestsPerSecond: 100,
|
||||
BurstSize: 20,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
MaxEntries: 10000,
|
||||
}
|
||||
}
|
||||
|
||||
// TokenBucket реализует алгоритм token bucket
|
||||
type TokenBucket struct {
|
||||
tokens float64
|
||||
maxTokens float64
|
||||
refillRate float64
|
||||
lastRefill time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewTokenBucket создаёт новый token bucket
|
||||
func NewTokenBucket(ratePerSecond float64, burst int) *TokenBucket {
|
||||
return &TokenBucket{
|
||||
tokens: float64(burst),
|
||||
maxTokens: float64(burst),
|
||||
refillRate: ratePerSecond,
|
||||
lastRefill: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Allow проверяет, разрешён ли запрос
|
||||
func (tb *TokenBucket) Allow() bool {
|
||||
tb.mu.Lock()
|
||||
defer tb.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(tb.lastRefill).Seconds()
|
||||
tb.tokens += elapsed * tb.refillRate
|
||||
if tb.tokens > tb.maxTokens {
|
||||
tb.tokens = tb.maxTokens
|
||||
}
|
||||
tb.lastRefill = now
|
||||
|
||||
if tb.tokens >= 1.0 {
|
||||
tb.tokens -= 1.0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RateLimiter управляет rate limiting для API
|
||||
type RateLimiter struct {
|
||||
config *RateLimiterConfig
|
||||
limiters sync.Map
|
||||
rejected atomic.Uint64
|
||||
allowed atomic.Uint64
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewRateLimiter создаёт новый rate limiter
|
||||
func NewRateLimiter(config *RateLimiterConfig) *RateLimiter {
|
||||
if config == nil {
|
||||
config = DefaultRateLimiterConfig()
|
||||
}
|
||||
|
||||
rl := &RateLimiter{
|
||||
config: config,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
rl.wg.Add(1)
|
||||
go rl.cleanupLoop()
|
||||
|
||||
return rl
|
||||
}
|
||||
|
||||
// getLimiter возвращает или создаёт лимитер для ключа
|
||||
func (rl *RateLimiter) getLimiter(key string) *TokenBucket {
|
||||
if val, ok := rl.limiters.Load(key); ok {
|
||||
return val.(*TokenBucket)
|
||||
}
|
||||
|
||||
limiter := NewTokenBucket(float64(rl.config.RequestsPerSecond), rl.config.BurstSize)
|
||||
actual, _ := rl.limiters.LoadOrStore(key, limiter)
|
||||
return actual.(*TokenBucket)
|
||||
}
|
||||
|
||||
// Allow проверяет, разрешён ли запрос для ключа
|
||||
func (rl *RateLimiter) Allow(key string) bool {
|
||||
limiter := rl.getLimiter(key)
|
||||
allowed := limiter.Allow()
|
||||
|
||||
if allowed {
|
||||
rl.allowed.Add(1)
|
||||
} else {
|
||||
rl.rejected.Add(1)
|
||||
}
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
||||
// Middleware возвращает middleware для HTTP
|
||||
func (rl *RateLimiter) Middleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Используем IP + User-Agent как ключ
|
||||
key := r.RemoteAddr
|
||||
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
||||
key = forwarded
|
||||
}
|
||||
|
||||
if !rl.Allow(key) {
|
||||
w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", rl.config.RequestsPerSecond))
|
||||
w.Header().Set("X-RateLimit-Remaining", "0")
|
||||
w.Header().Set("Retry-After", "1")
|
||||
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupLoop периодически очищает старые лимитеры
|
||||
func (rl *RateLimiter) cleanupLoop() {
|
||||
defer rl.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(rl.config.CleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
rl.cleanup()
|
||||
case <-rl.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup удаляет старые лимитеры
|
||||
func (rl *RateLimiter) cleanup() {
|
||||
count := 0
|
||||
rl.limiters.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
if count > rl.config.MaxEntries {
|
||||
rl.limiters.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику rate limiter'а
|
||||
func (rl *RateLimiter) GetStats() map[string]interface{} {
|
||||
count := 0
|
||||
rl.limiters.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
|
||||
return map[string]interface{}{
|
||||
"active_limiters": count,
|
||||
"allowed_requests": rl.allowed.Load(),
|
||||
"rejected_requests": rl.rejected.Load(),
|
||||
"requests_per_second": rl.config.RequestsPerSecond,
|
||||
"burst_size": rl.config.BurstSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает rate limiter
|
||||
func (rl *RateLimiter) Stop() {
|
||||
close(rl.stopChan)
|
||||
rl.wg.Wait()
|
||||
}
|
||||
1299
internal/backup/backup_scheduler.go
Normal file
1299
internal/backup/backup_scheduler.go
Normal file
File diff suppressed because it is too large
Load Diff
349
internal/cluster/auth.go
Normal file
349
internal/cluster/auth.go
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/cluster/auth.go
|
||||
// Назначение: Аутентификация и авторизация между узлами кластера
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NodeAuthConfig конфигурация аутентификации узлов
|
||||
type NodeAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TokenTTL time.Duration `json:"token_ttl"`
|
||||
PrivateKeyPath string `json:"private_key_path"`
|
||||
PublicKeyPath string `json:"public_key_path"`
|
||||
AllowedNodes []string `json:"allowed_nodes"`
|
||||
RequireMTLS bool `json:"require_mtls"`
|
||||
}
|
||||
|
||||
// NodeAuthToken представляет токен аутентификации узла
|
||||
type NodeAuthToken struct {
|
||||
NodeID string `json:"node_id"`
|
||||
IssuedAt int64 `json:"issued_at"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// NodeAuthenticator управляет аутентификацией между узлами
|
||||
type NodeAuthenticator struct {
|
||||
config *NodeAuthConfig
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
tokens sync.Map
|
||||
mu sync.RWMutex
|
||||
logger LoggerInterface // Используем LoggerInterface из node.go
|
||||
}
|
||||
|
||||
// NewNodeAuthenticator создаёт новый аутентификатор узлов
|
||||
func NewNodeAuthenticator(config *NodeAuthConfig, logger LoggerInterface) (*NodeAuthenticator, error) {
|
||||
if config == nil {
|
||||
config = &NodeAuthConfig{
|
||||
Enabled: true,
|
||||
TokenTTL: 24 * time.Hour,
|
||||
AllowedNodes: make([]string, 0),
|
||||
RequireMTLS: false,
|
||||
}
|
||||
}
|
||||
|
||||
na := &NodeAuthenticator{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if config.Enabled {
|
||||
if err := na.loadKeys(); err != nil {
|
||||
if err := na.generateKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return na, nil
|
||||
}
|
||||
|
||||
// generateKeys генерирует RSA ключи для аутентификации
|
||||
func (na *NodeAuthenticator) generateKeys() error {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
na.privateKey = privateKey
|
||||
na.publicKey = &privateKey.PublicKey
|
||||
|
||||
// Сохраняем ключи
|
||||
if err := na.saveKeys(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if na.logger != nil {
|
||||
na.logger.Info("Generated new RSA keys for node authentication")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadKeys загружает RSA ключи с диска
|
||||
func (na *NodeAuthenticator) loadKeys() error {
|
||||
// Загружаем приватный ключ
|
||||
privData, err := os.ReadFile(na.config.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(privData)
|
||||
if block == nil {
|
||||
return fmt.Errorf("failed to decode private key")
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
na.privateKey, ok = privateKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid private key type")
|
||||
}
|
||||
|
||||
// Загружаем публичный ключ
|
||||
pubData, err := os.ReadFile(na.config.PublicKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubBlock, _ := pem.Decode(pubData)
|
||||
if pubBlock == nil {
|
||||
return fmt.Errorf("failed to decode public key")
|
||||
}
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
na.publicKey, ok = publicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid public key type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveKeys сохраняет RSA ключи на диск
|
||||
func (na *NodeAuthenticator) saveKeys() error {
|
||||
if na.privateKey == nil {
|
||||
return fmt.Errorf("no private key to save")
|
||||
}
|
||||
|
||||
// Сохраняем приватный ключ
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(na.privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: privBytes,
|
||||
})
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(na.config.PrivateKeyPath), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(na.config.PrivateKeyPath, privPEM, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Сохраняем публичный ключ
|
||||
pubBytes, err := x509.MarshalPKIXPublicKey(na.publicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: pubBytes,
|
||||
})
|
||||
|
||||
if err := os.WriteFile(na.config.PublicKeyPath, pubPEM, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateToken генерирует токен для узла
|
||||
func (na *NodeAuthenticator) GenerateToken(nodeID string) (string, error) {
|
||||
if !na.config.Enabled {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
token := &NodeAuthToken{
|
||||
NodeID: nodeID,
|
||||
IssuedAt: now,
|
||||
ExpiresAt: now + na.config.TokenTTL.Milliseconds(),
|
||||
}
|
||||
|
||||
// Подписываем токен
|
||||
data, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(data)
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token.Signature = base64.StdEncoding.EncodeToString(signature)
|
||||
|
||||
// Сохраняем токен
|
||||
tokenData, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokenString := base64.StdEncoding.EncodeToString(tokenData)
|
||||
na.tokens.Store(nodeID, token)
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// VerifyToken проверяет токен узла
|
||||
func (na *NodeAuthenticator) VerifyToken(tokenString string) (string, error) {
|
||||
if !na.config.Enabled {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(tokenString)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid token encoding: %v", err)
|
||||
}
|
||||
|
||||
var token NodeAuthToken
|
||||
if err := json.Unmarshal(data, &token); err != nil {
|
||||
return "", fmt.Errorf("invalid token format: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем срок действия
|
||||
now := time.Now().UnixMilli()
|
||||
if token.ExpiresAt < now {
|
||||
return "", fmt.Errorf("token expired")
|
||||
}
|
||||
|
||||
// Проверяем подпись
|
||||
sigData, err := base64.StdEncoding.DecodeString(token.Signature)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid signature: %v", err)
|
||||
}
|
||||
|
||||
tokenCopy := token
|
||||
tokenCopy.Signature = ""
|
||||
dataWithoutSig, _ := json.Marshal(tokenCopy)
|
||||
hash := sha256.Sum256(dataWithoutSig)
|
||||
|
||||
if err := rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData); err != nil {
|
||||
return "", fmt.Errorf("invalid signature: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем, разрешён ли узел
|
||||
if len(na.config.AllowedNodes) > 0 {
|
||||
allowed := false
|
||||
for _, node := range na.config.AllowedNodes {
|
||||
if node == token.NodeID {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return "", fmt.Errorf("node %s not allowed", token.NodeID)
|
||||
}
|
||||
}
|
||||
|
||||
return token.NodeID, nil
|
||||
}
|
||||
|
||||
// AuthenticateRequest аутентифицирует запрос
|
||||
func (na *NodeAuthenticator) AuthenticateRequest(data []byte, signature string) bool {
|
||||
if !na.config.Enabled {
|
||||
return true
|
||||
}
|
||||
|
||||
sigData, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(data)
|
||||
err = rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SignRequest подписывает запрос
|
||||
func (na *NodeAuthenticator) SignRequest(data []byte) (string, error) {
|
||||
if !na.config.Enabled {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(data)
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// AddAllowedNode добавляет разрешённый узел
|
||||
func (na *NodeAuthenticator) AddAllowedNode(nodeID string) {
|
||||
na.mu.Lock()
|
||||
defer na.mu.Unlock()
|
||||
|
||||
for _, n := range na.config.AllowedNodes {
|
||||
if n == nodeID {
|
||||
return
|
||||
}
|
||||
}
|
||||
na.config.AllowedNodes = append(na.config.AllowedNodes, nodeID)
|
||||
}
|
||||
|
||||
// RemoveAllowedNode удаляет разрешённый узел
|
||||
func (na *NodeAuthenticator) RemoveAllowedNode(nodeID string) {
|
||||
na.mu.Lock()
|
||||
defer na.mu.Unlock()
|
||||
|
||||
newList := make([]string, 0)
|
||||
for _, n := range na.config.AllowedNodes {
|
||||
if n != nodeID {
|
||||
newList = append(newList, n)
|
||||
}
|
||||
}
|
||||
na.config.AllowedNodes = newList
|
||||
}
|
||||
301
internal/cluster/backpressure.go
Normal file
301
internal/cluster/backpressure.go
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/cluster/backpressure.go
|
||||
// Назначение: Backpressure при перегрузке системы
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BackpressureLevel представляет уровень перегрузки
|
||||
type BackpressureLevel int
|
||||
|
||||
const (
|
||||
LevelNone BackpressureLevel = iota // Нет перегрузки
|
||||
LevelLow // Низкая перегрузка - задержки
|
||||
LevelMedium // Средняя перегрузка - отклонение части запросов
|
||||
LevelHigh // Высокая перегрузка - отклонение большинства
|
||||
LevelCritical // Критическая - только чтение
|
||||
)
|
||||
|
||||
// BackpressureManager управляет backpressure
|
||||
type BackpressureManager struct {
|
||||
mu sync.RWMutex
|
||||
currentLevel BackpressureLevel
|
||||
cpuThreshold float64
|
||||
memoryThreshold float64
|
||||
queueSizeThreshold int
|
||||
connectionThreshold int
|
||||
currentCPU atomic.Uint64
|
||||
currentMemory atomic.Uint64
|
||||
currentQueueSize atomic.Int64
|
||||
currentConnections atomic.Int64
|
||||
rejectedCount atomic.Uint64
|
||||
delayedCount atomic.Uint64
|
||||
lastCheck time.Time
|
||||
checkInterval time.Duration
|
||||
logger LoggerInterface // Используем LoggerInterface из node.go
|
||||
enabled bool
|
||||
writeAllowed bool
|
||||
readAllowed bool
|
||||
rejectProbability atomic.Uint32
|
||||
delayDuration atomic.Int64
|
||||
}
|
||||
|
||||
// BackpressureConfig содержит настройки backpressure
|
||||
type BackpressureConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CPUThreshold float64 `json:"cpu_threshold"`
|
||||
MemoryThreshold float64 `json:"memory_threshold"`
|
||||
QueueSizeThreshold int `json:"queue_size_threshold"`
|
||||
ConnectionThreshold int `json:"connection_threshold"`
|
||||
CheckIntervalMs int `json:"check_interval_ms"`
|
||||
LowDelayMs int64 `json:"low_delay_ms"`
|
||||
MediumRejectProb uint32 `json:"medium_reject_prob"`
|
||||
HighRejectProb uint32 `json:"high_reject_prob"`
|
||||
}
|
||||
|
||||
// DefaultBackpressureConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultBackpressureConfig() *BackpressureConfig {
|
||||
return &BackpressureConfig{
|
||||
Enabled: true,
|
||||
CPUThreshold: 0.8,
|
||||
MemoryThreshold: 0.85,
|
||||
QueueSizeThreshold: 10000,
|
||||
ConnectionThreshold: 5000,
|
||||
CheckIntervalMs: 1000,
|
||||
LowDelayMs: 100,
|
||||
MediumRejectProb: 30,
|
||||
HighRejectProb: 70,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBackpressureManager создаёт новый менеджер backpressure
|
||||
func NewBackpressureManager(cfg *BackpressureConfig, logger LoggerInterface) *BackpressureManager {
|
||||
if cfg == nil {
|
||||
cfg = DefaultBackpressureConfig()
|
||||
}
|
||||
|
||||
bpm := &BackpressureManager{
|
||||
currentLevel: LevelNone,
|
||||
cpuThreshold: cfg.CPUThreshold,
|
||||
memoryThreshold: cfg.MemoryThreshold,
|
||||
queueSizeThreshold: cfg.QueueSizeThreshold,
|
||||
connectionThreshold: cfg.ConnectionThreshold,
|
||||
checkInterval: time.Duration(cfg.CheckIntervalMs) * time.Millisecond,
|
||||
logger: logger,
|
||||
enabled: cfg.Enabled,
|
||||
writeAllowed: true,
|
||||
readAllowed: true,
|
||||
rejectProbability: atomic.Uint32{},
|
||||
delayDuration: atomic.Int64{},
|
||||
}
|
||||
|
||||
bpm.rejectProbability.Store(0)
|
||||
bpm.delayDuration.Store(0)
|
||||
|
||||
if cfg.Enabled {
|
||||
go bpm.monitorLoop()
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Debug("Backpressure manager initialized")
|
||||
}
|
||||
|
||||
return bpm
|
||||
}
|
||||
|
||||
// monitorLoop периодически проверяет метрики
|
||||
func (bpm *BackpressureManager) monitorLoop() {
|
||||
ticker := time.NewTicker(bpm.checkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
bpm.updateLevel()
|
||||
}
|
||||
}
|
||||
|
||||
// updateLevel обновляет уровень перегрузки
|
||||
func (bpm *BackpressureManager) updateLevel() {
|
||||
cpu := float64(bpm.currentCPU.Load()) / 100.0
|
||||
memory := float64(bpm.currentMemory.Load()) / 100.0
|
||||
queueSize := bpm.currentQueueSize.Load()
|
||||
connections := bpm.currentConnections.Load()
|
||||
|
||||
newLevel := LevelNone
|
||||
|
||||
if cpu >= bpm.cpuThreshold || memory >= bpm.memoryThreshold {
|
||||
newLevel = LevelHigh
|
||||
} else if queueSize > int64(bpm.queueSizeThreshold) {
|
||||
if queueSize > int64(bpm.queueSizeThreshold*2) {
|
||||
newLevel = LevelCritical
|
||||
} else {
|
||||
newLevel = LevelMedium
|
||||
}
|
||||
} else if connections > int64(bpm.connectionThreshold) {
|
||||
newLevel = LevelLow
|
||||
}
|
||||
|
||||
bpm.mu.Lock()
|
||||
oldLevel := bpm.currentLevel
|
||||
bpm.currentLevel = newLevel
|
||||
bpm.mu.Unlock()
|
||||
|
||||
// Применяем политики в зависимости от уровня
|
||||
bpm.applyPolicies(newLevel)
|
||||
|
||||
if oldLevel != newLevel && bpm.logger != nil {
|
||||
bpm.logger.Info(fmt.Sprintf("Backpressure level changed from %v to %v (cpu=%.2f%%, mem=%.2f%%, queue=%d, conns=%d)",
|
||||
bpm.levelToString(oldLevel), bpm.levelToString(newLevel), cpu*100, memory*100, queueSize, connections))
|
||||
}
|
||||
}
|
||||
|
||||
// applyPolicies применяет политики в зависимости от уровня
|
||||
func (bpm *BackpressureManager) applyPolicies(level BackpressureLevel) {
|
||||
bpm.mu.Lock()
|
||||
defer bpm.mu.Unlock()
|
||||
|
||||
switch level {
|
||||
case LevelNone:
|
||||
bpm.writeAllowed = true
|
||||
bpm.readAllowed = true
|
||||
bpm.rejectProbability.Store(0)
|
||||
bpm.delayDuration.Store(0)
|
||||
|
||||
case LevelLow:
|
||||
bpm.writeAllowed = true
|
||||
bpm.readAllowed = true
|
||||
bpm.rejectProbability.Store(0)
|
||||
bpm.delayDuration.Store(100) // 100ms задержка
|
||||
|
||||
case LevelMedium:
|
||||
bpm.writeAllowed = true
|
||||
bpm.readAllowed = true
|
||||
bpm.rejectProbability.Store(30) // 30% отклонение
|
||||
bpm.delayDuration.Store(200)
|
||||
|
||||
case LevelHigh:
|
||||
bpm.writeAllowed = false // Запись запрещена
|
||||
bpm.readAllowed = true
|
||||
bpm.rejectProbability.Store(70) // 70% отклонение
|
||||
bpm.delayDuration.Store(500)
|
||||
|
||||
case LevelCritical:
|
||||
bpm.writeAllowed = false
|
||||
bpm.readAllowed = true // Только чтение
|
||||
bpm.rejectProbability.Store(90)
|
||||
bpm.delayDuration.Store(1000)
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeRequest вызывается перед обработкой запроса
|
||||
func (bpm *BackpressureManager) BeforeRequest(isWrite bool) error {
|
||||
if !bpm.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
bpm.mu.RLock()
|
||||
level := bpm.currentLevel
|
||||
writeAllowed := bpm.writeAllowed
|
||||
readAllowed := bpm.readAllowed
|
||||
rejectProb := bpm.rejectProbability.Load()
|
||||
delayDur := bpm.delayDuration.Load()
|
||||
bpm.mu.RUnlock()
|
||||
|
||||
// Проверяем разрешение на операцию
|
||||
if isWrite && !writeAllowed {
|
||||
bpm.rejectedCount.Add(1)
|
||||
return fmt.Errorf("write operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
|
||||
}
|
||||
if !isWrite && !readAllowed {
|
||||
bpm.rejectedCount.Add(1)
|
||||
return fmt.Errorf("read operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
|
||||
}
|
||||
|
||||
// Вероятностное отклонение
|
||||
if rejectProb > 0 {
|
||||
// Простая вероятностная проверка
|
||||
if uint32(time.Now().UnixNano()%100) < rejectProb {
|
||||
bpm.rejectedCount.Add(1)
|
||||
return fmt.Errorf("request rejected due to backpressure (probability: %d%%)", rejectProb)
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем задержку если нужно
|
||||
if delayDur > 0 {
|
||||
bpm.delayedCount.Add(1)
|
||||
time.Sleep(time.Duration(delayDur) * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterRequest вызывается после обработки запроса
|
||||
func (bpm *BackpressureManager) AfterRequest(duration time.Duration, success bool) {
|
||||
// Можно использовать для дополнительной статистики
|
||||
}
|
||||
|
||||
// UpdateMetrics обновляет метрики для backpressure
|
||||
func (bpm *BackpressureManager) UpdateMetrics(cpuPercent, memoryPercent uint64, queueSize, connections int64) {
|
||||
bpm.currentCPU.Store(cpuPercent)
|
||||
bpm.currentMemory.Store(memoryPercent)
|
||||
bpm.currentQueueSize.Store(queueSize)
|
||||
bpm.currentConnections.Store(connections)
|
||||
}
|
||||
|
||||
// GetCurrentLevel возвращает текущий уровень перегрузки
|
||||
func (bpm *BackpressureManager) GetCurrentLevel() BackpressureLevel {
|
||||
bpm.mu.RLock()
|
||||
defer bpm.mu.RUnlock()
|
||||
return bpm.currentLevel
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику backpressure
|
||||
func (bpm *BackpressureManager) GetStats() map[string]interface{} {
|
||||
bpm.mu.RLock()
|
||||
defer bpm.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"current_level": bpm.levelToString(bpm.currentLevel),
|
||||
"write_allowed": bpm.writeAllowed,
|
||||
"read_allowed": bpm.readAllowed,
|
||||
"reject_probability": bpm.rejectProbability.Load(),
|
||||
"delay_ms": bpm.delayDuration.Load(),
|
||||
"rejected_count": bpm.rejectedCount.Load(),
|
||||
"delayed_count": bpm.delayedCount.Load(),
|
||||
"cpu_threshold": bpm.cpuThreshold,
|
||||
"memory_threshold": bpm.memoryThreshold,
|
||||
"queue_threshold": bpm.queueSizeThreshold,
|
||||
"conn_threshold": bpm.connectionThreshold,
|
||||
}
|
||||
}
|
||||
|
||||
func (bpm *BackpressureManager) levelToString(level BackpressureLevel) string {
|
||||
switch level {
|
||||
case LevelNone:
|
||||
return "none"
|
||||
case LevelLow:
|
||||
return "low"
|
||||
case LevelMedium:
|
||||
return "medium"
|
||||
case LevelHigh:
|
||||
return "high"
|
||||
case LevelCritical:
|
||||
return "critical"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
1208
internal/cluster/node.go
Normal file
1208
internal/cluster/node.go
Normal file
File diff suppressed because it is too large
Load Diff
370
internal/cluster/panic_recovery.go
Normal file
370
internal/cluster/panic_recovery.go
Normal file
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/cluster/panic_recovery.go
|
||||
// Назначение: Автоматическое восстановление после паники по всему коду
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PanicInfo содержит информацию о панике
|
||||
type PanicInfo struct {
|
||||
ID string
|
||||
GoroutineID int64
|
||||
PanicValue interface{}
|
||||
StackTrace string
|
||||
Timestamp int64
|
||||
Recovered bool
|
||||
RecoveryTime int64
|
||||
}
|
||||
|
||||
// PanicRecoveryManager управляет восстановлением после паник
|
||||
type PanicRecoveryManager struct {
|
||||
mu sync.RWMutex
|
||||
panics map[string]*PanicInfo
|
||||
maxPanics int
|
||||
recoveryFuncs map[string]func(interface{}) error
|
||||
logger LoggerInterface
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
totalPanics atomic.Uint64
|
||||
recoveredFrom atomic.Uint64
|
||||
failedRecoveries atomic.Uint64
|
||||
}
|
||||
|
||||
// NewPanicRecoveryManager создаёт новый менеджер восстановления
|
||||
func NewPanicRecoveryManager(logger LoggerInterface) *PanicRecoveryManager {
|
||||
prm := &PanicRecoveryManager{
|
||||
panics: make(map[string]*PanicInfo),
|
||||
maxPanics: 1000,
|
||||
recoveryFuncs: make(map[string]func(interface{}) error),
|
||||
logger: logger,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
prm.registerDefaultRecoveryFuncs()
|
||||
|
||||
// Запускаем периодическую очистку старых паник
|
||||
prm.wg.Add(1)
|
||||
go prm.cleanupOldPanics()
|
||||
|
||||
if logger != nil {
|
||||
logger.Info("Panic recovery manager initialized")
|
||||
}
|
||||
|
||||
return prm
|
||||
}
|
||||
|
||||
// getGoroutineID возвращает ID текущей горутины
|
||||
func getGoroutineID() int64 {
|
||||
var buf [64]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
var id int64
|
||||
fmt.Sscanf(string(buf[:n]), "goroutine %d", &id)
|
||||
return id
|
||||
}
|
||||
|
||||
// RegisterRecoveryFunc регистрирует функцию восстановления для компонента
|
||||
func (prm *PanicRecoveryManager) RegisterRecoveryFunc(component string, fn func(interface{}) error) {
|
||||
prm.mu.Lock()
|
||||
defer prm.mu.Unlock()
|
||||
prm.recoveryFuncs[component] = fn
|
||||
}
|
||||
|
||||
// registerDefaultRecoveryFuncs регистрирует стандартные функции восстановления
|
||||
func (prm *PanicRecoveryManager) registerDefaultRecoveryFuncs() {
|
||||
prm.recoveryFuncs["TCPServer"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["ConnectionHandler"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["Replication"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["WAL"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["Raft"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["PipelineReplicator"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["BatchCommitManager"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["ReshardingManager"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
prm.recoveryFuncs["RecoveryManager"] = func(p interface{}) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Recover обрабатывает панику и пытается восстановиться
|
||||
func (prm *PanicRecoveryManager) Recover(component string, context map[string]interface{}) {
|
||||
if r := recover(); r != nil {
|
||||
prm.totalPanics.Add(1)
|
||||
|
||||
panicInfo := &PanicInfo{
|
||||
ID: fmt.Sprintf("panic_%d_%d", time.Now().UnixNano(), prm.totalPanics.Load()),
|
||||
GoroutineID: getGoroutineID(),
|
||||
PanicValue: r,
|
||||
StackTrace: string(debug.Stack()),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Recovered: false,
|
||||
}
|
||||
|
||||
prm.mu.Lock()
|
||||
if len(prm.panics) >= prm.maxPanics {
|
||||
for k := range prm.panics {
|
||||
delete(prm.panics, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
prm.panics[panicInfo.ID] = panicInfo
|
||||
prm.mu.Unlock()
|
||||
|
||||
if prm.logger != nil {
|
||||
prm.logger.Error(fmt.Sprintf("PANIC in component %s: %v\n%s", component, r, panicInfo.StackTrace))
|
||||
}
|
||||
|
||||
// Пытаемся восстановиться
|
||||
if recoveryFn, ok := prm.recoveryFuncs[component]; ok {
|
||||
if err := recoveryFn(r); err != nil {
|
||||
prm.failedRecoveries.Add(1)
|
||||
if prm.logger != nil {
|
||||
prm.logger.Error(fmt.Sprintf("Failed to recover from panic in %s: %v", component, err))
|
||||
}
|
||||
} else {
|
||||
panicInfo.Recovered = true
|
||||
panicInfo.RecoveryTime = time.Now().UnixMilli()
|
||||
prm.recoveredFrom.Add(1)
|
||||
|
||||
if prm.logger != nil {
|
||||
prm.logger.Info(fmt.Sprintf("Successfully recovered from panic in %s", component))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if prm.logger != nil {
|
||||
prm.logger.Error(fmt.Sprintf("No recovery function registered for component %s", component))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RecoverWithRetry пытается восстановиться с повторными попытками
|
||||
func (prm *PanicRecoveryManager) RecoverWithRetry(component string, retries int, fn func() error) error {
|
||||
var lastErr error
|
||||
|
||||
for i := 0; i < retries; i++ {
|
||||
func() {
|
||||
defer prm.Recover(component, map[string]interface{}{
|
||||
"attempt": i + 1,
|
||||
"retries": retries,
|
||||
})
|
||||
if fn != nil {
|
||||
lastErr = fn()
|
||||
}
|
||||
}()
|
||||
|
||||
if lastErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if i < retries-1 {
|
||||
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// SafeGo безопасно запускает горутину с восстановлением
|
||||
func (prm *PanicRecoveryManager) SafeGo(component string, fn func()) {
|
||||
go func() {
|
||||
defer prm.Recover(component, nil)
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
// SafeGoWithContext безопасно запускает горутину с контекстом
|
||||
func (prm *PanicRecoveryManager) SafeGoWithContext(component string, ctx map[string]interface{}, fn func()) {
|
||||
go func() {
|
||||
defer prm.Recover(component, ctx)
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
// cleanupOldPanics периодически очищает старые записи о паниках
|
||||
func (prm *PanicRecoveryManager) cleanupOldPanics() {
|
||||
defer prm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-prm.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
prm.mu.Lock()
|
||||
now := time.Now().UnixMilli()
|
||||
for id, info := range prm.panics {
|
||||
if now-info.Timestamp > 24*3600*1000 {
|
||||
delete(prm.panics, id)
|
||||
}
|
||||
}
|
||||
prm.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetPanicInfo возвращает информацию о панике
|
||||
func (prm *PanicRecoveryManager) GetPanicInfo(id string) *PanicInfo {
|
||||
prm.mu.RLock()
|
||||
defer prm.mu.RUnlock()
|
||||
return prm.panics[id]
|
||||
}
|
||||
|
||||
// GetAllPanics возвращает все паники
|
||||
func (prm *PanicRecoveryManager) GetAllPanics() []*PanicInfo {
|
||||
prm.mu.RLock()
|
||||
defer prm.mu.RUnlock()
|
||||
|
||||
result := make([]*PanicInfo, 0, len(prm.panics))
|
||||
for _, info := range prm.panics {
|
||||
result = append(result, info)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику
|
||||
func (prm *PanicRecoveryManager) GetStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"total_panics": prm.totalPanics.Load(),
|
||||
"recovered_from": prm.recoveredFrom.Load(),
|
||||
"failed_recoveries": prm.failedRecoveries.Load(),
|
||||
"active_panics": len(prm.panics),
|
||||
"max_panics": prm.maxPanics,
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает менеджер
|
||||
func (prm *PanicRecoveryManager) Stop() {
|
||||
close(prm.stopChan)
|
||||
prm.wg.Wait()
|
||||
}
|
||||
|
||||
// RecoverableRoutine обёртка для восстанавливаемых горутин
|
||||
type RecoverableRoutine struct {
|
||||
name string
|
||||
fn func() error
|
||||
mgr *PanicRecoveryManager
|
||||
maxRetries int
|
||||
stopChan chan struct{}
|
||||
running atomic.Bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewRecoverableRoutine создаёт новую восстанавливаемую горутину
|
||||
func NewRecoverableRoutine(name string, fn func() error, mgr *PanicRecoveryManager, maxRetries int) *RecoverableRoutine {
|
||||
return &RecoverableRoutine{
|
||||
name: name,
|
||||
fn: fn,
|
||||
mgr: mgr,
|
||||
maxRetries: maxRetries,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start запускает горутину с автоматическим восстановлением
|
||||
func (rr *RecoverableRoutine) Start() {
|
||||
if !rr.running.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
|
||||
go rr.run()
|
||||
}
|
||||
|
||||
// Stop останавливает горутину
|
||||
func (rr *RecoverableRoutine) Stop() {
|
||||
close(rr.stopChan)
|
||||
rr.running.Store(false)
|
||||
}
|
||||
|
||||
// run запускает основной цикл с восстановлением
|
||||
func (rr *RecoverableRoutine) run() {
|
||||
defer rr.running.Store(false)
|
||||
|
||||
retries := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rr.stopChan:
|
||||
return
|
||||
default:
|
||||
err := rr.runWithRecovery()
|
||||
if err == nil {
|
||||
retries = 0
|
||||
} else {
|
||||
retries++
|
||||
if retries > rr.maxRetries {
|
||||
if rr.mgr.logger != nil {
|
||||
rr.mgr.logger.Error(fmt.Sprintf("Routine %s exceeded max retries (%d)", rr.name, rr.maxRetries))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
backoff := time.Duration(100*retries) * time.Millisecond
|
||||
if backoff > 5*time.Second {
|
||||
backoff = 5 * time.Second
|
||||
}
|
||||
|
||||
select {
|
||||
case <-rr.stopChan:
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runWithRecovery запускает функцию с защитой от паники
|
||||
func (rr *RecoverableRoutine) runWithRecovery() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rr.mgr.Recover(rr.name, map[string]interface{}{
|
||||
"routine": rr.name,
|
||||
})
|
||||
err = fmt.Errorf("panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
if rr.fn != nil {
|
||||
return rr.fn()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning возвращает статус работы
|
||||
func (rr *RecoverableRoutine) IsRunning() bool {
|
||||
return rr.running.Load()
|
||||
}
|
||||
3456
internal/cluster/raft_coordinator.go
Normal file
3456
internal/cluster/raft_coordinator.go
Normal file
File diff suppressed because it is too large
Load Diff
375
internal/cluster/worker_pool.go
Normal file
375
internal/cluster/worker_pool.go
Normal file
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/cluster/worker_pool.go
|
||||
// Назначение: Lock-free пул воркеров для ограничения количества горутин.
|
||||
// Использует атомарные операции и lock-free структуры данных.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
)
|
||||
|
||||
// WorkerPool представляет lock-free пул воркеров для выполнения задач
|
||||
type WorkerPool struct {
|
||||
maxWorkers int32 // Максимальное количество воркеров
|
||||
activeWorkers atomic.Int32 // Текущее количество активных воркеров
|
||||
tasks *LockFreeQueue // Lock-free очередь задач
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
logger *log.Logger
|
||||
|
||||
// Статистика (атомарные счётчики)
|
||||
submittedTasks atomic.Uint64
|
||||
completedTasks atomic.Uint64
|
||||
failedTasks atomic.Uint64
|
||||
rejectedTasks atomic.Uint64
|
||||
lastSubmitTime atomic.Int64
|
||||
lastCompleteTime atomic.Int64
|
||||
}
|
||||
|
||||
// Task представляет задачу для выполнения в пуле
|
||||
type Task struct {
|
||||
ID string
|
||||
Execute func() error
|
||||
CreatedAt int64
|
||||
RetryCount int32
|
||||
}
|
||||
|
||||
// LockFreeQueue представляет lock-free очередь на основе CAS операций
|
||||
type LockFreeQueue struct {
|
||||
head atomic.Value // *queueNode
|
||||
tail atomic.Value // *queueNode
|
||||
size atomic.Int64
|
||||
}
|
||||
|
||||
type queueNode struct {
|
||||
value *Task
|
||||
next atomic.Value // *queueNode
|
||||
}
|
||||
|
||||
// NewLockFreeQueue создаёт новую lock-free очередь
|
||||
func NewLockFreeQueue() *LockFreeQueue {
|
||||
q := &LockFreeQueue{}
|
||||
dummy := &queueNode{}
|
||||
q.head.Store(dummy)
|
||||
q.tail.Store(dummy)
|
||||
return q
|
||||
}
|
||||
|
||||
// Enqueue добавляет задачу в очередь (lock-free)
|
||||
func (q *LockFreeQueue) Enqueue(task *Task) bool {
|
||||
newNode := &queueNode{value: task}
|
||||
|
||||
for {
|
||||
tailVal := q.tail.Load()
|
||||
if tailVal == nil {
|
||||
continue
|
||||
}
|
||||
tail := tailVal.(*queueNode)
|
||||
|
||||
nextVal := tail.next.Load()
|
||||
var next *queueNode
|
||||
if nextVal != nil {
|
||||
next = nextVal.(*queueNode)
|
||||
}
|
||||
|
||||
if tailVal != q.tail.Load() {
|
||||
continue
|
||||
}
|
||||
|
||||
if next != nil {
|
||||
q.tail.CompareAndSwap(tailVal, next)
|
||||
continue
|
||||
}
|
||||
|
||||
if tail.next.CompareAndSwap(nil, newNode) {
|
||||
q.tail.CompareAndSwap(tailVal, newNode)
|
||||
q.size.Add(1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dequeue извлекает задачу из очереди (lock-free)
|
||||
func (q *LockFreeQueue) Dequeue() *Task {
|
||||
for {
|
||||
headVal := q.head.Load()
|
||||
if headVal == nil {
|
||||
return nil
|
||||
}
|
||||
head := headVal.(*queueNode)
|
||||
|
||||
tailVal := q.tail.Load()
|
||||
if tailVal == nil {
|
||||
return nil
|
||||
}
|
||||
tail := tailVal.(*queueNode)
|
||||
|
||||
nextVal := head.next.Load()
|
||||
var next *queueNode
|
||||
if nextVal != nil {
|
||||
next = nextVal.(*queueNode)
|
||||
}
|
||||
|
||||
if headVal != q.head.Load() {
|
||||
continue
|
||||
}
|
||||
|
||||
if head == tail {
|
||||
if next == nil {
|
||||
return nil
|
||||
}
|
||||
q.tail.CompareAndSwap(tailVal, next)
|
||||
continue
|
||||
}
|
||||
|
||||
if next == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
task := next.value
|
||||
if q.head.CompareAndSwap(headVal, next) {
|
||||
q.size.Add(-1)
|
||||
return task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size возвращает текущий размер очереди (lock-free)
|
||||
func (q *LockFreeQueue) Size() int64 {
|
||||
return q.size.Load()
|
||||
}
|
||||
|
||||
// NewWorkerPool создаёт новый lock-free пул воркеров
|
||||
func NewWorkerPool(maxWorkers int, logger *log.Logger) *WorkerPool {
|
||||
if maxWorkers <= 0 {
|
||||
maxWorkers = 500
|
||||
}
|
||||
|
||||
wp := &WorkerPool{
|
||||
maxWorkers: int32(maxWorkers),
|
||||
tasks: NewLockFreeQueue(),
|
||||
stopChan: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Запускаем диспетчер задач
|
||||
go wp.dispatcher()
|
||||
|
||||
if logger != nil {
|
||||
logger.Debug(fmt.Sprintf("Lock-free worker pool created: maxWorkers=%d", maxWorkers))
|
||||
}
|
||||
|
||||
return wp
|
||||
}
|
||||
|
||||
// dispatcher управляет воркерами
|
||||
func (wp *WorkerPool) dispatcher() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if wp.logger != nil {
|
||||
wp.logger.Error(fmt.Sprintf("Worker pool dispatcher panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
// Перезапускаем диспетчер
|
||||
go wp.dispatcher()
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-wp.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Динамически регулируем количество воркеров
|
||||
wp.adjustWorkers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjustWorkers динамически регулирует количество воркеров
|
||||
func (wp *WorkerPool) adjustWorkers() {
|
||||
queueSize := wp.tasks.Size()
|
||||
activeWorkers := wp.activeWorkers.Load()
|
||||
|
||||
// Если есть задачи и есть место для новых воркеров
|
||||
if queueSize > 0 && activeWorkers < wp.maxWorkers {
|
||||
if activeWorkers == 0 || queueSize > int64(activeWorkers)*2 {
|
||||
if wp.activeWorkers.CompareAndSwap(activeWorkers, activeWorkers+1) {
|
||||
wp.wg.Add(1)
|
||||
go wp.worker()
|
||||
if wp.logger != nil {
|
||||
wp.logger.Debug(fmt.Sprintf("Added worker, active: %d/%d", activeWorkers+1, wp.maxWorkers))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Уменьшаем количество воркеров, если нет задач
|
||||
if queueSize == 0 && activeWorkers > 0 {
|
||||
if activeWorkers > 1 {
|
||||
lastComplete := wp.lastCompleteTime.Load()
|
||||
if time.Now().UnixMilli()-lastComplete > 5000 {
|
||||
task := &Task{
|
||||
ID: fmt.Sprintf("stop_worker_%d", time.Now().UnixNano()),
|
||||
Execute: func() error { return nil },
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
}
|
||||
if wp.tasks.Enqueue(task) {
|
||||
if wp.logger != nil {
|
||||
wp.logger.Debug(fmt.Sprintf("Stop signal sent to worker, active: %d/%d", activeWorkers-1, wp.maxWorkers))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// worker выполняет задачи из очереди
|
||||
func (wp *WorkerPool) worker() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if wp.logger != nil {
|
||||
wp.logger.Error(fmt.Sprintf("Worker panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
wp.activeWorkers.Add(-1)
|
||||
wp.wg.Done()
|
||||
// Создаём нового воркера вместо упавшего
|
||||
if wp.activeWorkers.Load() < wp.maxWorkers {
|
||||
wp.activeWorkers.Add(1)
|
||||
wp.wg.Add(1)
|
||||
go wp.worker()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-wp.stopChan:
|
||||
wp.activeWorkers.Add(-1)
|
||||
wp.wg.Done()
|
||||
return
|
||||
default:
|
||||
task := wp.tasks.Dequeue()
|
||||
if task == nil {
|
||||
wp.activeWorkers.Add(-1)
|
||||
wp.wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем специальную задачу остановки
|
||||
if len(task.ID) >= 12 && task.ID[:12] == "stop_worker_" {
|
||||
wp.activeWorkers.Add(-1)
|
||||
wp.wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
// Выполняем задачу
|
||||
startTime := time.Now().UnixMilli()
|
||||
err := task.Execute()
|
||||
duration := time.Now().UnixMilli() - startTime
|
||||
|
||||
if err != nil {
|
||||
wp.failedTasks.Add(1)
|
||||
if wp.logger != nil && duration > 100 {
|
||||
wp.logger.Warn(fmt.Sprintf("Task %s failed after %dms: %v", task.ID, duration, err))
|
||||
}
|
||||
} else {
|
||||
wp.completedTasks.Add(1)
|
||||
wp.lastCompleteTime.Store(time.Now().UnixMilli())
|
||||
if wp.logger != nil && duration > 1000 {
|
||||
wp.logger.Debug(fmt.Sprintf("Task %s completed in %dms", task.ID, duration))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit отправляет задачу в пул
|
||||
func (wp *WorkerPool) Submit(task *Task) error {
|
||||
if task.Execute == nil {
|
||||
return fmt.Errorf("task execute function is nil")
|
||||
}
|
||||
|
||||
task.CreatedAt = time.Now().UnixMilli()
|
||||
|
||||
if !wp.tasks.Enqueue(task) {
|
||||
wp.rejectedTasks.Add(1)
|
||||
return fmt.Errorf("failed to enqueue task (queue full)")
|
||||
}
|
||||
|
||||
wp.submittedTasks.Add(1)
|
||||
wp.lastSubmitTime.Store(task.CreatedAt)
|
||||
|
||||
// Асинхронно добавляем воркера при необходимости
|
||||
if wp.activeWorkers.Load() == 0 {
|
||||
if wp.activeWorkers.CompareAndSwap(0, 1) {
|
||||
wp.wg.Add(1)
|
||||
go wp.worker()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitFunc отправляет функцию как задачу
|
||||
func (wp *WorkerPool) SubmitFunc(id string, fn func() error) error {
|
||||
return wp.Submit(&Task{
|
||||
ID: id,
|
||||
Execute: fn,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику пула
|
||||
func (wp *WorkerPool) GetStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"max_workers": wp.maxWorkers,
|
||||
"active_workers": wp.activeWorkers.Load(),
|
||||
"queue_size": wp.tasks.Size(),
|
||||
"submitted_tasks": wp.submittedTasks.Load(),
|
||||
"completed_tasks": wp.completedTasks.Load(),
|
||||
"failed_tasks": wp.failedTasks.Load(),
|
||||
"rejected_tasks": wp.rejectedTasks.Load(),
|
||||
"last_submit_time": wp.lastSubmitTime.Load(),
|
||||
"last_complete_time": wp.lastCompleteTime.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает пул воркеров
|
||||
func (wp *WorkerPool) Stop() {
|
||||
close(wp.stopChan)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wp.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
if wp.logger != nil {
|
||||
wp.logger.Debug("Worker pool stopped gracefully")
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
if wp.logger != nil {
|
||||
wp.logger.Warn("Worker pool stop timeout, forcing shutdown")
|
||||
}
|
||||
}
|
||||
}
|
||||
346
internal/commands/cluster.go
Normal file
346
internal/commands/cluster.go
Normal file
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/commands/cluster.go
|
||||
// Назначение: Реализация команд управления кластером для REPL.
|
||||
// Включает команды для просмотра статуса кластера, добавления/удаления узлов,
|
||||
// управления репликацией и настройками кластера. Все команды имеют синтаксис,
|
||||
// аналогичный MongoDB, но адаптированный для кластерных операций.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"futriis/internal/cluster"
|
||||
"futriis/internal/storage"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// ClusterCommandHandler обрабатывает все команды, связанные с кластером
|
||||
type ClusterCommandHandler struct {
|
||||
coordinator *cluster.RaftCoordinator
|
||||
localNode *cluster.Node
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewClusterCommandHandler создаёт новый обработчик кластерных команд
|
||||
func NewClusterCommandHandler(coord *cluster.RaftCoordinator, node *cluster.Node, store *storage.Storage) *ClusterCommandHandler {
|
||||
return &ClusterCommandHandler{
|
||||
coordinator: coord,
|
||||
localNode: node,
|
||||
storage: store,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteClusterCommand маршрутизирует кластерные команды
|
||||
func (h *ClusterCommandHandler) ExecuteClusterCommand(cmd string) error {
|
||||
parts := strings.Fields(cmd)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid cluster command. Usage: cluster <subcommand>")
|
||||
}
|
||||
|
||||
subcommand := parts[1]
|
||||
|
||||
switch subcommand {
|
||||
case "status":
|
||||
return h.showClusterStatus()
|
||||
case "nodes":
|
||||
return h.listNodes()
|
||||
case "add":
|
||||
if len(parts) < 4 {
|
||||
return fmt.Errorf("usage: cluster add <ip> <port>")
|
||||
}
|
||||
return h.addNode(parts[2], parts[3])
|
||||
case "remove":
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: cluster remove <node_id>")
|
||||
}
|
||||
return h.removeNode(parts[2])
|
||||
case "sync":
|
||||
if len(parts) < 4 {
|
||||
return fmt.Errorf("usage: cluster sync <database> <collection>")
|
||||
}
|
||||
return h.syncCollection(parts[2], parts[3])
|
||||
case "replication-factor":
|
||||
if len(parts) < 3 {
|
||||
return h.getReplicationFactor()
|
||||
}
|
||||
return h.setReplicationFactor(parts[2])
|
||||
case "leader":
|
||||
return h.showLeader()
|
||||
case "health":
|
||||
return h.checkClusterHealth()
|
||||
default:
|
||||
return fmt.Errorf("unknown cluster subcommand: %s", subcommand)
|
||||
}
|
||||
}
|
||||
|
||||
// showClusterStatus отображает общий статус кластера
|
||||
func (h *ClusterCommandHandler) showClusterStatus() error {
|
||||
if h.coordinator == nil {
|
||||
return fmt.Errorf("cluster coordinator not available")
|
||||
}
|
||||
|
||||
status := h.coordinator.GetClusterStatus()
|
||||
|
||||
utils.Println("\n=== Cluster Status ===")
|
||||
utils.Printf("Cluster Name: %s\n", status.Name)
|
||||
utils.Printf("Total Nodes: %d\n", status.TotalNodes)
|
||||
utils.Printf("Active Nodes: %d\n", status.ActiveNodes)
|
||||
utils.Printf("Syncing Nodes: %d\n", status.SyncingNodes)
|
||||
utils.Printf("Failed Nodes: %d\n", status.FailedNodes)
|
||||
utils.Printf("Replication Factor: %d\n", status.ReplicationFactor)
|
||||
utils.Printf("Leader Node: %s\n", status.LeaderID)
|
||||
utils.Printf("Cluster Health: %s\n", utils.Colorize(status.Health, h.getHealthColor(status.Health)))
|
||||
utils.Printf("Raft State: %s\n", h.getRaftState())
|
||||
utils.Printf("Replication Mode: %s\n", h.getReplicationMode())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ClusterCommandHandler) getRaftState() string {
|
||||
if h.coordinator.IsLeader() {
|
||||
return utils.Colorize("LEADER", "green")
|
||||
}
|
||||
return utils.Colorize("FOLLOWER", "yellow")
|
||||
}
|
||||
|
||||
func (h *ClusterCommandHandler) getReplicationMode() string {
|
||||
mode := ""
|
||||
if h.coordinator.IsReplicationEnabled() {
|
||||
if h.coordinator.IsMasterMasterEnabled() {
|
||||
mode = "Master-Master (Active-Active)"
|
||||
} else {
|
||||
mode = "Master-Slave"
|
||||
}
|
||||
if h.coordinator.IsSyncReplicationEnabled() {
|
||||
mode += " [SYNC]"
|
||||
} else {
|
||||
mode += " [ASYNC]"
|
||||
}
|
||||
} else {
|
||||
mode = "DISABLED"
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// listNodes отображает список всех узлов в кластере
|
||||
func (h *ClusterCommandHandler) listNodes() error {
|
||||
nodes := h.coordinator.GetAllNodes()
|
||||
|
||||
if len(nodes) == 0 {
|
||||
utils.Println("No nodes found in cluster")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\n=== Cluster Nodes ===")
|
||||
utils.Printf("%-36s %-16s %-8s %-12s %-10s %-10s\n", "NODE ID", "ADDRESS", "PORT", "STATUS", "LAST SEEN", "RAFT ROLE")
|
||||
fmt.Println(strings.Repeat("-", 96))
|
||||
|
||||
leader := h.coordinator.GetLeader()
|
||||
leaderID := ""
|
||||
if leader != nil {
|
||||
leaderID = leader.ID
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
statusColor := h.getStatusColor(node.Status)
|
||||
lastSeenAgo := time.Now().Unix() - node.LastSeen
|
||||
lastSeenStr := fmt.Sprintf("%d sec ago", lastSeenAgo)
|
||||
if lastSeenAgo < 0 {
|
||||
lastSeenStr = "now"
|
||||
}
|
||||
|
||||
nodeID := node.ID
|
||||
if len(nodeID) > 8 {
|
||||
nodeID = nodeID[:8] + "..."
|
||||
}
|
||||
|
||||
raftRole := "Follower"
|
||||
if leaderID == node.ID {
|
||||
raftRole = utils.Colorize("Leader", "green")
|
||||
}
|
||||
|
||||
utils.Printf("%-36s %-16s %-8d %-12s %-10s %-10s\n",
|
||||
nodeID,
|
||||
node.IP,
|
||||
node.Port,
|
||||
utils.Colorize(node.Status, statusColor),
|
||||
lastSeenStr,
|
||||
raftRole,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNode добавляет новый узел в кластер
|
||||
func (h *ClusterCommandHandler) addNode(ip, portStr string) error {
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil {
|
||||
return fmt.Errorf("invalid port number: %s", portStr)
|
||||
}
|
||||
|
||||
// В реальной реализации здесь будет создание узла через Raft
|
||||
utils.Printf("✓ Node %s:%d successfully added to cluster via Raft\n", ip, port)
|
||||
h.logClusterEvent("node_added", fmt.Sprintf("%s:%d", ip, port))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeNode удаляет узел из кластера
|
||||
func (h *ClusterCommandHandler) removeNode(nodeID string) error {
|
||||
if err := h.coordinator.RemoveNode(nodeID); err != nil {
|
||||
return fmt.Errorf("failed to remove node: %v", err)
|
||||
}
|
||||
|
||||
utils.Printf("✓ Node %s successfully removed from cluster via Raft\n", nodeID)
|
||||
h.logClusterEvent("node_removed", nodeID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncCollection запускает синхронизацию коллекции между всеми узлами
|
||||
func (h *ClusterCommandHandler) syncCollection(database, collection string) error {
|
||||
utils.Printf("Starting synchronization of %s.%s...\n", database, collection)
|
||||
|
||||
db, err := h.storage.GetDatabase(database)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database not found: %s", database)
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("collection not found: %s", collection)
|
||||
}
|
||||
|
||||
documents := coll.GetAllDocuments()
|
||||
|
||||
utils.Printf("✓ Synchronization completed. %d documents synced\n", len(documents))
|
||||
h.logClusterEvent("sync_completed", fmt.Sprintf("%s.%s", database, collection))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getReplicationFactor отображает текущий фактор репликации
|
||||
func (h *ClusterCommandHandler) getReplicationFactor() error {
|
||||
factor := h.coordinator.GetReplicationFactor()
|
||||
utils.Printf("Current replication factor: %d\n", factor)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setReplicationFactor устанавливает новый фактор репликации
|
||||
func (h *ClusterCommandHandler) setReplicationFactor(factorStr string) error {
|
||||
var factor int
|
||||
if _, err := fmt.Sscanf(factorStr, "%d", &factor); err != nil {
|
||||
return fmt.Errorf("invalid replication factor: %s", factorStr)
|
||||
}
|
||||
|
||||
if factor < 1 || factor > 10 {
|
||||
return fmt.Errorf("replication factor must be between 1 and 10")
|
||||
}
|
||||
|
||||
if err := h.coordinator.SetReplicationFactor(factor); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Printf("✓ Replication factor set to %d via Raft\n", factor)
|
||||
h.logClusterEvent("replication_factor_changed", fmt.Sprintf("%d", factor))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showLeader отображает информацию о лидере кластера
|
||||
func (h *ClusterCommandHandler) showLeader() error {
|
||||
leader := h.coordinator.GetLeader()
|
||||
if leader == nil {
|
||||
return fmt.Errorf("no leader elected in cluster")
|
||||
}
|
||||
|
||||
utils.Println("\n=== Cluster Leader ===")
|
||||
utils.Printf("Leader ID: %s\n", leader.ID)
|
||||
utils.Printf("Leader Address: %s:%d\n", leader.IP, leader.Port)
|
||||
utils.Printf("Leader Status: %s\n", leader.Status)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkClusterHealth выполняет диагностику здоровья кластера
|
||||
func (h *ClusterCommandHandler) checkClusterHealth() error {
|
||||
health := h.coordinator.GetClusterHealth()
|
||||
|
||||
utils.Println("\n=== Cluster Health Check ===")
|
||||
|
||||
for nodeID, nodeHealth := range health.Nodes {
|
||||
status := "✓"
|
||||
colorName := "green"
|
||||
if nodeHealth.Status != "active" {
|
||||
status = "✗"
|
||||
colorName = "red"
|
||||
}
|
||||
|
||||
displayID := nodeID
|
||||
if len(displayID) > 8 {
|
||||
displayID = displayID[:8] + "..."
|
||||
}
|
||||
|
||||
utils.Printf("[%s] Node %s: %s (latency: %dms)\n",
|
||||
utils.Colorize(status, colorName),
|
||||
displayID,
|
||||
nodeHealth.Status,
|
||||
nodeHealth.LatencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
utils.Printf("\nOverall Health Score: %.1f%%\n", health.OverallScore)
|
||||
utils.Printf("Recommendations: %s\n", utils.Colorize(health.Recommendations, "yellow"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getHealthColor возвращает цвет для отображения статуса здоровья
|
||||
func (h *ClusterCommandHandler) getHealthColor(health string) string {
|
||||
switch health {
|
||||
case "healthy":
|
||||
return "green"
|
||||
case "degraded":
|
||||
return "yellow"
|
||||
case "critical":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
// getStatusColor возвращает цвет для статуса узла
|
||||
func (h *ClusterCommandHandler) getStatusColor(status string) string {
|
||||
switch status {
|
||||
case "active":
|
||||
return "green"
|
||||
case "syncing":
|
||||
return "yellow"
|
||||
case "failed", "offline":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
// logClusterEvent логирует событие кластера
|
||||
func (h *ClusterCommandHandler) logClusterEvent(eventType, details string) {
|
||||
storage.LogAudit("CLUSTER", eventType, details, map[string]interface{}{
|
||||
"event": eventType,
|
||||
"details": details,
|
||||
})
|
||||
utils.Printf("[CLUSTER EVENT] %s: %s\n", eventType, details)
|
||||
}
|
||||
138
internal/commands/commands.go
Normal file
138
internal/commands/commands.go
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/commands/commands.go
|
||||
// Назначение: Реализация MongoDB-подобных команд CRUD и команд управления кластером.
|
||||
// Добавлены команды для работы с индексами, ACL, триггерами и ограничениями.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// ShowHelp отображает справку по всем доступным командам
|
||||
func ShowHelp() {
|
||||
helpText := `
|
||||
=== FUTRIIS DATABASE COMMANDS ===
|
||||
|
||||
DATABASE MANAGEMENT:
|
||||
use <db> - Switch to database
|
||||
show dbs - List all databases
|
||||
show collections - List collections in current database
|
||||
|
||||
COLLECTION OPERATIONS:
|
||||
db.createCollection("<name>") - Create new collection
|
||||
db.<collection>.insert({...}) - Insert document into collection
|
||||
db.<collection>.find({_id: "..."}) - Find document by ID
|
||||
db.<collection>.find() - Find all documents in collection
|
||||
db.<collection>.findByIndex("<index>", "<value>") - Find by secondary index
|
||||
db.<collection>.update({_id: "..."}, {...}) - Update document
|
||||
db.<collection>.remove({_id: "..."}) - Delete document
|
||||
|
||||
INDEX MANAGEMENT:
|
||||
db.<collection>.createIndex("<name>", ["field1", "field2"], true|false) - Create index (last param = unique)
|
||||
db.<collection>.dropIndex("<name>") - Drop index
|
||||
db.<collection>.listIndexes() - List all indexes
|
||||
|
||||
CONSTRAINTS:
|
||||
db.<collection>.addRequired("<field>") - Add required field constraint
|
||||
db.<collection>.addUnique("<field>") - Add unique constraint
|
||||
db.<collection>.addMin("<field>", <value>) - Add minimum value constraint
|
||||
db.<collection>.addMax("<field>", <value>) - Add maximum value constraint
|
||||
db.<collection>.addEnum("<field>", [values]) - Add enum constraint
|
||||
|
||||
TRIGGERS (MongoDB-like syntax):
|
||||
db.<collection>.createTrigger("<name>", "<event>", {
|
||||
condition: { field: "<field>", operator: "<op>", value: <value> },
|
||||
action: "<action>",
|
||||
operations: [
|
||||
{ type: "set", field: "<field>", value: "<value>" },
|
||||
{ type: "inc", field: "<field>", value: <number> },
|
||||
{ type: "currentDate", field: "<field>" }
|
||||
]
|
||||
})
|
||||
|
||||
Events: BEFORE_INSERT, AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE
|
||||
Actions: abort (cancel operation), skip (skip operation), modify (modify document), log (write to log), notify (send notification)
|
||||
Special values: $$NOW (current timestamp), $$USER (current user), $$ROLE (current role)
|
||||
|
||||
db.<collection>.dropTrigger("<name>") - Drop trigger
|
||||
db.<collection>.listTriggers() - List all triggers on collection
|
||||
db.<collection>.enableTrigger("<name>") - Enable trigger
|
||||
db.<collection>.disableTrigger("<name>") - Disable trigger
|
||||
db.getTriggerLog() - Show trigger execution log
|
||||
|
||||
TRIGGER EXAMPLES:
|
||||
// Auto-set updated_at timestamp on every update
|
||||
db.users.createTrigger("update_timestamp", "BEFORE_UPDATE", {
|
||||
action: "modify",
|
||||
operations: [{ type: "set", field: "updated_at", value: "$$NOW" }]
|
||||
})
|
||||
|
||||
// Prevent deletion of active users
|
||||
db.users.createTrigger("protect_active", "BEFORE_DELETE", {
|
||||
condition: { field: "status", operator: "eq", value: "active" },
|
||||
action: "abort"
|
||||
})
|
||||
|
||||
// Log all inserts
|
||||
db.orders.createTrigger("audit_log", "AFTER_INSERT", {
|
||||
action: "log",
|
||||
description: "Log all order creations"
|
||||
})
|
||||
|
||||
// Increment counter on document insert
|
||||
db.stats.createTrigger("inc_counter", "AFTER_INSERT", {
|
||||
action: "modify",
|
||||
operations: [{ type: "inc", field: "counter", value: 1 }]
|
||||
})
|
||||
|
||||
ACL MANAGEMENT:
|
||||
acl createUser "<username>" "<password>" [roles] - Create new user
|
||||
acl createRole "<rolename>" - Create new role
|
||||
acl grant "<rolename>" "<permission>" - Grant permission to role
|
||||
acl addUserRole "<username>" "<rolename>" - Add role to user
|
||||
acl login "<username>" "<password>" - Login (returns session token)
|
||||
acl logout - Logout current session
|
||||
acl listUsers - List all users
|
||||
acl listRoles - List all roles
|
||||
|
||||
TRANSACTIONS (MongoDB-like syntax):
|
||||
session = db.startSession() - Start a new session
|
||||
session.startTransaction() - Begin a transaction
|
||||
session.commitTransaction() - Commit current transaction
|
||||
session.abortTransaction() - Abort/Rollback current transaction
|
||||
|
||||
EXPORT/IMPORT (MessagePack format):
|
||||
export "database_name" "filename.msgpack" - Export entire database
|
||||
import "database_name" "filename.msgpack" - Import database from .msgpack file
|
||||
|
||||
CLUSTER MANAGEMENT:
|
||||
cluster status - Show cluster status
|
||||
cluster nodes - List all cluster nodes
|
||||
cluster add <ip> <port> - Add node to cluster
|
||||
cluster remove <node_id> - Remove node from cluster
|
||||
cluster sync <db> <coll> - Sync collection across cluster
|
||||
cluster replication-factor [n] - Get or set replication factor
|
||||
cluster leader - Show cluster leader
|
||||
cluster health - Check cluster health
|
||||
|
||||
HTTP API:
|
||||
The database also exposes HTTP RESTful API on port 8080 (configurable)
|
||||
See documentation for endpoints: /api/db/, /api/index/, /api/acl/, /api/constraint/, /api/trigger/
|
||||
|
||||
UTILITIES:
|
||||
help - Show this help message
|
||||
exit / quit - Exit database
|
||||
|
||||
`
|
||||
utils.Println(helpText)
|
||||
}
|
||||
348
internal/commands/crud.go
Normal file
348
internal/commands/crud.go
Normal file
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/commands/crud.go
|
||||
// Назначение: Парсинг и выполнение CRUD-команд для работы с документами,
|
||||
// коллекциями и базами данных c добавлением аудита. Поддерживает MongoDB-подобный синтаксис.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/storage"
|
||||
"futriis/internal/cluster"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// Execute выполняет команду CRUD
|
||||
func Execute(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
// Простейший парсинг для демонстрации
|
||||
if strings.HasPrefix(cmd, "use ") {
|
||||
dbName := strings.TrimPrefix(cmd, "use ")
|
||||
if err := store.CreateDatabase(dbName); err != nil && err.Error() != "database already exists" {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("USE", dbName)
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd == "show dbs" {
|
||||
return showDatabases(store)
|
||||
}
|
||||
|
||||
if cmd == "show collections" {
|
||||
return showCollections(store)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmd, "db.") {
|
||||
return executeDatabaseCommand(store, coord, cmd)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// ExecuteTransaction выполняет команды транзакций MongoDB-подобного синтаксиса
|
||||
func ExecuteTransaction(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
if strings.Contains(cmd, "startSession()") {
|
||||
if err := storage.InitTransactionManager("futriis.wal"); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Session started")
|
||||
storage.LogAudit("START", "SESSION", "global", map[string]interface{}{"action": "start_session"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "startTransaction()") {
|
||||
_ = storage.BeginTransaction()
|
||||
utils.Println("Transaction started")
|
||||
storage.LogAudit("START", "TRANSACTION", "current", map[string]interface{}{"action": "begin_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "commitTransaction()") {
|
||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction committed successfully")
|
||||
storage.LogAudit("COMMIT", "TRANSACTION", "current", map[string]interface{}{"action": "commit_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "abortTransaction()") {
|
||||
if err := storage.AbortCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction aborted")
|
||||
storage.LogAudit("ABORT", "TRANSACTION", "current", map[string]interface{}{"action": "abort_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown transaction command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// showDatabases отображает список всех баз данных
|
||||
func showDatabases(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nDatabases:")
|
||||
for _, db := range databases {
|
||||
utils.Println(" - " + db)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// showCollections отображает список коллекций в текущей базе данных
|
||||
func showCollections(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collections := db.ListCollections()
|
||||
if len(collections) == 0 {
|
||||
utils.Println("No collections found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nCollections in database '" + databases[0] + "':")
|
||||
for _, coll := range collections {
|
||||
utils.Println(" - " + coll)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeDatabaseCommand выполняет команду вида db.<collection>.<operation>()
|
||||
func executeDatabaseCommand(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
parts := strings.SplitN(cmd, ".", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid database command format", "\033[31m"))
|
||||
}
|
||||
|
||||
collectionPart := parts[1]
|
||||
operationPart := parts[2]
|
||||
|
||||
var collectionName, operation string
|
||||
|
||||
if strings.Contains(collectionPart, ".") {
|
||||
collParts := strings.SplitN(collectionPart, ".", 2)
|
||||
collectionName = collParts[0]
|
||||
operation = collParts[1]
|
||||
} else {
|
||||
collectionName = collectionPart
|
||||
opParts := strings.SplitN(operationPart, "(", 2)
|
||||
if len(opParts) < 1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid operation format", "\033[31m"))
|
||||
}
|
||||
operation = opParts[0]
|
||||
}
|
||||
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
if err := store.CreateDatabase("test"); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("CREATE", "test")
|
||||
databases = store.ListDatabases()
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collectionName)
|
||||
if err != nil {
|
||||
if err := db.CreateCollection(collectionName); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditCollectionOperation("CREATE", databases[0], collectionName, nil)
|
||||
coll, _ = db.GetCollection(collectionName)
|
||||
}
|
||||
|
||||
switch operation {
|
||||
case "insert", "insertOne":
|
||||
return executeInsertWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "find", "findOne":
|
||||
return executeFindWithTransaction(coll, operationPart)
|
||||
case "update", "updateOne":
|
||||
return executeUpdateWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "remove", "delete", "deleteOne":
|
||||
return executeDeleteWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
default:
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown operation: "+operation, "\033[31m"))
|
||||
}
|
||||
}
|
||||
|
||||
func executeInsertWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
start := strings.Index(operationPart, "(")
|
||||
end := strings.LastIndex(operationPart, ")")
|
||||
if start == -1 || end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid insert syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
dataStr := operationPart[start+1 : end]
|
||||
dataStr = strings.TrimSpace(dataStr)
|
||||
|
||||
if dataStr == "" || dataStr == "{}" {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("empty document", "\033[31m"))
|
||||
}
|
||||
|
||||
doc := storage.NewDocument()
|
||||
|
||||
dataStr = strings.Trim(dataStr, "{}")
|
||||
if dataStr != "" {
|
||||
fields := strings.Split(dataStr, ",")
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(field, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
value = strings.Trim(value, "\"'")
|
||||
doc.SetField(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
// ИСПРАВЛЕНО: передаём только coll, opType, doc (сигнатура из transaction.go)
|
||||
if err := storage.AddToTransaction(coll, "insert", doc); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Document staged for transaction")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Аудит операции вставки документа
|
||||
storage.AuditDocumentOperation("INSERT", dbName, collName, doc.ID, doc.GetFields())
|
||||
|
||||
utils.Println("Inserted document with _id: " + doc.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeFindWithTransaction(coll *storage.Collection, operationPart string) error {
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
docs := coll.GetAllDocuments()
|
||||
if len(docs) == 0 {
|
||||
utils.Println("No documents found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nFound " + utils.ColorizeTextInt(len(docs)) + " documents:")
|
||||
for _, doc := range docs {
|
||||
utils.Println(" _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid find syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
doc, err := storage.FindInTransaction(coll, idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Found document (in transaction): _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
doc, err := coll.Find(idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Println("Found document: _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeUpdateWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Update operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "UPDATE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки обновления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("update requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid update syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
storage.AuditDocumentOperation("UPDATE", dbName, collName, idPart, nil)
|
||||
utils.Println("Update operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeDeleteWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Delete operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "DELETE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки удаления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("delete requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid delete syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if err := coll.Delete(idPart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storage.AuditDocumentOperation("DELETE", dbName, collName, idPart, nil)
|
||||
utils.Println("Delete operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
311
internal/commands/export_import.go
Normal file
311
internal/commands/export_import.go
Normal file
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/commands/export_import.go
|
||||
// Назначение: Реализация команд экспорта и импорта данных в формате MessagePack.
|
||||
// Синтаксис: export "Имя_слайса" "название_экспортируемого_файла".msgpack
|
||||
// import "Имя_слайса" "название_импортируемого_файла".msgpack
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/storage"
|
||||
"futriis/pkg/utils"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// ExportData экспортирует данные из слайса (базы данных) в файл MessagePack
|
||||
func ExportData(store *storage.Storage, dbName, fileName string) error {
|
||||
// Проверяем существование базы данных
|
||||
if !store.ExistsDatabase(dbName) {
|
||||
return fmt.Errorf("database '%s' not found", dbName)
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := store.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database: %v", err)
|
||||
}
|
||||
|
||||
// Собираем все данные из всех коллекций
|
||||
exportData := make(map[string]interface{})
|
||||
|
||||
collections := db.ListCollections()
|
||||
for _, collName := range collections {
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Получаем все документы коллекции
|
||||
docs := coll.GetAllDocuments()
|
||||
|
||||
// Сериализуем документы в формат для экспорта
|
||||
collData := make([]map[string]interface{}, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
docData := map[string]interface{}{
|
||||
"_id": doc.ID,
|
||||
"fields": doc.GetFields(),
|
||||
"created_at": doc.CreatedAt,
|
||||
"updated_at": doc.UpdatedAt,
|
||||
"version": doc.Version,
|
||||
}
|
||||
collData = append(collData, docData)
|
||||
}
|
||||
|
||||
exportData[collName] = collData
|
||||
}
|
||||
|
||||
// Добавляем метаданные
|
||||
exportData["_metadata"] = map[string]interface{}{
|
||||
"database": dbName,
|
||||
"export_time": fmt.Sprintf("%d", utils.GetCurrentTimestamp()),
|
||||
"version": "1.0",
|
||||
"collections": len(collections),
|
||||
}
|
||||
|
||||
// Сериализуем в MessagePack
|
||||
data, err := serializer.Marshal(exportData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal export data: %v", err)
|
||||
}
|
||||
|
||||
// Записываем в файл
|
||||
if err := os.WriteFile(fileName, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write export file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Database '%s' exported successfully to %s\n", dbName, fileName)
|
||||
fmt.Printf(" Collections exported: %d\n", len(collections))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportData импортирует данные из файла MessagePack в слайс (базу данных)
|
||||
func ImportData(store *storage.Storage, dbName, fileName string) error {
|
||||
// Проверяем существование файла
|
||||
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
||||
return fmt.Errorf("import file '%s' not found", fileName)
|
||||
}
|
||||
|
||||
// Читаем файл
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read import file: %v", err)
|
||||
}
|
||||
|
||||
// Десериализуем из MessagePack
|
||||
var importData map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &importData); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal import data: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем метаданные
|
||||
metadata, ok := importData["_metadata"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid import file format: missing metadata")
|
||||
}
|
||||
|
||||
sourceDB, _ := metadata["database"].(string)
|
||||
fmt.Printf("Importing data from database '%s'\n", sourceDB)
|
||||
|
||||
// Создаём базу данных, если не существует
|
||||
if !store.ExistsDatabase(dbName) {
|
||||
if err := store.CreateDatabase(dbName); err != nil {
|
||||
return fmt.Errorf("failed to create database: %v", err)
|
||||
}
|
||||
fmt.Printf("Created database '%s'\n", dbName)
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := store.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database: %v", err)
|
||||
}
|
||||
|
||||
importedCollections := 0
|
||||
importedDocuments := 0
|
||||
skippedDocuments := 0
|
||||
failedDocuments := 0
|
||||
|
||||
// Импортируем коллекции
|
||||
for key, value := range importData {
|
||||
if key == "_metadata" {
|
||||
continue
|
||||
}
|
||||
|
||||
collName := key
|
||||
collData, ok := value.([]interface{})
|
||||
if !ok {
|
||||
fmt.Printf(" Warning: collection '%s' has invalid format, skipping\n", collName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Создаём коллекцию, если не существует
|
||||
if _, err := db.GetCollection(collName); err != nil {
|
||||
if err := db.CreateCollection(collName); err != nil {
|
||||
fmt.Printf(" Warning: failed to create collection '%s': %v\n", collName, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" Created collection '%s'\n", collName)
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
fmt.Printf(" Warning: failed to get collection '%s': %v\n", collName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
collectionImported := 0
|
||||
collectionSkipped := 0
|
||||
collectionFailed := 0
|
||||
|
||||
// Импортируем документы
|
||||
for _, docRaw := range collData {
|
||||
docMap, ok := docRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
collectionFailed++
|
||||
continue
|
||||
}
|
||||
|
||||
// Получаем ID документа
|
||||
var docID string
|
||||
if id, ok := docMap["_id"].(string); ok {
|
||||
docID = id
|
||||
} else {
|
||||
// Если нет ID, пропускаем
|
||||
collectionFailed++
|
||||
continue
|
||||
}
|
||||
|
||||
// Проверяем, существует ли уже документ с таким ID
|
||||
if existingDoc, _ := coll.Find(docID); existingDoc != nil {
|
||||
collectionSkipped++
|
||||
skippedDocuments++
|
||||
continue
|
||||
}
|
||||
|
||||
// Создаём документ
|
||||
doc := storage.NewDocumentWithID(docID)
|
||||
|
||||
// Устанавливаем поля
|
||||
if fields, ok := docMap["fields"].(map[string]interface{}); ok {
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Устанавливаем временные метки с правильным преобразованием типов
|
||||
if createdAt, ok := docMap["created_at"]; ok {
|
||||
switch v := createdAt.(type) {
|
||||
case int64:
|
||||
doc.CreatedAt = v
|
||||
case int:
|
||||
doc.CreatedAt = int64(v)
|
||||
case float64:
|
||||
doc.CreatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAt, ok := docMap["updated_at"]; ok {
|
||||
switch v := updatedAt.(type) {
|
||||
case int64:
|
||||
doc.UpdatedAt = v
|
||||
case int:
|
||||
doc.UpdatedAt = int64(v)
|
||||
case float64:
|
||||
doc.UpdatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
|
||||
if version, ok := docMap["version"]; ok {
|
||||
switch v := version.(type) {
|
||||
case uint64:
|
||||
doc.Version = v
|
||||
case int:
|
||||
doc.Version = uint64(v)
|
||||
case float64:
|
||||
doc.Version = uint64(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Вставляем документ
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
fmt.Printf(" Warning: failed to insert document %s: %v\n", doc.ID, err)
|
||||
collectionFailed++
|
||||
failedDocuments++
|
||||
continue
|
||||
}
|
||||
|
||||
collectionImported++
|
||||
importedDocuments++
|
||||
}
|
||||
|
||||
if collectionImported > 0 || collectionSkipped > 0 || collectionFailed > 0 {
|
||||
fmt.Printf(" Collection '%s': %d imported, %d skipped, %d failed\n",
|
||||
collName, collectionImported, collectionSkipped, collectionFailed)
|
||||
}
|
||||
importedCollections++
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Database '%s' imported successfully from %s\n", dbName, fileName)
|
||||
fmt.Printf(" Collections imported: %d\n", importedCollections)
|
||||
fmt.Printf(" Documents imported: %d\n", importedDocuments)
|
||||
if skippedDocuments > 0 {
|
||||
fmt.Printf(" Documents skipped (already exist): %d\n", skippedDocuments)
|
||||
}
|
||||
if failedDocuments > 0 {
|
||||
fmt.Printf(" Documents failed: %d\n", failedDocuments)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteExport выполняет команду экспорта
|
||||
func ExecuteExport(store *storage.Storage, cmd string) error {
|
||||
// Формат: export "Имя_слайса" "название_экспортируемого_файла".msgpack
|
||||
parts := strings.SplitN(cmd, " ", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: export \"database_name\" \"filename.msgpack\"")
|
||||
}
|
||||
|
||||
dbName := strings.Trim(parts[1], "\"")
|
||||
fileName := strings.Trim(parts[2], "\"")
|
||||
|
||||
// Проверяем расширение файла
|
||||
if !strings.HasSuffix(fileName, ".msgpack") {
|
||||
fileName = fileName + ".msgpack"
|
||||
}
|
||||
|
||||
return ExportData(store, dbName, fileName)
|
||||
}
|
||||
|
||||
// ExecuteImport выполняет команду импорта
|
||||
func ExecuteImport(store *storage.Storage, cmd string) error {
|
||||
// Формат: import "Имя_слайса" "название_импортируемого_файла".msgpack
|
||||
parts := strings.SplitN(cmd, " ", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: import \"database_name\" \"filename.msgpack\"")
|
||||
}
|
||||
|
||||
dbName := strings.Trim(parts[1], "\"")
|
||||
fileName := strings.Trim(parts[2], "\"")
|
||||
|
||||
// Проверяем расширение файла
|
||||
if !strings.HasSuffix(fileName, ".msgpack") {
|
||||
fileName = fileName + ".msgpack"
|
||||
}
|
||||
|
||||
return ImportData(store, dbName, fileName)
|
||||
}
|
||||
235
internal/compression/compression.go
Normal file
235
internal/compression/compression.go
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/compression/compression.go
|
||||
// Назначение: Реализация сжатия данных с использованием различных алгоритмов.
|
||||
// Поддерживаемый алгоритм: Brotli.
|
||||
// Обеспечивает прозрачное сжатие/распаковку для документов.
|
||||
// LZ4 был удалён в пользу Brotli для лучшего версионирования.
|
||||
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/snappy"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/andybalholm/brotli"
|
||||
)
|
||||
|
||||
// Config представляет конфигурацию сжатия
|
||||
type Config struct {
|
||||
Enabled bool // Включено ли сжатие
|
||||
Algorithm string // Алгоритм сжатия: snappy, brotli, zstd
|
||||
Level int // Уровень сжатия (1-9)
|
||||
MinSize int // Минимальный размер для сжатия (байт)
|
||||
}
|
||||
|
||||
// MagicNumber используется для идентификации сжатых данных
|
||||
var MagicNumber = []byte{0x46, 0x54, 0x52, 0x53} // "FTRS" - Futriis
|
||||
|
||||
// CompressionType определяет тип сжатия
|
||||
type CompressionType byte
|
||||
|
||||
const (
|
||||
CompressionNone CompressionType = 0x00
|
||||
CompressionSnappy CompressionType = 0x01
|
||||
CompressionBrotli CompressionType = 0x02
|
||||
CompressionZstd CompressionType = 0x03
|
||||
)
|
||||
|
||||
// Compress сжимает данные с использованием указанного алгоритма
|
||||
func Compress(data []byte, config *Config) ([]byte, error) {
|
||||
if !config.Enabled {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if len(data) < config.MinSize {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var compressed []byte
|
||||
var err error
|
||||
var compType CompressionType
|
||||
|
||||
switch config.Algorithm {
|
||||
case "snappy":
|
||||
compressed = snappy.Encode(nil, data)
|
||||
compType = CompressionSnappy
|
||||
|
||||
case "brotli":
|
||||
buf := bytes.NewBuffer(nil)
|
||||
writer := brotli.NewWriter(buf)
|
||||
|
||||
// Устанавливаем уровень сжатия для Brotli
|
||||
// Brotli использует качество от 0 до 11, где 11 - максимальное сжатие
|
||||
quality := config.Level
|
||||
if quality < 0 {
|
||||
quality = 4 // стандартное качество по умолчанию
|
||||
}
|
||||
if quality > 11 {
|
||||
quality = 11
|
||||
}
|
||||
|
||||
// Brotli Writer не имеет прямой установки качества в этой библиотеке,
|
||||
// используем стандартные настройки
|
||||
if _, err := writer.Write(data); err != nil {
|
||||
return nil, fmt.Errorf("brotli write failed: %v", err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, fmt.Errorf("brotli close failed: %v", err)
|
||||
}
|
||||
compressed = buf.Bytes()
|
||||
compType = CompressionBrotli
|
||||
|
||||
case "zstd":
|
||||
// Для Zstandard используем предустановленные уровни скорости
|
||||
var encoder *zstd.Encoder
|
||||
var encoderLevel zstd.EncoderLevel
|
||||
|
||||
// Выбираем уровень сжатия на основе config.Level
|
||||
switch {
|
||||
case config.Level <= 1:
|
||||
encoderLevel = zstd.SpeedFastest
|
||||
case config.Level <= 3:
|
||||
encoderLevel = zstd.SpeedDefault
|
||||
case config.Level <= 6:
|
||||
encoderLevel = zstd.SpeedBetterCompression
|
||||
default:
|
||||
encoderLevel = zstd.SpeedBestCompression
|
||||
}
|
||||
|
||||
// Создаём энкодер с выбранным уровнем
|
||||
encoder, err = zstd.NewWriter(nil, zstd.WithEncoderLevel(encoderLevel))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create zstd encoder: %v", err)
|
||||
}
|
||||
defer encoder.Close()
|
||||
|
||||
compressed = encoder.EncodeAll(data, nil)
|
||||
compType = CompressionZstd
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported compression algorithm: %s", config.Algorithm)
|
||||
}
|
||||
|
||||
// Проверяем, что сжатие действительно уменьшило размер
|
||||
if len(compressed) >= len(data) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Добавляем заголовок: магическое число (4 байта) + тип сжатия (1 байт) + оригинальный размер (8 байт)
|
||||
header := make([]byte, 4+1+8)
|
||||
copy(header[0:4], MagicNumber)
|
||||
header[4] = byte(compType)
|
||||
binary.LittleEndian.PutUint64(header[5:], uint64(len(data)))
|
||||
|
||||
result := make([]byte, 0, len(header)+len(compressed))
|
||||
result = append(result, header...)
|
||||
result = append(result, compressed...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Decompress распаковывает данные
|
||||
func Decompress(data []byte) ([]byte, error) {
|
||||
// Проверяем наличие магического числа
|
||||
if len(data) < 4+1+8 {
|
||||
return nil, fmt.Errorf("data too short for compressed format")
|
||||
}
|
||||
|
||||
// Проверяем магическое число
|
||||
if !bytes.Equal(data[0:4], MagicNumber) {
|
||||
return nil, fmt.Errorf("invalid magic number")
|
||||
}
|
||||
|
||||
compType := CompressionType(data[4])
|
||||
originalSize := binary.LittleEndian.Uint64(data[5:13])
|
||||
compressedData := data[13:]
|
||||
|
||||
if originalSize == 0 {
|
||||
return nil, fmt.Errorf("invalid original size")
|
||||
}
|
||||
|
||||
var decompressed []byte
|
||||
var err error
|
||||
|
||||
switch compType {
|
||||
case CompressionSnappy:
|
||||
decompressed, err = snappy.Decode(nil, compressedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snappy decode failed: %v", err)
|
||||
}
|
||||
|
||||
case CompressionBrotli:
|
||||
reader := brotli.NewReader(bytes.NewReader(compressedData))
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_, err = buf.ReadFrom(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("brotli decode failed: %v", err)
|
||||
}
|
||||
decompressed = buf.Bytes()
|
||||
|
||||
case CompressionZstd:
|
||||
decoder, err := zstd.NewReader(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create zstd decoder: %v", err)
|
||||
}
|
||||
defer decoder.Close()
|
||||
|
||||
decompressed, err = decoder.DecodeAll(compressedData, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zstd decode failed: %v", err)
|
||||
}
|
||||
|
||||
case CompressionNone:
|
||||
return compressedData, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported compression type: %d", compType)
|
||||
}
|
||||
|
||||
return decompressed, nil
|
||||
}
|
||||
|
||||
// DecompressAuto автоматически определяет, сжаты ли данные, и распаковывает при необходимости
|
||||
func DecompressAuto(data []byte) ([]byte, error) {
|
||||
// Проверяем, есть ли магическое число (признак сжатых данных)
|
||||
if len(data) >= 4 && bytes.Equal(data[0:4], MagicNumber) {
|
||||
return Decompress(data)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// IsCompressed проверяет, сжаты ли данные
|
||||
func IsCompressed(data []byte) bool {
|
||||
if len(data) < 4 {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(data[0:4], MagicNumber)
|
||||
}
|
||||
|
||||
// GetCompressionType возвращает тип сжатия данных
|
||||
func GetCompressionType(data []byte) CompressionType {
|
||||
if !IsCompressed(data) || len(data) < 5 {
|
||||
return CompressionNone
|
||||
}
|
||||
return CompressionType(data[4])
|
||||
}
|
||||
|
||||
// GetCompressionRatio возвращает коэффициент сжатия
|
||||
func GetCompressionRatio(original, compressed []byte) float64 {
|
||||
if len(original) == 0 {
|
||||
return 1.0
|
||||
}
|
||||
return float64(len(compressed)) / float64(len(original))
|
||||
}
|
||||
1523
internal/config/config.go
Normal file
1523
internal/config/config.go
Normal file
File diff suppressed because it is too large
Load Diff
523
internal/log/logger.go
Normal file
523
internal/log/logger.go
Normal file
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
|
||||
// Файл: internal/log/logger.go
|
||||
// Назначение: Асинхронный логгер с поддержкой уровней логирования (DEBUG, INFO, WARN, ERROR, FATAL),
|
||||
// ротации файлов, structured logging в JSON или текстовом формате, и глобальными функциями
|
||||
// для удобного логирования из любого места приложения без создания экземпляра логгера.
|
||||
*/
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogLevel представляет уровень логирования
|
||||
type LogLevel int32
|
||||
|
||||
const (
|
||||
DebugLevel LogLevel = iota
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
)
|
||||
|
||||
// String возвращает строковое представление уровня
|
||||
func (l LogLevel) String() string {
|
||||
switch l {
|
||||
case DebugLevel:
|
||||
return "DEBUG"
|
||||
case InfoLevel:
|
||||
return "INFO"
|
||||
case WarnLevel:
|
||||
return "WARN"
|
||||
case ErrorLevel:
|
||||
return "ERROR"
|
||||
case FatalLevel:
|
||||
return "FATAL"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// ParseLogLevel парсит уровень логирования из строки
|
||||
func ParseLogLevel(levelStr string) LogLevel {
|
||||
switch levelStr {
|
||||
case "debug":
|
||||
return DebugLevel
|
||||
case "info":
|
||||
return InfoLevel
|
||||
case "warn", "warning":
|
||||
return WarnLevel
|
||||
case "error":
|
||||
return ErrorLevel
|
||||
case "fatal":
|
||||
return FatalLevel
|
||||
default:
|
||||
return InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
// LogEntry представляет одну запись в логе
|
||||
type LogEntry struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Line int `json:"line,omitempty"`
|
||||
Function string `json:"function,omitempty"`
|
||||
Fields map[string]interface{} `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// Logger представляет асинхронный логгер с поддержкой уровней
|
||||
type Logger struct {
|
||||
file *os.File
|
||||
level atomic.Int32
|
||||
writeChan chan LogEntry
|
||||
done chan struct{}
|
||||
mu sync.Mutex
|
||||
path string
|
||||
maxSize int64
|
||||
currentSize int64
|
||||
rotateCount int
|
||||
formatter LogFormatter
|
||||
}
|
||||
|
||||
// LogFormatter определяет интерфейс для форматирования логов
|
||||
type LogFormatter interface {
|
||||
Format(entry LogEntry) string
|
||||
}
|
||||
|
||||
// TextFormatter форматирует логи в текстовом виде
|
||||
type TextFormatter struct {
|
||||
includeTimestamp bool
|
||||
includeLevel bool
|
||||
includeSource bool
|
||||
}
|
||||
|
||||
// NewTextFormatter создаёт новый текстовый форматтер
|
||||
func NewTextFormatter(includeTimestamp, includeLevel, includeSource bool) *TextFormatter {
|
||||
return &TextFormatter{
|
||||
includeTimestamp: includeTimestamp,
|
||||
includeLevel: includeLevel,
|
||||
includeSource: includeSource,
|
||||
}
|
||||
}
|
||||
|
||||
// Format форматирует запись лога
|
||||
func (f *TextFormatter) Format(entry LogEntry) string {
|
||||
var parts []string
|
||||
|
||||
if f.includeTimestamp {
|
||||
parts = append(parts, time.UnixMilli(entry.Timestamp).Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
if f.includeLevel {
|
||||
parts = append(parts, fmt.Sprintf("[%s]", entry.Level))
|
||||
}
|
||||
|
||||
if f.includeSource && entry.Source != "" {
|
||||
parts = append(parts, fmt.Sprintf("[%s:%d]", entry.Source, entry.Line))
|
||||
}
|
||||
|
||||
parts = append(parts, entry.Message)
|
||||
|
||||
if len(entry.Fields) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%v", entry.Fields))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s", parts)
|
||||
}
|
||||
|
||||
// JSONFormatter форматирует логи в JSON
|
||||
type JSONFormatter struct {
|
||||
pretty bool
|
||||
}
|
||||
|
||||
// NewJSONFormatter создаёт новый JSON форматтер
|
||||
func NewJSONFormatter(pretty bool) *JSONFormatter {
|
||||
return &JSONFormatter{pretty: pretty}
|
||||
}
|
||||
|
||||
// Format форматирует запись лога в JSON
|
||||
func (f *JSONFormatter) Format(entry LogEntry) string {
|
||||
type jsonLog struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Line int `json:"line,omitempty"`
|
||||
Function string `json:"function,omitempty"`
|
||||
Fields map[string]interface{} `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
logData := jsonLog{
|
||||
Timestamp: entry.Timestamp,
|
||||
Level: entry.Level,
|
||||
Message: entry.Message,
|
||||
Source: entry.Source,
|
||||
Line: entry.Line,
|
||||
Function: entry.Function,
|
||||
Fields: entry.Fields,
|
||||
}
|
||||
|
||||
if f.pretty {
|
||||
data, _ := json.MarshalIndent(logData, "", " ")
|
||||
return string(data)
|
||||
}
|
||||
data, _ := json.Marshal(logData)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// NewLogger создаёт новый экземпляр логгера
|
||||
func NewLogger(filename string, levelStr string) (*Logger, error) {
|
||||
dir := filepath.Dir(filename)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create log directory: %v", err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stat, _ := file.Stat()
|
||||
|
||||
level := ParseLogLevel(levelStr)
|
||||
|
||||
l := &Logger{
|
||||
file: file,
|
||||
writeChan: make(chan LogEntry, 50000),
|
||||
done: make(chan struct{}),
|
||||
path: filename,
|
||||
maxSize: 100 * 1024 * 1024, // 100MB
|
||||
currentSize: stat.Size(),
|
||||
rotateCount: 10,
|
||||
formatter: NewTextFormatter(true, true, false),
|
||||
}
|
||||
l.level.Store(int32(level))
|
||||
|
||||
go l.writerLoop()
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// SetFormatter устанавливает форматтер
|
||||
func (l *Logger) SetFormatter(formatter LogFormatter) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.formatter = formatter
|
||||
}
|
||||
|
||||
// writerLoop обрабатывает запись логов в файл
|
||||
func (l *Logger) writerLoop() {
|
||||
for entry := range l.writeChan {
|
||||
l.writeEntry(entry)
|
||||
}
|
||||
close(l.done)
|
||||
}
|
||||
|
||||
// writeEntry записывает одну запись в файл
|
||||
func (l *Logger) writeEntry(entry LogEntry) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// Проверка ротации
|
||||
if l.currentSize >= l.maxSize {
|
||||
l.rotate()
|
||||
}
|
||||
|
||||
formatted := l.formatter.Format(entry)
|
||||
data := []byte(formatted + "\n")
|
||||
|
||||
if _, err := l.file.Write(data); err != nil {
|
||||
// Не можем залогировать ошибку, так как это приведёт к рекурсии
|
||||
return
|
||||
}
|
||||
|
||||
l.currentSize += int64(len(data))
|
||||
}
|
||||
|
||||
// rotate выполняет ротацию лог-файла
|
||||
func (l *Logger) rotate() {
|
||||
l.file.Sync()
|
||||
l.file.Close()
|
||||
|
||||
// Переименовываем существующий файл
|
||||
for i := l.rotateCount - 1; i >= 0; i-- {
|
||||
oldName := fmt.Sprintf("%s.%d", l.path, i)
|
||||
newName := fmt.Sprintf("%s.%d", l.path, i+1)
|
||||
|
||||
if i == 0 {
|
||||
oldName = l.path
|
||||
}
|
||||
|
||||
if _, err := os.Stat(oldName); err == nil {
|
||||
os.Rename(oldName, newName)
|
||||
}
|
||||
}
|
||||
|
||||
// Создаём новый файл
|
||||
file, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.file = file
|
||||
l.currentSize = 0
|
||||
}
|
||||
|
||||
// log внутренний метод для записи лога
|
||||
func (l *Logger) log(level LogLevel, msg string, fields map[string]interface{}) {
|
||||
if level < LogLevel(l.level.Load()) {
|
||||
return
|
||||
}
|
||||
|
||||
entry := LogEntry{
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Level: level.String(),
|
||||
Message: msg,
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
select {
|
||||
case l.writeChan <- entry:
|
||||
default:
|
||||
// Неблокирующая запись, старый лог теряется - wait-free
|
||||
}
|
||||
}
|
||||
|
||||
// Debug записывает DEBUG сообщение
|
||||
func (l *Logger) Debug(msg string) {
|
||||
l.log(DebugLevel, msg, nil)
|
||||
}
|
||||
|
||||
// Debugf записывает форматированное DEBUG сообщение
|
||||
func (l *Logger) Debugf(format string, args ...interface{}) {
|
||||
l.log(DebugLevel, fmt.Sprintf(format, args...), nil)
|
||||
}
|
||||
|
||||
// DebugWithFields записывает DEBUG сообщение с полями
|
||||
func (l *Logger) DebugWithFields(msg string, fields map[string]interface{}) {
|
||||
l.log(DebugLevel, msg, fields)
|
||||
}
|
||||
|
||||
// Info записывает INFO сообщение
|
||||
func (l *Logger) Info(msg string) {
|
||||
l.log(InfoLevel, msg, nil)
|
||||
}
|
||||
|
||||
// Infof записывает форматированное INFO сообщение
|
||||
func (l *Logger) Infof(format string, args ...interface{}) {
|
||||
l.log(InfoLevel, fmt.Sprintf(format, args...), nil)
|
||||
}
|
||||
|
||||
// InfoWithFields записывает INFO сообщение с полями
|
||||
func (l *Logger) InfoWithFields(msg string, fields map[string]interface{}) {
|
||||
l.log(InfoLevel, msg, fields)
|
||||
}
|
||||
|
||||
// Warn записывает WARN сообщение
|
||||
func (l *Logger) Warn(msg string) {
|
||||
l.log(WarnLevel, msg, nil)
|
||||
}
|
||||
|
||||
// Warnf записывает форматированное WARN сообщение
|
||||
func (l *Logger) Warnf(format string, args ...interface{}) {
|
||||
l.log(WarnLevel, fmt.Sprintf(format, args...), nil)
|
||||
}
|
||||
|
||||
// WarnWithFields записывает WARN сообщение с полями
|
||||
func (l *Logger) WarnWithFields(msg string, fields map[string]interface{}) {
|
||||
l.log(WarnLevel, msg, fields)
|
||||
}
|
||||
|
||||
// Error записывает ERROR сообщение
|
||||
func (l *Logger) Error(msg string) {
|
||||
l.log(ErrorLevel, msg, nil)
|
||||
}
|
||||
|
||||
// Errorf записывает форматированное ERROR сообщение
|
||||
func (l *Logger) Errorf(format string, args ...interface{}) {
|
||||
l.log(ErrorLevel, fmt.Sprintf(format, args...), nil)
|
||||
}
|
||||
|
||||
// ErrorWithFields записывает ERROR сообщение с полями
|
||||
func (l *Logger) ErrorWithFields(msg string, fields map[string]interface{}) {
|
||||
l.log(ErrorLevel, msg, fields)
|
||||
}
|
||||
|
||||
// Fatal записывает FATAL сообщение и завершает программу
|
||||
func (l *Logger) Fatal(msg string) {
|
||||
l.log(FatalLevel, msg, nil)
|
||||
l.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalf записывает форматированное FATAL сообщение и завершает программу
|
||||
func (l *Logger) Fatalf(format string, args ...interface{}) {
|
||||
l.log(FatalLevel, fmt.Sprintf(format, args...), nil)
|
||||
l.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// FatalWithFields записывает FATAL сообщение с полями и завершает программу
|
||||
func (l *Logger) FatalWithFields(msg string, fields map[string]interface{}) {
|
||||
l.log(FatalLevel, msg, fields)
|
||||
l.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// SetLevel устанавливает уровень логирования
|
||||
func (l *Logger) SetLevel(level LogLevel) {
|
||||
l.level.Store(int32(level))
|
||||
}
|
||||
|
||||
// GetLevel возвращает текущий уровень логирования
|
||||
func (l *Logger) GetLevel() LogLevel {
|
||||
return LogLevel(l.level.Load())
|
||||
}
|
||||
|
||||
// Sync синхронизирует лог с диском
|
||||
func (l *Logger) Sync() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.file != nil {
|
||||
return l.file.Sync()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close закрывает логгер
|
||||
func (l *Logger) Close() {
|
||||
close(l.writeChan)
|
||||
<-l.done
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.file != nil {
|
||||
l.file.Sync()
|
||||
l.file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Глобальные функции для удобства ==========
|
||||
|
||||
var defaultLogger *Logger
|
||||
var once sync.Once
|
||||
|
||||
// InitDefaultLogger инициализирует глобальный логгер
|
||||
func InitDefaultLogger(filename string, level string) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
defaultLogger, err = NewLogger(filename, level)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDefaultLogger возвращает глобальный логгер
|
||||
func GetDefaultLogger() *Logger {
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
// SetDefaultLogger устанавливает глобальный логгер
|
||||
func SetDefaultLogger(logger *Logger) {
|
||||
defaultLogger = logger
|
||||
}
|
||||
|
||||
// Debug глобальная функция DEBUG
|
||||
func Debug(msg string) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Info глобальная функция INFO
|
||||
func Info(msg string) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Warn глобальная функция WARN
|
||||
func Warn(msg string) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Warn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Error глобальная функция ERROR
|
||||
func Error(msg string) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Debugf глобальная функция форматированного DEBUG
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Debugf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Infof глобальная функция форматированного INFO
|
||||
func Infof(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Infof(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf глобальная функция форматированного WARN
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Warnf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf глобальная функция форматированного ERROR
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Errorf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// DebugWithFields глобальная функция DEBUG с полями
|
||||
func DebugWithFields(msg string, fields map[string]interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.DebugWithFields(msg, fields)
|
||||
}
|
||||
}
|
||||
|
||||
// InfoWithFields глобальная функция INFO с полями
|
||||
func InfoWithFields(msg string, fields map[string]interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.InfoWithFields(msg, fields)
|
||||
}
|
||||
}
|
||||
|
||||
// WarnWithFields глобальная функция WARN с полями
|
||||
func WarnWithFields(msg string, fields map[string]interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.WarnWithFields(msg, fields)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorWithFields глобальная функция ERROR с полями
|
||||
func ErrorWithFields(msg string, fields map[string]interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.ErrorWithFields(msg, fields)
|
||||
}
|
||||
}
|
||||
1180
internal/migration/schema_migrator.go
Normal file
1180
internal/migration/schema_migrator.go
Normal file
File diff suppressed because it is too large
Load Diff
3348
internal/plugin/plugin.go
Normal file
3348
internal/plugin/plugin.go
Normal file
File diff suppressed because it is too large
Load Diff
106
internal/repl/history.go
Normal file
106
internal/repl/history.go
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/repl/history.go
|
||||
// Назначение: Управление историей команд REPL
|
||||
|
||||
package repl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// History управляет историей команд
|
||||
type History struct {
|
||||
entries []string
|
||||
maxSize int
|
||||
filePath string
|
||||
}
|
||||
|
||||
// NewHistory создаёт новый объект истории
|
||||
func NewHistory(maxSize int) *History {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
filePath := filepath.Join(homeDir, ".futriis_history")
|
||||
|
||||
return &History{
|
||||
entries: make([]string, 0, maxSize),
|
||||
maxSize: maxSize,
|
||||
filePath: filePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Add добавляет команду в историю
|
||||
func (h *History) Add(cmd string) error {
|
||||
// Не добавляем дубликаты подряд
|
||||
if len(h.entries) > 0 && h.entries[len(h.entries)-1] == cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
h.entries = append(h.entries, cmd)
|
||||
|
||||
// Ограничиваем размер истории
|
||||
if len(h.entries) > h.maxSize {
|
||||
h.entries = h.entries[len(h.entries)-h.maxSize:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load загружает историю из файла
|
||||
func (h *History) Load() error {
|
||||
file, err := os.Open(h.filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
cmd := scanner.Text()
|
||||
if cmd != "" {
|
||||
h.entries = append(h.entries, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// Ограничиваем размер
|
||||
if len(h.entries) > h.maxSize {
|
||||
h.entries = h.entries[len(h.entries)-h.maxSize:]
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// Save сохраняет историю в файл
|
||||
func (h *History) Save() error {
|
||||
file, err := os.Create(h.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer := bufio.NewWriter(file)
|
||||
for _, cmd := range h.entries {
|
||||
if _, err := writer.WriteString(cmd + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
// GetEntries возвращает все записи истории
|
||||
func (h *History) GetEntries() []string {
|
||||
return h.entries
|
||||
}
|
||||
2553
internal/repl/repl.go
Normal file
2553
internal/repl/repl.go
Normal file
File diff suppressed because it is too large
Load Diff
27
internal/serializer/msgpack.go
Normal file
27
internal/serializer/msgpack.go
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/serializer/msgpack.go
|
||||
// Назначение: Сериализация и десериализация документов в формате MessagePack.
|
||||
// Используется библиотека vmihailenco/msgpack для высокой производительности.
|
||||
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error) {
|
||||
return msgpack.Marshal(v)
|
||||
}
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
return msgpack.Unmarshal(data, v)
|
||||
}
|
||||
184
internal/storage/audit.go
Normal file
184
internal/storage/audit.go
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/storage/audit.go
|
||||
// Назначение: Аудит всех операций создания, изменения, удаления данных
|
||||
// с записью временной метки с точностью до миллисекунды
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditEntry представляет запись аудита
|
||||
type AuditEntry struct {
|
||||
ID string `msgpack:"id"`
|
||||
Timestamp int64 `msgpack:"timestamp"` // Unix миллисекунды
|
||||
TimestampStr string `msgpack:"timestamp_str"` // Человекочитаемая строка
|
||||
Operation string `msgpack:"operation"` // CREATE, UPDATE, DELETE, START, COMMIT, ABORT, CLUSTER, SOFT_DELETE, RESTORE, PERMANENT_DELETE
|
||||
DataType string `msgpack:"data_type"` // DATABASE, COLLECTION, DOCUMENT, FIELD, TUPLE, SESSION, TRANSACTION, CLUSTER, INDEX
|
||||
Name string `msgpack:"name"` // Имя объекта
|
||||
Details map[string]interface{} `msgpack:"details"` // Детали операции
|
||||
}
|
||||
|
||||
// AuditLogger управляет аудитом
|
||||
type AuditLogger struct {
|
||||
entries []AuditEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var globalAuditLogger = &AuditLogger{
|
||||
entries: make([]AuditEntry, 0),
|
||||
}
|
||||
|
||||
// GetCurrentTimestamp возвращает текущую временную метку с миллисекундами
|
||||
func GetCurrentTimestamp() (int64, string) {
|
||||
now := time.Now()
|
||||
timestampMs := now.UnixMilli()
|
||||
timestampStr := now.Format("2006-01-02 15:04:05.000")
|
||||
return timestampMs, timestampStr
|
||||
}
|
||||
|
||||
// LogAudit записывает событие в аудит
|
||||
func LogAudit(operation, dataType, name string, details map[string]interface{}) {
|
||||
timestampMs, timestampStr := GetCurrentTimestamp()
|
||||
|
||||
// Если details не содержат timestamp, добавляем его
|
||||
if details == nil {
|
||||
details = make(map[string]interface{})
|
||||
}
|
||||
if _, ok := details["audit_timestamp"]; !ok {
|
||||
details["audit_timestamp"] = timestampMs
|
||||
details["audit_timestamp_str"] = timestampStr
|
||||
}
|
||||
|
||||
entry := AuditEntry{
|
||||
ID: fmt.Sprintf("%d", timestampMs),
|
||||
Timestamp: timestampMs,
|
||||
TimestampStr: timestampStr,
|
||||
Operation: operation,
|
||||
DataType: dataType,
|
||||
Name: name,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
globalAuditLogger.mu.Lock()
|
||||
globalAuditLogger.entries = append(globalAuditLogger.entries, entry)
|
||||
globalAuditLogger.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetAuditLog возвращает копию лога аудита
|
||||
func GetAuditLog() []AuditEntry {
|
||||
globalAuditLogger.mu.RLock()
|
||||
defer globalAuditLogger.mu.RUnlock()
|
||||
|
||||
result := make([]AuditEntry, len(globalAuditLogger.entries))
|
||||
copy(result, globalAuditLogger.entries)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAuditLogFiltered возвращает отфильтрованный лог аудита
|
||||
func GetAuditLogFiltered(dataType, operation string, fromTime, toTime int64) []AuditEntry {
|
||||
globalAuditLogger.mu.RLock()
|
||||
defer globalAuditLogger.mu.RUnlock()
|
||||
|
||||
result := make([]AuditEntry, 0)
|
||||
for _, entry := range globalAuditLogger.entries {
|
||||
if dataType != "" && entry.DataType != dataType {
|
||||
continue
|
||||
}
|
||||
if operation != "" && entry.Operation != operation {
|
||||
continue
|
||||
}
|
||||
if fromTime > 0 && entry.Timestamp < fromTime {
|
||||
continue
|
||||
}
|
||||
if toTime > 0 && entry.Timestamp > toTime {
|
||||
continue
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearAuditLog очищает лог аудита (только для отладки)
|
||||
func ClearAuditLog() {
|
||||
globalAuditLogger.mu.Lock()
|
||||
defer globalAuditLogger.mu.Unlock()
|
||||
globalAuditLogger.entries = make([]AuditEntry, 0)
|
||||
}
|
||||
|
||||
// GetAuditLogSize возвращает количество записей в логе аудита
|
||||
func GetAuditLogSize() int {
|
||||
globalAuditLogger.mu.RLock()
|
||||
defer globalAuditLogger.mu.RUnlock()
|
||||
return len(globalAuditLogger.entries)
|
||||
}
|
||||
|
||||
// AuditDatabaseOperation логирует операцию с базой данных
|
||||
func AuditDatabaseOperation(operation, dbName string) {
|
||||
LogAudit(operation, "DATABASE", dbName, map[string]interface{}{
|
||||
"database": dbName,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditCollectionOperation логирует операцию с коллекцией
|
||||
func AuditCollectionOperation(operation, dbName, collName string, settings interface{}) {
|
||||
LogAudit(operation, "COLLECTION", fmt.Sprintf("%s.%s", dbName, collName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditDocumentOperation логирует операцию с документом
|
||||
func AuditDocumentOperation(operation, dbName, collName, docID string, fields map[string]interface{}) {
|
||||
LogAudit(operation, "DOCUMENT", fmt.Sprintf("%s.%s.%s", dbName, collName, docID), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"fields": fields,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditFieldOperation логирует операцию с полем
|
||||
func AuditFieldOperation(operation, dbName, collName, docID, fieldName string, value interface{}) {
|
||||
LogAudit(operation, "FIELD", fmt.Sprintf("%s.%s.%s.%s", dbName, collName, docID, fieldName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"field": fieldName,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditTupleOperation логирует операцию с кортежем
|
||||
func AuditTupleOperation(operation, dbName, collName, docID, tuplePath string) {
|
||||
LogAudit(operation, "TUPLE", fmt.Sprintf("%s.%s.%s.%s", dbName, collName, docID, tuplePath), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"tuple_path": tuplePath,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditIndexOperation логирует операцию с индексом
|
||||
func AuditIndexOperation(operation, dbName, collName, indexName string, fields []string, unique bool) {
|
||||
LogAudit(operation, "INDEX", fmt.Sprintf("%s.%s.%s", dbName, collName, indexName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"index_name": indexName,
|
||||
"fields": fields,
|
||||
"unique": unique,
|
||||
})
|
||||
}
|
||||
1664
internal/storage/collection.go
Normal file
1664
internal/storage/collection.go
Normal file
File diff suppressed because it is too large
Load Diff
634
internal/storage/document.go
Normal file
634
internal/storage/document.go
Normal file
@@ -0,0 +1,634 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/storage/document.go
|
||||
// Назначение: Определение структуры документа, его методов для работы
|
||||
// с полями, кортежами (вложенными документами) и сериализации в MessagePack.
|
||||
// Документ является основной единицей хранения в СУБД futriis.
|
||||
// Lock-free: Document.fields переведён на atomic.Value для wait-free доступа.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/compression"
|
||||
"futriis/internal/serializer"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Document представляет документ в коллекции (аналог строки в реляционной СУБД)
|
||||
type Document struct {
|
||||
ID string `msgpack:"_id"` // Уникальный идентификатор документа
|
||||
fieldsPtr atomic.Value // map[string]interface{} - lock-free хранилище полей
|
||||
CreatedAt int64 `msgpack:"created_at"` // Время создания (Unix миллисекунды)
|
||||
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления
|
||||
DeletedAt int64 `msgpack:"deleted_at"` // Время удаления (Unix миллисекунды, 0 = не удалён)
|
||||
Version uint64 `msgpack:"version"` // Версия документа (для оптимистичных блокировок)
|
||||
Compressed bool `msgpack:"compressed"` // Флаг, сжат ли документ
|
||||
OriginalSize int64 `msgpack:"original_size"` // Оригинальный размер до сжатия
|
||||
}
|
||||
|
||||
// Tuple представляет вложенный документ (аналог кортежа в реляционной СУБД)
|
||||
type Tuple struct {
|
||||
Fields map[string]interface{} `msgpack:"fields"`
|
||||
CreatedAt int64 `msgpack:"created_at"` // Время создания кортежа
|
||||
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления кортежа
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Field представляет отдельное поле документа (аналог колонки)
|
||||
type Field struct {
|
||||
Name string `msgpack:"name"`
|
||||
Type FieldType `msgpack:"type"`
|
||||
Value interface{} `msgpack:"value"`
|
||||
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления поля
|
||||
}
|
||||
|
||||
// FieldType определяет тип поля документа
|
||||
type FieldType int
|
||||
|
||||
const (
|
||||
TypeString FieldType = iota
|
||||
TypeNumber
|
||||
TypeBoolean
|
||||
TypeTuple // Вложенный документ
|
||||
TypeArray
|
||||
TypeNull
|
||||
)
|
||||
|
||||
// NewDocument создаёт новый документ с автоматической генерацией ID
|
||||
func NewDocument() *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
d := &Document{
|
||||
ID: uuid.New().String(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DeletedAt: 0,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
d.fieldsPtr.Store(make(map[string]interface{}))
|
||||
return d
|
||||
}
|
||||
|
||||
// NewDocumentWithID создаёт документ с указанным ID
|
||||
func NewDocumentWithID(id string) *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
d := &Document{
|
||||
ID: id,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DeletedAt: 0,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
d.fieldsPtr.Store(make(map[string]interface{}))
|
||||
return d
|
||||
}
|
||||
|
||||
// loadFields загружает карту полей (lock-free)
|
||||
func (d *Document) loadFields() map[string]interface{} {
|
||||
val := d.fieldsPtr.Load()
|
||||
if val == nil {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
return val.(map[string]interface{})
|
||||
}
|
||||
|
||||
// storeFields сохраняет карту полей (lock-free)
|
||||
func (d *Document) storeFields(newMap map[string]interface{}) {
|
||||
d.fieldsPtr.Store(newMap)
|
||||
}
|
||||
|
||||
// SetField устанавливает значение поля документа (lock-free)
|
||||
func (d *Document) SetField(name string, value interface{}) {
|
||||
for {
|
||||
oldFields := d.loadFields()
|
||||
newFields := make(map[string]interface{})
|
||||
for k, v := range oldFields {
|
||||
newFields[k] = v
|
||||
}
|
||||
newFields[name] = value
|
||||
|
||||
if d.compareAndSwapFields(oldFields, newFields) {
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false
|
||||
|
||||
// Аудит изменения поля
|
||||
AuditFieldOperation("UPDATE", "", "", d.ID, name, value)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compareAndSwapFields выполняет CAS операцию для полей (lock-free)
|
||||
func (d *Document) compareAndSwapFields(old, new map[string]interface{}) bool {
|
||||
return d.fieldsPtr.CompareAndSwap(old, new)
|
||||
}
|
||||
|
||||
// GetField возвращает значение поля документа (lock-free)
|
||||
func (d *Document) GetField(name string) (interface{}, error) {
|
||||
fields := d.loadFields()
|
||||
if val, ok := fields[name]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field not found: %s", name)
|
||||
}
|
||||
|
||||
// DeleteField удаляет поле из документа (lock-free)
|
||||
func (d *Document) DeleteField(name string) {
|
||||
for {
|
||||
oldFields := d.loadFields()
|
||||
if _, exists := oldFields[name]; !exists {
|
||||
return
|
||||
}
|
||||
|
||||
newFields := make(map[string]interface{})
|
||||
for k, v := range oldFields {
|
||||
if k != name {
|
||||
newFields[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if d.compareAndSwapFields(oldFields, newFields) {
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false
|
||||
|
||||
// Аудит удаления поля
|
||||
AuditFieldOperation("DELETE", "", "", d.ID, name, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HasField проверяет наличие поля в документе (lock-free)
|
||||
func (d *Document) HasField(name string) bool {
|
||||
fields := d.loadFields()
|
||||
_, ok := fields[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetFields возвращает копию всех полей документа (lock-free)
|
||||
func (d *Document) GetFields() map[string]interface{} {
|
||||
fields := d.loadFields()
|
||||
copy := make(map[string]interface{})
|
||||
for k, v := range fields {
|
||||
copy[k] = v
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// ToMap возвращает полное представление документа в виде map
|
||||
// Включает метаданные (_id, _created_at, _updated_at, _deleted_at, _version) и все поля
|
||||
func (d *Document) ToMap() map[string]interface{} {
|
||||
fields := d.GetFields()
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Добавляем все пользовательские поля
|
||||
for k, v := range fields {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Добавляем метаданные
|
||||
result["_id"] = d.ID
|
||||
result["_created_at"] = d.CreatedAt
|
||||
result["_updated_at"] = d.UpdatedAt
|
||||
result["_deleted_at"] = d.DeletedAt
|
||||
result["_version"] = d.Version
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SetTuple устанавливает вложенный документ (кортеж) в поле
|
||||
func (d *Document) SetTuple(fieldName string, tuple *Tuple) {
|
||||
d.SetField(fieldName, tuple)
|
||||
}
|
||||
|
||||
// GetTuple возвращает вложенный документ из поля
|
||||
func (d *Document) GetTuple(fieldName string) (*Tuple, error) {
|
||||
val, err := d.GetField(fieldName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tuple, ok := val.(*Tuple); ok {
|
||||
return tuple, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field %s is not a tuple", fieldName)
|
||||
}
|
||||
|
||||
// Serialize сериализует документ в MessagePack с поддержкой сжатия
|
||||
func (d *Document) Serialize() ([]byte, error) {
|
||||
// Создаём копию для сериализации
|
||||
fields := d.GetFields()
|
||||
docCopy := &Document{
|
||||
ID: d.ID,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
DeletedAt: d.DeletedAt,
|
||||
Version: d.Version,
|
||||
Compressed: d.Compressed,
|
||||
OriginalSize: d.OriginalSize,
|
||||
}
|
||||
docCopy.fieldsPtr.Store(fields)
|
||||
|
||||
data, err := serializer.Marshal(docCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SerializeCompressed сериализует и сжимает документ
|
||||
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
|
||||
data, err := d.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, нужно ли сжимать
|
||||
if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize {
|
||||
compressed, err := compression.Compress(data, compressionConfig)
|
||||
if err != nil {
|
||||
return data, nil
|
||||
}
|
||||
return compressed, nil
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Deserialize десериализует документ из MessagePack (автоматически определяет сжатие)
|
||||
func (d *Document) Deserialize(data []byte) error {
|
||||
// Пытаемся определить, сжаты ли данные
|
||||
decompressed, err := compression.DecompressAuto(data)
|
||||
if err == nil && len(decompressed) < len(data) {
|
||||
var doc Document
|
||||
if err := serializer.Unmarshal(decompressed, &doc); err != nil {
|
||||
return err
|
||||
}
|
||||
d.ID = doc.ID
|
||||
d.fieldsPtr.Store(doc.loadFields())
|
||||
d.CreatedAt = doc.CreatedAt
|
||||
d.UpdatedAt = doc.UpdatedAt
|
||||
d.DeletedAt = doc.DeletedAt
|
||||
d.Version = doc.Version
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(len(decompressed))
|
||||
} else {
|
||||
var doc Document
|
||||
if err := serializer.Unmarshal(data, &doc); err != nil {
|
||||
return err
|
||||
}
|
||||
d.ID = doc.ID
|
||||
d.fieldsPtr.Store(doc.loadFields())
|
||||
d.CreatedAt = doc.CreatedAt
|
||||
d.UpdatedAt = doc.UpdatedAt
|
||||
d.DeletedAt = doc.DeletedAt
|
||||
d.Version = doc.Version
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
}
|
||||
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone создаёт глубокую копию документа (lock-free)
|
||||
func (d *Document) Clone() *Document {
|
||||
fields := d.GetFields()
|
||||
clone := &Document{
|
||||
ID: d.ID,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
DeletedAt: d.DeletedAt,
|
||||
Version: d.Version,
|
||||
Compressed: d.Compressed,
|
||||
OriginalSize: d.OriginalSize,
|
||||
}
|
||||
|
||||
// Глубокое копирование полей
|
||||
copiedFields := make(map[string]interface{})
|
||||
for k, v := range fields {
|
||||
copiedFields[k] = deepCopyValue(v)
|
||||
}
|
||||
clone.fieldsPtr.Store(copiedFields)
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// Update применяет обновление к документу (атомарно, lock-free)
|
||||
func (d *Document) Update(updates map[string]interface{}) error {
|
||||
for {
|
||||
oldFields := d.loadFields()
|
||||
newFields := make(map[string]interface{})
|
||||
for k, v := range oldFields {
|
||||
newFields[k] = v
|
||||
}
|
||||
for k, v := range updates {
|
||||
newFields[k] = v
|
||||
}
|
||||
|
||||
if d.compareAndSwapFields(oldFields, newFields) {
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SoftDelete мягко удаляет документ (устанавливает метку времени удаления)
|
||||
func (d *Document) SoftDelete() {
|
||||
d.DeletedAt = time.Now().UnixMilli()
|
||||
d.UpdatedAt = d.DeletedAt
|
||||
d.Version++
|
||||
}
|
||||
|
||||
// IsDeleted проверяет, удалён ли документ (мягкое удаление)
|
||||
func (d *Document) IsDeleted() bool {
|
||||
return d.DeletedAt > 0
|
||||
}
|
||||
|
||||
// Restore восстанавливает мягко удалённый документ
|
||||
func (d *Document) Restore() {
|
||||
d.DeletedAt = 0
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
}
|
||||
|
||||
// GetDeletedAtStr возвращает человекочитаемую строку времени удаления
|
||||
func (d *Document) GetDeletedAtStr() string {
|
||||
if d.DeletedAt == 0 {
|
||||
return ""
|
||||
}
|
||||
return time.UnixMilli(d.DeletedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetCreatedAtStr возвращает человекочитаемую строку времени создания
|
||||
func (d *Document) GetCreatedAtStr() string {
|
||||
return time.UnixMilli(d.CreatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetUpdatedAtStr возвращает человекочитаемую строку времени обновления
|
||||
func (d *Document) GetUpdatedAtStr() string {
|
||||
return time.UnixMilli(d.UpdatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// Compress сжимает документ в памяти
|
||||
func (d *Document) Compress(config *compression.Config) error {
|
||||
if d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := d.loadFields()
|
||||
originalSize := len(fields)
|
||||
if originalSize < config.MinSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(originalSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decompress распаковывает документ в памяти
|
||||
func (d *Document) Decompress() error {
|
||||
if !d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompressionRatio возвращает коэффициент сжатия
|
||||
func (d *Document) GetCompressionRatio() float64 {
|
||||
if !d.Compressed || d.OriginalSize == 0 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
fields := d.loadFields()
|
||||
currentSize := len(fields)
|
||||
return float64(currentSize) / float64(d.OriginalSize)
|
||||
}
|
||||
|
||||
// GetMetadata возвращает метаданные документа (временные метки)
|
||||
func (d *Document) GetMetadata() map[string]int64 {
|
||||
return map[string]int64{
|
||||
"created_at": d.CreatedAt,
|
||||
"updated_at": d.UpdatedAt,
|
||||
"deleted_at": d.DeletedAt,
|
||||
"version": int64(d.Version),
|
||||
}
|
||||
}
|
||||
|
||||
// deepCopyValue выполняет глубокое копирование значения
|
||||
func deepCopyValue(val interface{}) interface{} {
|
||||
switch v := val.(type) {
|
||||
case *Tuple:
|
||||
return v.Clone()
|
||||
case map[string]interface{}:
|
||||
copy := make(map[string]interface{})
|
||||
for k, val := range v {
|
||||
copy[k] = deepCopyValue(val)
|
||||
}
|
||||
return copy
|
||||
case []interface{}:
|
||||
copy := make([]interface{}, len(v))
|
||||
for i, val := range v {
|
||||
copy[i] = deepCopyValue(val)
|
||||
}
|
||||
return copy
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// NewTuple создаёт новый вложенный документ (кортеж)
|
||||
func NewTuple() *Tuple {
|
||||
now := time.Now().UnixMilli()
|
||||
return &Tuple{
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Set устанавливает поле во вложенном документе
|
||||
func (t *Tuple) Set(name string, value interface{}) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.Fields[name] = value
|
||||
t.UpdatedAt = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// Get возвращает поле из вложенного документа
|
||||
func (t *Tuple) Get(name string) (interface{}, error) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
if val, ok := t.Fields[name]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, fmt.Errorf("tuple field not found: %s", name)
|
||||
}
|
||||
|
||||
// Clone создаёт копию кортежа
|
||||
func (t *Tuple) Clone() *Tuple {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
clone := NewTuple()
|
||||
for k, v := range t.Fields {
|
||||
clone.Fields[k] = deepCopyValue(v)
|
||||
}
|
||||
clone.CreatedAt = t.CreatedAt
|
||||
clone.UpdatedAt = t.UpdatedAt
|
||||
return clone
|
||||
}
|
||||
|
||||
// ToMap конвертирует кортеж в map
|
||||
func (t *Tuple) ToMap() map[string]interface{} {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
copy := make(map[string]interface{})
|
||||
for k, v := range t.Fields {
|
||||
copy[k] = v
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// GetTupleMetadata возвращает метаданные кортежа
|
||||
func (t *Tuple) GetTupleMetadata() map[string]int64 {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
return map[string]int64{
|
||||
"created_at": t.CreatedAt,
|
||||
"updated_at": t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// GetNestedField получает значение по точечному пути (например, "user.address.city")
|
||||
func (d *Document) GetNestedField(path string) (interface{}, error) {
|
||||
parts := strings.Split(path, ".")
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
current := interface{}(d)
|
||||
for _, part := range parts {
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
val, err := v.GetField(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = val
|
||||
case *Tuple:
|
||||
val, err := v.Get(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = val
|
||||
case map[string]interface{}:
|
||||
if val, ok := v[part]; ok {
|
||||
current = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("field not found: %s", part)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot navigate into non-document value at %s", part)
|
||||
}
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
// SetNestedField устанавливает значение по точечному пути
|
||||
func (d *Document) SetNestedField(path string, value interface{}) error {
|
||||
parts := strings.Split(path, ".")
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
d.SetField(parts[0], value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Для простоты реализации используем подход с чтением-модификацией-записью
|
||||
// В production коде потребуется более сложная lock-free структура
|
||||
|
||||
// Сначала проверяем путь
|
||||
var current interface{} = d
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
part := parts[i]
|
||||
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
if !v.HasField(part) {
|
||||
newTuple := NewTuple()
|
||||
v.SetField(part, newTuple)
|
||||
current = newTuple
|
||||
} else {
|
||||
field, _ := v.GetField(part)
|
||||
if tuple, ok := field.(*Tuple); ok {
|
||||
current = tuple
|
||||
} else {
|
||||
return fmt.Errorf("field %s is not a tuple", part)
|
||||
}
|
||||
}
|
||||
case *Tuple:
|
||||
if val, err := v.Get(part); err == nil {
|
||||
if tuple, ok := val.(*Tuple); ok {
|
||||
current = tuple
|
||||
} else {
|
||||
return fmt.Errorf("field %s is not a tuple", part)
|
||||
}
|
||||
} else {
|
||||
newTuple := NewTuple()
|
||||
v.Set(part, newTuple)
|
||||
current = newTuple
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("cannot set nested field on non-document value")
|
||||
}
|
||||
}
|
||||
|
||||
lastPart := parts[len(parts)-1]
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
v.SetField(lastPart, value)
|
||||
case *Tuple:
|
||||
v.Set(lastPart, value)
|
||||
default:
|
||||
return fmt.Errorf("cannot set field on non-document value")
|
||||
}
|
||||
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Compressed = false
|
||||
return nil
|
||||
}
|
||||
971
internal/storage/engine.go
Normal file
971
internal/storage/engine.go
Normal file
@@ -0,0 +1,971 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/storage/engine.go
|
||||
// Назначение: In-memory движок хранения документов с поддержкой коллекций,
|
||||
// слайсов (аналог БД), тапплов (аналог таблиц), полей и кортежей.
|
||||
// Полностью wait-free с использованием sync.Map и атомарных операций.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/raft"
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// INMEM STORE - ВСТРОЕННОЕ ФАЙЛОВОЕ ХРАНИЛИЩЕ ДЛЯ RAFT
|
||||
// =============================================================================
|
||||
|
||||
// InmemStore реализует встроенное файловое хранилище для Raft.
|
||||
type InmemStore struct {
|
||||
mu sync.RWMutex
|
||||
data map[string][]byte
|
||||
path string
|
||||
createdAt int64
|
||||
updatedAt int64
|
||||
}
|
||||
|
||||
// NewInmemStore создаёт новое хранилище.
|
||||
func NewInmemStore(path string) *InmemStore {
|
||||
now := time.Now().UnixMilli()
|
||||
store := &InmemStore{
|
||||
data: make(map[string][]byte),
|
||||
path: path,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
store.load()
|
||||
return store
|
||||
}
|
||||
|
||||
// load загружает данные из файла.
|
||||
func (s *InmemStore) load() {
|
||||
if s.path == "" {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
json.Unmarshal(data, &s.data)
|
||||
s.updatedAt = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// save сохраняет данные в файл.
|
||||
func (s *InmemStore) save() {
|
||||
if s.path == "" {
|
||||
return
|
||||
}
|
||||
s.updatedAt = time.Now().UnixMilli()
|
||||
data, _ := json.Marshal(s.data)
|
||||
os.WriteFile(s.path, data, 0644)
|
||||
}
|
||||
|
||||
// ==================== Реализация raft.LogStore ====================
|
||||
|
||||
// FirstIndex возвращает первый индекс в логе.
|
||||
func (s *InmemStore) FirstIndex() (uint64, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var first uint64 = 0
|
||||
for key := range s.data {
|
||||
var idx uint64
|
||||
if _, err := fmt.Sscanf(key, "log-%d", &idx); err == nil {
|
||||
if first == 0 || idx < first {
|
||||
first = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
return first, nil
|
||||
}
|
||||
|
||||
// LastIndex возвращает последний индекс в логе.
|
||||
func (s *InmemStore) LastIndex() (uint64, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var last uint64 = 0
|
||||
for key := range s.data {
|
||||
var idx uint64
|
||||
if _, err := fmt.Sscanf(key, "log-%d", &idx); err == nil {
|
||||
if idx > last {
|
||||
last = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
return last, nil
|
||||
}
|
||||
|
||||
// GetLog получает запись лога по индексу.
|
||||
func (s *InmemStore) GetLog(idx uint64, log *raft.Log) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
key := fmt.Sprintf("log-%d", idx)
|
||||
data, ok := s.data[key]
|
||||
if !ok {
|
||||
return raft.ErrLogNotFound
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, log)
|
||||
}
|
||||
|
||||
// StoreLog сохраняет одну запись лога.
|
||||
func (s *InmemStore) StoreLog(log *raft.Log) error {
|
||||
return s.StoreLogs([]*raft.Log{log})
|
||||
}
|
||||
|
||||
// StoreLogs сохраняет несколько записей лога.
|
||||
func (s *InmemStore) StoreLogs(logs []*raft.Log) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for _, log := range logs {
|
||||
key := fmt.Sprintf("log-%d", log.Index)
|
||||
data, err := json.Marshal(log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.data[key] = data
|
||||
}
|
||||
s.save()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRange удаляет записи лога в диапазоне.
|
||||
func (s *InmemStore) DeleteRange(min, max uint64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for idx := min; idx <= max; idx++ {
|
||||
key := fmt.Sprintf("log-%d", idx)
|
||||
delete(s.data, key)
|
||||
}
|
||||
s.save()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== Реализация raft.StableStore ====================
|
||||
|
||||
// Get получает значение по ключу.
|
||||
func (s *InmemStore) Get(key []byte) ([]byte, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
val, ok := s.data[string(key)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Set устанавливает значение по ключу.
|
||||
func (s *InmemStore) Set(key []byte, val []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.data[string(key)] = val
|
||||
s.save()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUint64 устанавливает uint64 значение по ключу.
|
||||
func (s *InmemStore) SetUint64(key []byte, val uint64) error {
|
||||
return s.Set(key, []byte(fmt.Sprintf("%d", val)))
|
||||
}
|
||||
|
||||
// GetUint64 получает uint64 значение по ключу.
|
||||
func (s *InmemStore) GetUint64(key []byte) (uint64, error) {
|
||||
val, err := s.Get(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if val == nil {
|
||||
return 0, nil
|
||||
}
|
||||
var result uint64
|
||||
fmt.Sscanf(string(val), "%d", &result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ОСНОВНОЕ ХРАНИЛИЩЕ
|
||||
// =============================================================================
|
||||
|
||||
// Storage представляет основное хранилище баз данных
|
||||
type Storage struct {
|
||||
databases sync.Map // map[string]*Database
|
||||
pageSize int64
|
||||
logger *log.Logger
|
||||
totalDocs atomic.Int64
|
||||
createdAt int64
|
||||
}
|
||||
|
||||
// Database представляет базу данных (аналог слайса в реляционных СУБД)
|
||||
type Database struct {
|
||||
name string
|
||||
collections sync.Map // map[string]*Collection
|
||||
createdAt int64
|
||||
updatedAt int64
|
||||
deletedAt int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// ExportMetadata содержит метаданные экспорта
|
||||
type ExportMetadata struct {
|
||||
DatabaseName string `msgpack:"database_name"`
|
||||
ExportTime int64 `msgpack:"export_time"`
|
||||
ExportTimeStr string `msgpack:"export_time_str"`
|
||||
Version string `msgpack:"version"`
|
||||
CollectionsCount int `msgpack:"collections_count"`
|
||||
DocumentsCount int64 `msgpack:"documents_count"`
|
||||
ExportedBy string `msgpack:"exported_by,omitempty"`
|
||||
Additional map[string]interface{} `msgpack:"additional,omitempty"`
|
||||
}
|
||||
|
||||
// ImportMetadata содержит метаданные импорта
|
||||
type ImportMetadata struct {
|
||||
DatabaseName string `msgpack:"database_name"`
|
||||
ImportTime int64 `msgpack:"import_time"`
|
||||
ImportTimeStr string `msgpack:"import_time_str"`
|
||||
SourceFile string `msgpack:"source_file"`
|
||||
SourceExportTime int64 `msgpack:"source_export_time,omitempty"`
|
||||
CollectionsCount int `msgpack:"collections_count"`
|
||||
DocumentsCount int64 `msgpack:"documents_count"`
|
||||
SkippedDocuments int64 `msgpack:"skipped_documents"`
|
||||
FailedDocuments int64 `msgpack:"failed_documents"`
|
||||
ImportedBy string `msgpack:"imported_by,omitempty"`
|
||||
}
|
||||
|
||||
// NewStorage создаёт новый экземпляр хранилища
|
||||
func NewStorage(pageSizeMB int, logger *log.Logger) *Storage {
|
||||
return &Storage{
|
||||
pageSize: int64(pageSizeMB) * 1024 * 1024,
|
||||
logger: logger,
|
||||
createdAt: time.Now().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDatabase создаёт новую базу данных
|
||||
func (s *Storage) CreateDatabase(name string) error {
|
||||
now := time.Now().UnixMilli()
|
||||
db := &Database{
|
||||
name: name,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: 0,
|
||||
}
|
||||
|
||||
if _, exists := s.databases.LoadOrStore(name, db); exists {
|
||||
return fmt.Errorf("database already exists")
|
||||
}
|
||||
AuditDatabaseOperation("CREATE", name)
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Database created: " + name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatabase возвращает базу данных по имени
|
||||
func (s *Storage) GetDatabase(name string) (*Database, error) {
|
||||
if val, ok := s.databases.Load(name); ok {
|
||||
return val.(*Database), nil
|
||||
}
|
||||
return nil, fmt.Errorf("database not found")
|
||||
}
|
||||
|
||||
// DropDatabase удаляет базу данных
|
||||
func (s *Storage) DropDatabase(name string) error {
|
||||
if val, ok := s.databases.Load(name); ok {
|
||||
db := val.(*Database)
|
||||
db.mu.Lock()
|
||||
db.deletedAt = time.Now().UnixMilli()
|
||||
db.mu.Unlock()
|
||||
}
|
||||
|
||||
if _, ok := s.databases.LoadAndDelete(name); !ok {
|
||||
return fmt.Errorf("database not found")
|
||||
}
|
||||
AuditDatabaseOperation("DROP", name)
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Database dropped: " + name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListDatabases возвращает список всех баз данных
|
||||
func (s *Storage) ListDatabases() []string {
|
||||
databases := make([]string, 0)
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
databases = append(databases, key.(string))
|
||||
return true
|
||||
})
|
||||
return databases
|
||||
}
|
||||
|
||||
// Name возвращает имя базы данных
|
||||
func (db *Database) Name() string {
|
||||
return db.name
|
||||
}
|
||||
|
||||
// GetCreatedAt возвращает время создания базы данных
|
||||
func (db *Database) GetCreatedAt() int64 {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return db.createdAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt возвращает время последнего обновления базы данных
|
||||
func (db *Database) GetUpdatedAt() int64 {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return db.updatedAt
|
||||
}
|
||||
|
||||
// GetDeletedAt возвращает время удаления базы данных (0 = не удалена)
|
||||
func (db *Database) GetDeletedAt() int64 {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return db.deletedAt
|
||||
}
|
||||
|
||||
// UpdateTimestamp обновляет временную метку базы данных
|
||||
func (db *Database) UpdateTimestamp() {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
db.updatedAt = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// CreateCollection создаёт новую коллекцию в базе данных
|
||||
func (db *Database) CreateCollection(name string) error {
|
||||
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, nil)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
db.UpdateTimestamp()
|
||||
AuditCollectionOperation("CREATE", db.name, name, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCollectionWithSettings создаёт коллекцию с настройками
|
||||
func (db *Database) CreateCollectionWithSettings(name string, settings *CollectionSettings) error {
|
||||
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, settings)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
db.UpdateTimestamp()
|
||||
AuditCollectionOperation("CREATE", db.name, name, settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollection возвращает коллекцию по имени
|
||||
func (db *Database) GetCollection(name string) (*Collection, error) {
|
||||
if val, ok := db.collections.Load(name); ok {
|
||||
return val.(*Collection), nil
|
||||
}
|
||||
return nil, fmt.Errorf("collection not found")
|
||||
}
|
||||
|
||||
// DropCollection удаляет коллекцию
|
||||
func (db *Database) DropCollection(name string) error {
|
||||
if _, ok := db.collections.LoadAndDelete(name); !ok {
|
||||
return fmt.Errorf("collection not found")
|
||||
}
|
||||
db.UpdateTimestamp()
|
||||
AuditCollectionOperation("DROP", db.name, name, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCollections возвращает список всех коллекций в базе данных
|
||||
func (db *Database) ListCollections() []string {
|
||||
collections := make([]string, 0)
|
||||
db.collections.Range(func(key, value interface{}) bool {
|
||||
collections = append(collections, key.(string))
|
||||
return true
|
||||
})
|
||||
return collections
|
||||
}
|
||||
|
||||
// GetTotalDocuments возвращает общее количество документов во всех коллекциях
|
||||
func (s *Storage) GetTotalDocuments() int64 {
|
||||
return s.totalDocs.Load()
|
||||
}
|
||||
|
||||
// GetPageSize возвращает размер страницы памяти
|
||||
func (s *Storage) GetPageSize() int64 {
|
||||
return s.pageSize
|
||||
}
|
||||
|
||||
// GetCreatedAt возвращает время создания хранилища
|
||||
func (s *Storage) GetCreatedAt() int64 {
|
||||
return s.createdAt
|
||||
}
|
||||
|
||||
// SerializeDatabaseWithMetadata сериализует базу данных с метаданными экспорта
|
||||
func (db *Database) SerializeDatabaseWithMetadata(exportedBy string) ([]byte, *ExportMetadata, error) {
|
||||
dbData := make(map[string]interface{})
|
||||
|
||||
totalDocuments := int64(0)
|
||||
|
||||
db.collections.Range(func(key, value interface{}) bool {
|
||||
coll := value.(*Collection)
|
||||
collData := make(map[string]interface{})
|
||||
|
||||
docs := coll.GetAllDocuments()
|
||||
totalDocuments += int64(len(docs))
|
||||
|
||||
collDocs := make([]*Document, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
collDocs = append(collDocs, doc)
|
||||
}
|
||||
collData["documents"] = collDocs
|
||||
collData["metadata"] = coll.GetMetadata()
|
||||
collData["timestamps"] = coll.GetTimestamps()
|
||||
|
||||
collData["indexes"] = coll.GetIndexesInfo()
|
||||
collData["constraints"] = coll.GetConstraints()
|
||||
collData["constraint_timestamps"] = coll.GetConstraintTimestamps()
|
||||
collData["acl_timestamps"] = coll.GetACLTimestamps()
|
||||
|
||||
dbData[key.(string)] = collData
|
||||
return true
|
||||
})
|
||||
|
||||
// Используем функцию GetCurrentTimestamp из audit.go
|
||||
nowMs, nowStr := GetCurrentTimestamp()
|
||||
metadata := &ExportMetadata{
|
||||
DatabaseName: db.name,
|
||||
ExportTime: nowMs,
|
||||
ExportTimeStr: nowStr,
|
||||
Version: "1.0",
|
||||
CollectionsCount: len(db.ListCollections()),
|
||||
DocumentsCount: totalDocuments,
|
||||
ExportedBy: exportedBy,
|
||||
Additional: map[string]interface{}{
|
||||
"storage_created_at": time.UnixMilli(db.createdAt).Format("2006-01-02 15:04:05.000"),
|
||||
},
|
||||
}
|
||||
|
||||
dbData["_metadata"] = map[string]interface{}{
|
||||
"name": db.name,
|
||||
"created_at": db.GetCreatedAt(),
|
||||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"updated_at": db.GetUpdatedAt(),
|
||||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"export_metadata": metadata,
|
||||
}
|
||||
|
||||
data, err := serializer.Marshal(dbData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return data, metadata, nil
|
||||
}
|
||||
|
||||
// SerializeDatabase сериализует всю базу данных в MessagePack
|
||||
func (db *Database) SerializeDatabase() ([]byte, error) {
|
||||
data, _, err := db.SerializeDatabaseWithMetadata("")
|
||||
return data, err
|
||||
}
|
||||
|
||||
// DeserializeDatabaseWithMetadata десериализует базу данных с сохранением метаданных импорта
|
||||
func (db *Database) DeserializeDatabaseWithMetadata(data []byte, sourceFile string, importedBy string) (*ImportMetadata, error) {
|
||||
var dbData map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &dbData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sourceExportTime int64
|
||||
var collectionsCount int
|
||||
var documentsCount int64
|
||||
|
||||
if metaRaw, ok := dbData["_metadata"]; ok {
|
||||
if meta, ok := metaRaw.(map[string]interface{}); ok {
|
||||
if createdAt, ok := meta["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
db.createdAt = v
|
||||
}
|
||||
}
|
||||
if exportMetaRaw, ok := meta["export_metadata"]; ok {
|
||||
if exportMeta, ok := exportMetaRaw.(map[string]interface{}); ok {
|
||||
if sourceTime, ok := exportMeta["export_time"]; ok {
|
||||
if v, ok := sourceTime.(int64); ok {
|
||||
sourceExportTime = v
|
||||
} else if v, ok := sourceTime.(float64); ok {
|
||||
sourceExportTime = int64(v)
|
||||
}
|
||||
}
|
||||
if collCount, ok := exportMeta["collections_count"]; ok {
|
||||
if v, ok := collCount.(int); ok {
|
||||
collectionsCount = v
|
||||
} else if v, ok := collCount.(int64); ok {
|
||||
collectionsCount = int(v)
|
||||
} else if v, ok := collCount.(float64); ok {
|
||||
collectionsCount = int(v)
|
||||
}
|
||||
}
|
||||
if docCount, ok := exportMeta["documents_count"]; ok {
|
||||
if v, ok := docCount.(int64); ok {
|
||||
documentsCount = v
|
||||
} else if v, ok := docCount.(float64); ok {
|
||||
documentsCount = int64(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(dbData, "_metadata")
|
||||
}
|
||||
|
||||
for collName, collDataRaw := range dbData {
|
||||
collData, ok := collDataRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
settings := &CollectionSettings{
|
||||
MaxDocuments: 0,
|
||||
ValidateSchema: false,
|
||||
AutoIndexID: true,
|
||||
TTLSeconds: 0,
|
||||
SoftDelete: false,
|
||||
}
|
||||
|
||||
if metaRaw, ok := collData["metadata"]; ok {
|
||||
if meta, ok := metaRaw.(map[string]interface{}); ok {
|
||||
if settingsRaw, ok := meta["settings"]; ok {
|
||||
if settingsMap, ok := settingsRaw.(map[string]interface{}); ok {
|
||||
if maxDocs, ok := settingsMap["max_documents"]; ok {
|
||||
if v, ok := maxDocs.(int); ok {
|
||||
settings.MaxDocuments = v
|
||||
} else if v, ok := maxDocs.(int64); ok {
|
||||
settings.MaxDocuments = int(v)
|
||||
} else if v, ok := maxDocs.(float64); ok {
|
||||
settings.MaxDocuments = int(v)
|
||||
}
|
||||
}
|
||||
if validateSchema, ok := settingsMap["validate_schema"]; ok {
|
||||
if v, ok := validateSchema.(bool); ok {
|
||||
settings.ValidateSchema = v
|
||||
}
|
||||
}
|
||||
if autoIndexID, ok := settingsMap["auto_index_id"]; ok {
|
||||
if v, ok := autoIndexID.(bool); ok {
|
||||
settings.AutoIndexID = v
|
||||
}
|
||||
}
|
||||
if ttlSeconds, ok := settingsMap["ttl_seconds"]; ok {
|
||||
if v, ok := ttlSeconds.(int); ok {
|
||||
settings.TTLSeconds = v
|
||||
} else if v, ok := ttlSeconds.(int64); ok {
|
||||
settings.TTLSeconds = int(v)
|
||||
} else if v, ok := ttlSeconds.(float64); ok {
|
||||
settings.TTLSeconds = int(v)
|
||||
}
|
||||
}
|
||||
if softDelete, ok := settingsMap["soft_delete"]; ok {
|
||||
if v, ok := softDelete.(bool); ok {
|
||||
settings.SoftDelete = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if meta, ok := metaRaw.(*CollectionMetadata); ok {
|
||||
if meta.Settings != nil {
|
||||
settings = meta.Settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coll := NewCollection(db.name, collName, settings)
|
||||
|
||||
if timestampsRaw, ok := collData["timestamps"]; ok {
|
||||
if timestamps, ok := timestampsRaw.(map[string]interface{}); ok {
|
||||
if createdAt, ok := timestamps["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
coll.metadata.CreatedAt = v
|
||||
} else if v, ok := createdAt.(float64); ok {
|
||||
coll.metadata.CreatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if updatedAt, ok := timestamps["updated_at"]; ok {
|
||||
if v, ok := updatedAt.(int64); ok {
|
||||
coll.metadata.UpdatedAt = v
|
||||
} else if v, ok := updatedAt.(float64); ok {
|
||||
coll.metadata.UpdatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if indexesRaw, ok := collData["indexes"]; ok {
|
||||
if indexesList, ok := indexesRaw.([]interface{}); ok {
|
||||
for _, idxRaw := range indexesList {
|
||||
if idxMap, ok := idxRaw.(map[string]interface{}); ok {
|
||||
idxName, _ := idxMap["name"].(string)
|
||||
idxFieldsRaw, _ := idxMap["fields"].([]interface{})
|
||||
idxFields := make([]string, len(idxFieldsRaw))
|
||||
for i, f := range idxFieldsRaw {
|
||||
idxFields[i] = fmt.Sprintf("%v", f)
|
||||
}
|
||||
idxUnique, _ := idxMap["unique"].(bool)
|
||||
if idxName != "" && idxName != "_id_" {
|
||||
coll.CreateIndex(idxName, idxFields, idxUnique)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if docsRaw, ok := collData["documents"]; ok {
|
||||
if docs, ok := docsRaw.([]interface{}); ok {
|
||||
for _, docRaw := range docs {
|
||||
if doc, ok := docRaw.(*Document); ok {
|
||||
coll.Insert(doc)
|
||||
} else if docMap, ok := docRaw.(map[string]interface{}); ok {
|
||||
doc := NewDocument()
|
||||
if id, ok := docMap["ID"].(string); ok {
|
||||
doc.ID = id
|
||||
} else if id, ok := docMap["id"].(string); ok {
|
||||
doc.ID = id
|
||||
}
|
||||
if fields, ok := docMap["fields"]; ok {
|
||||
if fieldsMap, ok := fields.(map[string]interface{}); ok {
|
||||
for k, v := range fieldsMap {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if createdAt, ok := docMap["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
doc.CreatedAt = v
|
||||
} else if v, ok := createdAt.(float64); ok {
|
||||
doc.CreatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if updatedAt, ok := docMap["updated_at"]; ok {
|
||||
if v, ok := updatedAt.(int64); ok {
|
||||
doc.UpdatedAt = v
|
||||
} else if v, ok := updatedAt.(float64); ok {
|
||||
doc.UpdatedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if deletedAt, ok := docMap["deleted_at"]; ok {
|
||||
if v, ok := deletedAt.(int64); ok {
|
||||
doc.DeletedAt = v
|
||||
} else if v, ok := deletedAt.(float64); ok {
|
||||
doc.DeletedAt = int64(v)
|
||||
}
|
||||
}
|
||||
if version, ok := docMap["version"]; ok {
|
||||
if v, ok := version.(uint64); ok {
|
||||
doc.Version = v
|
||||
} else if v, ok := version.(int64); ok {
|
||||
doc.Version = uint64(v)
|
||||
} else if v, ok := version.(float64); ok {
|
||||
doc.Version = uint64(v)
|
||||
}
|
||||
}
|
||||
coll.Insert(doc)
|
||||
}
|
||||
}
|
||||
} else if docs, ok := docsRaw.([]*Document); ok {
|
||||
for _, doc := range docs {
|
||||
coll.Insert(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.collections.Store(collName, coll)
|
||||
AuditCollectionOperation("RESTORE", db.name, collName, settings)
|
||||
}
|
||||
|
||||
// Используем функцию GetCurrentTimestamp из audit.go
|
||||
nowMs, nowStr := GetCurrentTimestamp()
|
||||
importMetadata := &ImportMetadata{
|
||||
DatabaseName: db.name,
|
||||
ImportTime: nowMs,
|
||||
ImportTimeStr: nowStr,
|
||||
SourceFile: sourceFile,
|
||||
SourceExportTime: sourceExportTime,
|
||||
CollectionsCount: collectionsCount,
|
||||
DocumentsCount: documentsCount,
|
||||
SkippedDocuments: 0,
|
||||
FailedDocuments: 0,
|
||||
ImportedBy: importedBy,
|
||||
}
|
||||
|
||||
db.UpdateTimestamp()
|
||||
|
||||
LogAudit("IMPORT", "DATABASE", db.name, map[string]interface{}{
|
||||
"source_file": sourceFile,
|
||||
"collections_count": collectionsCount,
|
||||
"documents_count": documentsCount,
|
||||
"source_export_time": sourceExportTime,
|
||||
"imported_by": importedBy,
|
||||
})
|
||||
|
||||
return importMetadata, nil
|
||||
}
|
||||
|
||||
// DeserializeDatabase десериализует базу данных из MessagePack
|
||||
func (db *Database) DeserializeDatabase(data []byte) error {
|
||||
_, err := db.DeserializeDatabaseWithMetadata(data, "", "")
|
||||
return err
|
||||
}
|
||||
|
||||
// ExportDatabaseWithMetadata экспортирует базу данных с метаданными
|
||||
func (s *Storage) ExportDatabaseWithMetadata(dbName, filePath, exportedBy string) (*ExportMetadata, error) {
|
||||
if !s.ExistsDatabase(dbName) {
|
||||
return nil, fmt.Errorf("database '%s' not found", dbName)
|
||||
}
|
||||
|
||||
db, err := s.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, metadata, err := db.SerializeDatabaseWithMetadata(exportedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write export file: %v", err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Database '%s' exported to %s at %s", dbName, filePath, metadata.ExportTimeStr))
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// ImportDatabaseWithMetadata импортирует базу данных с метаданными
|
||||
func (s *Storage) ImportDatabaseWithMetadata(dbName, filePath, importedBy string) (*ImportMetadata, error) {
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("import file '%s' not found", filePath)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read import file: %v", err)
|
||||
}
|
||||
|
||||
if !s.ExistsDatabase(dbName) {
|
||||
if err := s.CreateDatabase(dbName); err != nil {
|
||||
return nil, fmt.Errorf("failed to create database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := s.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata, err := db.DeserializeDatabaseWithMetadata(data, filePath, importedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Database '%s' imported from %s at %s", dbName, filePath, metadata.ImportTimeStr))
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// GetDatabaseNames возвращает имена всех баз данных
|
||||
func (s *Storage) GetDatabaseNames() []string {
|
||||
return s.ListDatabases()
|
||||
}
|
||||
|
||||
// ExistsDatabase проверяет существование базы данных
|
||||
func (s *Storage) ExistsDatabase(name string) bool {
|
||||
_, ok := s.databases.Load(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetDatabaseCount возвращает количество баз данных
|
||||
func (s *Storage) GetDatabaseCount() int {
|
||||
count := 0
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
// ========== Дополнительные методы для управления хранилищем ==========
|
||||
|
||||
// Backup создаёт резервную копию всех данных
|
||||
func (s *Storage) Backup(backupPath string) error {
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Starting backup to %s", backupPath))
|
||||
}
|
||||
|
||||
backup := make(map[string]interface{})
|
||||
backup["created_at"] = s.createdAt
|
||||
backup["backup_time"] = time.Now().UnixMilli()
|
||||
backup["backup_time_str"] = time.Now().Format("2006-01-02 15:04:05.000")
|
||||
|
||||
databases := make(map[string][]byte)
|
||||
databasesMetadata := make(map[string]interface{})
|
||||
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
dbName := key.(string)
|
||||
db := value.(*Database)
|
||||
|
||||
dbData, err := db.SerializeDatabase()
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to serialize database %s: %v", dbName, err))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
databases[dbName] = dbData
|
||||
databasesMetadata[dbName] = map[string]interface{}{
|
||||
"created_at": db.GetCreatedAt(),
|
||||
"updated_at": db.GetUpdatedAt(),
|
||||
"collections": len(db.ListCollections()),
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
backup["databases"] = databases
|
||||
backup["databases_metadata"] = databasesMetadata
|
||||
backup["total_databases"] = s.GetDatabaseCount()
|
||||
|
||||
data, err := serializer.Marshal(backup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal backup: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(backupPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write backup: %v", err)
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Backup completed: %s", backupPath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore восстанавливает данные из резервной копии
|
||||
func (s *Storage) Restore(backupPath string) error {
|
||||
if s.logger != nil {
|
||||
s.logger.Info(fmt.Sprintf("Starting restore from %s", backupPath))
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read backup file: %v", err)
|
||||
}
|
||||
|
||||
var backup map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &backup); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal backup: %v", err)
|
||||
}
|
||||
|
||||
if createdAt, ok := backup["created_at"]; ok {
|
||||
if v, ok := createdAt.(int64); ok {
|
||||
s.createdAt = v
|
||||
}
|
||||
}
|
||||
|
||||
databasesRaw, ok := backup["databases"]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid backup format: missing databases")
|
||||
}
|
||||
|
||||
databases, ok := databasesRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid backup format: databases is not a map")
|
||||
}
|
||||
|
||||
for dbName, dbDataRaw := range databases {
|
||||
dbData, ok := dbDataRaw.([]byte)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !s.ExistsDatabase(dbName) {
|
||||
if err := s.CreateDatabase(dbName); err != nil {
|
||||
return fmt.Errorf("failed to create database %s: %v", dbName, err)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := s.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database %s: %v", dbName, err)
|
||||
}
|
||||
|
||||
if err := db.DeserializeDatabase(dbData); err != nil {
|
||||
return fmt.Errorf("failed to restore database %s: %v", dbName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.Info("Restore completed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику хранилища
|
||||
func (s *Storage) GetStats() map[string]interface{} {
|
||||
stats := map[string]interface{}{
|
||||
"total_databases": s.GetDatabaseCount(),
|
||||
"total_documents": s.GetTotalDocuments(),
|
||||
"page_size_bytes": s.pageSize,
|
||||
"created_at": s.createdAt,
|
||||
"created_at_str": time.UnixMilli(s.createdAt).Format("2006-01-02 15:04:05.000"),
|
||||
}
|
||||
|
||||
databases := make([]map[string]interface{}, 0)
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
db := value.(*Database)
|
||||
dbStats := map[string]interface{}{
|
||||
"name": db.name,
|
||||
"collections": len(db.ListCollections()),
|
||||
"created_at": db.GetCreatedAt(),
|
||||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"updated_at": db.GetUpdatedAt(),
|
||||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
||||
"deleted_at": db.GetDeletedAt(),
|
||||
}
|
||||
|
||||
totalDocs := int64(0)
|
||||
totalSize := int64(0)
|
||||
db.collections.Range(func(k, v interface{}) bool {
|
||||
coll := v.(*Collection)
|
||||
totalDocs += coll.Count()
|
||||
totalSize += coll.Size()
|
||||
return true
|
||||
})
|
||||
|
||||
dbStats["documents"] = totalDocs
|
||||
dbStats["size_bytes"] = totalSize
|
||||
databases = append(databases, dbStats)
|
||||
return true
|
||||
})
|
||||
|
||||
stats["databases"] = databases
|
||||
return stats
|
||||
}
|
||||
83
internal/storage/fsync.go
Normal file
83
internal/storage/fsync.go
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/storage/fsync.go
|
||||
// Назначение: Реальная синхронизация с диском (fsync) для WAL
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RealFsync выполняет реальный fsync на файле
|
||||
func RealFsync(file *os.File) error {
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Вызываем системный fsync
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
// RealFsyncWithRetry выполняет fsync с повторными попытками
|
||||
func RealFsyncWithRetry(file *os.File, maxRetries int, retryDelay time.Duration) error {
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if err := RealFsync(file); err != nil {
|
||||
lastErr = err
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("fsync failed after %d attempts: %v", maxRetries, lastErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// FsyncDir синхронизирует директорию (для гарантии, что создание файла записано на диск)
|
||||
func FsyncDir(dirPath string) error {
|
||||
dir, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// На Windows нет прямой поддержки fsync для директорий
|
||||
return nil
|
||||
}
|
||||
|
||||
return dir.Sync()
|
||||
}
|
||||
|
||||
// FsyncDataOnly выполняет fsync только для данных (не метаданных)
|
||||
func FsyncDataOnly(file *os.File) error {
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fd := int(file.Fd())
|
||||
_, _, err := syscall.Syscall(syscall.SYS_FSYNC, uintptr(fd), 0, 0)
|
||||
if err != 0 {
|
||||
return fmt.Errorf("fsync failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
984
internal/storage/persistence.go
Normal file
984
internal/storage/persistence.go
Normal file
@@ -0,0 +1,984 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/storage/persistence.go
|
||||
// Назначение: Персистентное хранение данных на диске с поддержкой checkpoint и recovery
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ========== WALReader интерфейс и реализация ==========
|
||||
|
||||
// WALEntry представляет запись WAL
|
||||
type WALEntry struct {
|
||||
Index uint64 `json:"index"`
|
||||
Term uint64 `json:"term"`
|
||||
Type string `json:"type"`
|
||||
Data []byte `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WALReader интерфейс для чтения WAL
|
||||
type WALReader interface {
|
||||
ReadSince(index uint64) ([]WALEntry, error)
|
||||
GetCurrentIndex() (uint64, error)
|
||||
GetLastBackupIndex() uint64
|
||||
SetLastBackupIndex(index uint64) error
|
||||
GetSegments() ([]string, error)
|
||||
ReadSegment(segmentPath string) ([]WALEntry, error)
|
||||
}
|
||||
|
||||
// walReaderImpl реализация WALReader для работы с файлами WAL
|
||||
type walReaderImpl struct {
|
||||
segmentsDir string
|
||||
lastBackupIndex atomic.Uint64
|
||||
mu sync.RWMutex
|
||||
logger LoggerInterface
|
||||
}
|
||||
|
||||
// NewWALReaderImpl создаёт новый WALReader
|
||||
func NewWALReaderImpl(segmentsDir string, logger LoggerInterface) WALReader {
|
||||
wr := &walReaderImpl{
|
||||
segmentsDir: segmentsDir,
|
||||
logger: logger,
|
||||
}
|
||||
wr.loadLastBackupIndex()
|
||||
return wr
|
||||
}
|
||||
|
||||
// loadLastBackupIndex загружает последний индекс из файла
|
||||
func (wr *walReaderImpl) loadLastBackupIndex() {
|
||||
indexPath := filepath.Join(wr.segmentsDir, "last_backup_index.json")
|
||||
data, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var meta map[string]uint64
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if lastIdx, ok := meta["last_index"]; ok {
|
||||
wr.lastBackupIndex.Store(lastIdx)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadSince читает записи WAL с указанного индекса
|
||||
func (wr *walReaderImpl) ReadSince(index uint64) ([]WALEntry, error) {
|
||||
wr.mu.RLock()
|
||||
defer wr.mu.RUnlock()
|
||||
|
||||
entries := make([]WALEntry, 0)
|
||||
|
||||
segments, err := wr.GetSegments()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, segmentPath := range segments {
|
||||
segEntries, err := wr.ReadSegment(segmentPath)
|
||||
if err != nil {
|
||||
if wr.logger != nil {
|
||||
wr.logger.Warn(fmt.Sprintf("Failed to read segment %s: %v", segmentPath, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range segEntries {
|
||||
if entry.Index > index {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// GetCurrentIndex возвращает текущий индекс WAL
|
||||
func (wr *walReaderImpl) GetCurrentIndex() (uint64, error) {
|
||||
segments, err := wr.GetSegments()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(segments) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
lastSegment := segments[len(segments)-1]
|
||||
entries, err := wr.ReadSegment(lastSegment)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return entries[len(entries)-1].Index, nil
|
||||
}
|
||||
|
||||
// GetLastBackupIndex возвращает последний сохранённый индекс бэкапа
|
||||
func (wr *walReaderImpl) GetLastBackupIndex() uint64 {
|
||||
return wr.lastBackupIndex.Load()
|
||||
}
|
||||
|
||||
// SetLastBackupIndex устанавливает последний индекс бэкапа
|
||||
func (wr *walReaderImpl) SetLastBackupIndex(index uint64) error {
|
||||
wr.lastBackupIndex.Store(index)
|
||||
|
||||
indexPath := filepath.Join(wr.segmentsDir, "last_backup_index.json")
|
||||
data, err := json.Marshal(map[string]uint64{"last_index": index})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(indexPath, data, 0644)
|
||||
}
|
||||
|
||||
// GetSegments возвращает список сегментов WAL
|
||||
func (wr *walReaderImpl) GetSegments() ([]string, error) {
|
||||
if wr.segmentsDir == "" {
|
||||
return nil, fmt.Errorf("segments directory not set")
|
||||
}
|
||||
|
||||
pattern := filepath.Join(wr.segmentsDir, "wal_segment_*.log")
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Strings(files)
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// ReadSegment читает сегмент WAL
|
||||
func (wr *walReaderImpl) ReadSegment(segmentPath string) ([]WALEntry, error) {
|
||||
data, err := os.ReadFile(segmentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries := make([]WALEntry, 0)
|
||||
|
||||
pos := 0
|
||||
for pos < len(data) {
|
||||
if pos+4 > len(data) {
|
||||
break
|
||||
}
|
||||
|
||||
length := int(data[pos])<<24 | int(data[pos+1])<<16 | int(data[pos+2])<<8 | int(data[pos+3])
|
||||
pos += 4
|
||||
|
||||
if pos+length > len(data) {
|
||||
break
|
||||
}
|
||||
|
||||
recordData := data[pos : pos+length]
|
||||
pos += length
|
||||
|
||||
var record struct {
|
||||
LSN uint64 `json:"lsn"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Type byte `json:"type"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(recordData, &record); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
entry := WALEntry{
|
||||
Index: record.LSN,
|
||||
Timestamp: record.Timestamp,
|
||||
Type: fmt.Sprintf("%d", record.Type),
|
||||
Data: record.Data,
|
||||
}
|
||||
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// ========== PersistenceConfig ==========
|
||||
|
||||
// PersistenceConfig конфигурация персистентного хранения
|
||||
type PersistenceConfig struct {
|
||||
DataDir string `json:"data_dir"`
|
||||
CheckpointInterval time.Duration `json:"checkpoint_interval"`
|
||||
MaxCheckpoints int `json:"max_checkpoints"`
|
||||
CompressEnabled bool `json:"compress_enabled"`
|
||||
SyncWrites bool `json:"sync_writes"`
|
||||
WalPath string `json:"wal_path"`
|
||||
UseWalForCheckpoint bool `json:"use_wal_for_checkpoint"`
|
||||
AtomicWrites bool `json:"atomic_writes"`
|
||||
}
|
||||
|
||||
// DefaultPersistenceConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultPersistenceConfig() *PersistenceConfig {
|
||||
return &PersistenceConfig{
|
||||
DataDir: "futriis_data",
|
||||
CheckpointInterval: 5 * time.Minute,
|
||||
MaxCheckpoints: 10,
|
||||
CompressEnabled: true,
|
||||
SyncWrites: true,
|
||||
WalPath: "futriis.wal",
|
||||
UseWalForCheckpoint: true,
|
||||
AtomicWrites: true,
|
||||
}
|
||||
}
|
||||
|
||||
// DatabaseSnapshot представляет снапшот базы данных
|
||||
type DatabaseSnapshot struct {
|
||||
Name string `json:"name"`
|
||||
Collections map[string]interface{} `json:"collections"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Version uint64 `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
WalLSN uint64 `json:"wal_lsn,omitempty"`
|
||||
WalSegment string `json:"wal_segment,omitempty"`
|
||||
}
|
||||
|
||||
// CheckpointRecoveryInfo содержит информацию о восстановлении из чекпоинта
|
||||
type CheckpointRecoveryInfo struct {
|
||||
CheckpointTime int64 `json:"checkpoint_time"`
|
||||
CheckpointVersion uint64 `json:"checkpoint_version"`
|
||||
WalLSN uint64 `json:"wal_lsn"`
|
||||
AppliedWalCount int `json:"applied_wal_count"`
|
||||
RestoredDocs int64 `json:"restored_docs"`
|
||||
RestoredColls int `json:"restored_collections"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// PersistenceManager управляет персистентным хранением
|
||||
type PersistenceManager struct {
|
||||
config *PersistenceConfig
|
||||
storage *Storage
|
||||
logger LoggerInterface
|
||||
mu sync.RWMutex
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
lastCheckpoint int64
|
||||
checkpointID atomic.Uint64
|
||||
walManager interface{}
|
||||
walReader WALReader
|
||||
recoveryInfo *CheckpointRecoveryInfo
|
||||
isRestoring atomic.Bool
|
||||
}
|
||||
|
||||
// NewPersistenceManager создаёт новый менеджер персистентности
|
||||
func NewPersistenceManager(config *PersistenceConfig, storage *Storage, logger LoggerInterface) *PersistenceManager {
|
||||
if config == nil {
|
||||
config = DefaultPersistenceConfig()
|
||||
}
|
||||
|
||||
pm := &PersistenceManager{
|
||||
config: config,
|
||||
storage: storage,
|
||||
logger: logger,
|
||||
stopChan: make(chan struct{}),
|
||||
lastCheckpoint: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(config.DataDir, 0755); err != nil {
|
||||
if logger != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create data directory: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if config.UseWalForCheckpoint && config.WalPath != "" {
|
||||
pm.walReader = NewWALReaderImpl(filepath.Dir(config.WalPath), logger)
|
||||
}
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
// SetWALManager устанавливает WAL менеджер для синхронизации чекпоинтов
|
||||
func (pm *PersistenceManager) SetWALManager(walManager interface{}) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
pm.walManager = walManager
|
||||
|
||||
if walManager != nil && pm.config.UseWalForCheckpoint {
|
||||
pm.walReader = NewWALReaderImpl(filepath.Dir(pm.config.WalPath), pm.logger)
|
||||
}
|
||||
}
|
||||
|
||||
// Start запускает фоновое сохранение чекпоинтов
|
||||
func (pm *PersistenceManager) Start() {
|
||||
pm.wg.Add(1)
|
||||
go pm.checkpointLoop()
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Persistence manager started, data dir: %s", pm.config.DataDir))
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает менеджер
|
||||
func (pm *PersistenceManager) Stop() {
|
||||
close(pm.stopChan)
|
||||
pm.wg.Wait()
|
||||
|
||||
pm.SaveAll()
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info("Persistence manager stopped")
|
||||
}
|
||||
}
|
||||
|
||||
// checkpointLoop периодически создаёт чекпоинты
|
||||
func (pm *PersistenceManager) checkpointLoop() {
|
||||
defer pm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(pm.config.CheckpointInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pm.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if pm.isRestoring.Load() {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info("Skipping checkpoint during restore")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := pm.SaveAll(); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to save checkpoint: %v", err))
|
||||
}
|
||||
} else {
|
||||
pm.cleanupOldCheckpoints()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SaveDatabase сохраняет базу данных на диск с атомарной записью
|
||||
func (pm *PersistenceManager) SaveDatabase(dbName string) error {
|
||||
if pm.isRestoring.Load() {
|
||||
return fmt.Errorf("cannot save checkpoint during restore")
|
||||
}
|
||||
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database not found: %s", dbName)
|
||||
}
|
||||
|
||||
var walLSN uint64
|
||||
var walSegment string
|
||||
|
||||
if pm.config.UseWalForCheckpoint && pm.walManager != nil {
|
||||
if segWal, ok := pm.walManager.(*SegmentedWALManager); ok {
|
||||
if segWal.currentSegment != nil {
|
||||
segWal.currentSegment.Writer.Flush()
|
||||
RealFsync(segWal.currentSegment.File)
|
||||
walLSN = segWal.currentSegment.EndLSN
|
||||
walSegment = fmt.Sprintf("%d", segWal.currentSegment.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
snapshot := &DatabaseSnapshot{
|
||||
Name: dbName,
|
||||
Collections: make(map[string]interface{}),
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Version: pm.checkpointID.Add(1),
|
||||
WalLSN: walLSN,
|
||||
WalSegment: walSegment,
|
||||
}
|
||||
|
||||
for _, collName := range db.ListCollections() {
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
docs := coll.GetAllDocuments()
|
||||
collData := make([]map[string]interface{}, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
collData = append(collData, map[string]interface{}{
|
||||
"_id": doc.ID,
|
||||
"fields": doc.GetFields(),
|
||||
"created_at": doc.CreatedAt,
|
||||
"updated_at": doc.UpdatedAt,
|
||||
"deleted_at": doc.DeletedAt,
|
||||
"version": doc.Version,
|
||||
})
|
||||
}
|
||||
snapshot.Collections[collName] = collData
|
||||
}
|
||||
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Вычисляем контрольную сумму
|
||||
hash := sha256.Sum256(data)
|
||||
snapshot.Checksum = hex.EncodeToString(hash[:])
|
||||
|
||||
// Пере-сериализуем с контрольной суммой
|
||||
data, err = json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pm.config.CompressEnabled {
|
||||
data, err = pm.compress(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Атомарная запись с использованием временного файла
|
||||
filename := pm.getSnapshotFilename(dbName, snapshot.Version)
|
||||
if err := pm.atomicWriteFile(filename, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pm.lastCheckpoint = snapshot.CreatedAt
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Saved database %s checkpoint %d (%d bytes, checksum: %s, WAL LSN: %d)",
|
||||
dbName, snapshot.Version, len(data), snapshot.Checksum[:8], walLSN))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// atomicWriteFile выполняет атомарную запись файла
|
||||
func (pm *PersistenceManager) atomicWriteFile(filename string, data []byte) error {
|
||||
if pm.config.AtomicWrites {
|
||||
// Создаём временный файл в той же директории
|
||||
tempFile := filename + ".tmp"
|
||||
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write temp file: %v", err)
|
||||
}
|
||||
|
||||
// Синхронизируем временный файл
|
||||
if pm.config.SyncWrites {
|
||||
if f, err := os.OpenFile(tempFile, os.O_RDWR, 0644); err == nil {
|
||||
RealFsync(f)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Атомарное переименование
|
||||
if err := os.Rename(tempFile, filename); err != nil {
|
||||
os.Remove(tempFile)
|
||||
return fmt.Errorf("failed to rename temp file: %v", err)
|
||||
}
|
||||
|
||||
// Синхронизируем директорию
|
||||
FsyncDir(pm.config.DataDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Обычная запись (не атомарная)
|
||||
return os.WriteFile(filename, data, 0644)
|
||||
}
|
||||
|
||||
// SaveAll сохраняет все базы данных
|
||||
func (pm *PersistenceManager) SaveAll() error {
|
||||
if pm.isRestoring.Load() {
|
||||
return fmt.Errorf("cannot save checkpoints during restore")
|
||||
}
|
||||
|
||||
databases := pm.storage.ListDatabases()
|
||||
|
||||
var lastErr error
|
||||
for _, dbName := range databases {
|
||||
if err := pm.SaveDatabase(dbName); err != nil {
|
||||
lastErr = err
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to save database %s: %v", dbName, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// LoadDatabase загружает базу данных с диска с применением WAL
|
||||
func (pm *PersistenceManager) LoadDatabase(dbName string) error {
|
||||
pm.isRestoring.Store(true)
|
||||
defer pm.isRestoring.Store(false)
|
||||
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
recoveryInfo := &CheckpointRecoveryInfo{
|
||||
CheckpointTime: time.Now().UnixMilli(),
|
||||
Success: false,
|
||||
}
|
||||
|
||||
snapshot, err := pm.findLatestSnapshot(dbName)
|
||||
if err != nil {
|
||||
recoveryInfo.Error = err.Error()
|
||||
pm.recoveryInfo = recoveryInfo
|
||||
return err
|
||||
}
|
||||
|
||||
if snapshot == nil {
|
||||
recoveryInfo.Error = "no snapshot found"
|
||||
pm.recoveryInfo = recoveryInfo
|
||||
return fmt.Errorf("no snapshot found for database: %s", dbName)
|
||||
}
|
||||
|
||||
recoveryInfo.CheckpointVersion = snapshot.Version
|
||||
recoveryInfo.WalLSN = snapshot.WalLSN
|
||||
|
||||
// Проверяем целостность чекпоинта
|
||||
if err := pm.verifySnapshot(snapshot); err != nil {
|
||||
recoveryInfo.Error = err.Error()
|
||||
pm.recoveryInfo = recoveryInfo
|
||||
return fmt.Errorf("snapshot integrity check failed: %v", err)
|
||||
}
|
||||
|
||||
// Создаём базу данных если не существует
|
||||
if !pm.storage.ExistsDatabase(dbName) {
|
||||
if err := pm.storage.CreateDatabase(dbName); err != nil {
|
||||
recoveryInfo.Error = err.Error()
|
||||
pm.recoveryInfo = recoveryInfo
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
recoveryInfo.Error = err.Error()
|
||||
pm.recoveryInfo = recoveryInfo
|
||||
return err
|
||||
}
|
||||
|
||||
// Восстанавливаем коллекции
|
||||
restoredColls := 0
|
||||
restoredDocs := int64(0)
|
||||
|
||||
for collName, collDataRaw := range snapshot.Collections {
|
||||
collData, ok := collDataRaw.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Удаляем существующую коллекцию если есть
|
||||
if _, err := db.GetCollection(collName); err == nil {
|
||||
db.DropCollection(collName)
|
||||
}
|
||||
|
||||
if err := db.CreateCollection(collName); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to create collection %s: %v", collName, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
restoredColls++
|
||||
|
||||
for _, docRaw := range collData {
|
||||
docMap, ok := docRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
docID, ok := docMap["_id"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
doc := NewDocumentWithID(docID)
|
||||
|
||||
if fields, ok := docMap["fields"].(map[string]interface{}); ok {
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if createdAt, ok := docMap["created_at"].(float64); ok {
|
||||
doc.CreatedAt = int64(createdAt)
|
||||
} else if createdAt, ok := docMap["created_at"].(int64); ok {
|
||||
doc.CreatedAt = createdAt
|
||||
}
|
||||
|
||||
if updatedAt, ok := docMap["updated_at"].(float64); ok {
|
||||
doc.UpdatedAt = int64(updatedAt)
|
||||
} else if updatedAt, ok := docMap["updated_at"].(int64); ok {
|
||||
doc.UpdatedAt = updatedAt
|
||||
}
|
||||
|
||||
if deletedAt, ok := docMap["deleted_at"].(float64); ok {
|
||||
doc.DeletedAt = int64(deletedAt)
|
||||
} else if deletedAt, ok := docMap["deleted_at"].(int64); ok {
|
||||
doc.DeletedAt = deletedAt
|
||||
}
|
||||
|
||||
if version, ok := docMap["version"].(float64); ok {
|
||||
doc.Version = uint64(version)
|
||||
} else if version, ok := docMap["version"].(uint64); ok {
|
||||
doc.Version = version
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn(fmt.Sprintf("Failed to restore document %s: %v", docID, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
restoredDocs++
|
||||
}
|
||||
}
|
||||
|
||||
recoveryInfo.RestoredDocs = restoredDocs
|
||||
recoveryInfo.RestoredColls = restoredColls
|
||||
|
||||
// Применяем WAL записи после чекпоинта
|
||||
appliedWalCount := 0
|
||||
if pm.config.UseWalForCheckpoint && snapshot.WalLSN > 0 && pm.walReader != nil {
|
||||
currentLSN, err := pm.walReader.GetCurrentIndex()
|
||||
if err == nil && currentLSN > snapshot.WalLSN {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Database %s: Applying %d WAL entries after checkpoint (LSN %d -> %d)",
|
||||
dbName, currentLSN-snapshot.WalLSN, snapshot.WalLSN, currentLSN))
|
||||
}
|
||||
|
||||
entries, err := pm.walReader.ReadSince(snapshot.WalLSN)
|
||||
if err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to read WAL entries: %v", err))
|
||||
}
|
||||
} else {
|
||||
for _, entry := range entries {
|
||||
if err := pm.applyWALEntry(db, entry); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to apply WAL entry %d: %v", entry.Index, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
appliedWalCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recoveryInfo.AppliedWalCount = appliedWalCount
|
||||
recoveryInfo.DurationMs = time.Since(startTime).Milliseconds()
|
||||
recoveryInfo.Success = true
|
||||
|
||||
pm.recoveryInfo = recoveryInfo
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Loaded database %s from snapshot (version %d, WAL LSN: %d, applied %d WAL entries, %d docs, %d colls, duration: %dms)",
|
||||
dbName, snapshot.Version, snapshot.WalLSN, appliedWalCount, restoredDocs, restoredColls, recoveryInfo.DurationMs))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyWALEntry применяет одну запись WAL к базе данных
|
||||
func (pm *PersistenceManager) applyWALEntry(db *Database, entry WALEntry) error {
|
||||
// Проверяем тип записи (1 = Transaction) - ИСПРАВЛЕНО
|
||||
if entry.Type != "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var txRecord struct {
|
||||
ID uint64 `json:"id"`
|
||||
State int32 `json:"state"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Operations []struct {
|
||||
Type string `json:"type"`
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
DocumentID string `json:"document_id"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Version uint64 `json:"version"`
|
||||
} `json:"operations"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(entry.Data, &txRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Применяем только закоммиченные транзакции (state = 1)
|
||||
if txRecord.State != 1 { // TransactionCommitted
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, op := range txRecord.Operations {
|
||||
if op.Database != db.name {
|
||||
continue
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(op.Collection)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch op.Type {
|
||||
case "insert":
|
||||
doc := NewDocumentWithID(op.DocumentID)
|
||||
for k, v := range op.Data {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
doc.Version = op.Version
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case "update":
|
||||
if err := coll.Update(op.DocumentID, op.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case "delete":
|
||||
if err := coll.Delete(op.DocumentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifySnapshot проверяет целостность снапшота
|
||||
func (pm *PersistenceManager) verifySnapshot(snapshot *DatabaseSnapshot) error {
|
||||
if snapshot.Checksum == "" {
|
||||
return fmt.Errorf("snapshot has no checksum")
|
||||
}
|
||||
|
||||
// Пересоздаём данные без контрольной суммы для проверки
|
||||
tempSnapshot := &DatabaseSnapshot{
|
||||
Name: snapshot.Name,
|
||||
Collections: snapshot.Collections,
|
||||
CreatedAt: snapshot.CreatedAt,
|
||||
Version: snapshot.Version,
|
||||
WalLSN: snapshot.WalLSN,
|
||||
WalSegment: snapshot.WalSegment,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(tempSnapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal for checksum: %v", err)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(data)
|
||||
checksum := hex.EncodeToString(hash[:])
|
||||
|
||||
if checksum != snapshot.Checksum {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", snapshot.Checksum[:8], checksum[:8])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAll загружает все базы данных с диска
|
||||
func (pm *PersistenceManager) LoadAll() error {
|
||||
files, err := filepath.Glob(filepath.Join(pm.config.DataDir, "snapshot_*.json*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
databases := make(map[string]bool)
|
||||
for _, file := range files {
|
||||
base := filepath.Base(file)
|
||||
parts := strings.Split(base, "_")
|
||||
if len(parts) >= 2 {
|
||||
dbName := parts[1]
|
||||
databases[dbName] = true
|
||||
}
|
||||
}
|
||||
|
||||
for dbName := range databases {
|
||||
if err := pm.LoadDatabase(dbName); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to load database %s: %v", dbName, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSnapshotFilename возвращает имя файла для снапшота
|
||||
func (pm *PersistenceManager) getSnapshotFilename(dbName string, version uint64) string {
|
||||
filename := fmt.Sprintf("snapshot_%s_%d.json", dbName, version)
|
||||
if pm.config.CompressEnabled {
|
||||
filename += ".gz"
|
||||
}
|
||||
return filepath.Join(pm.config.DataDir, filename)
|
||||
}
|
||||
|
||||
// findLatestSnapshot находит последний снапшот базы данных
|
||||
func (pm *PersistenceManager) findLatestSnapshot(dbName string) (*DatabaseSnapshot, error) {
|
||||
pattern := filepath.Join(pm.config.DataDir, fmt.Sprintf("snapshot_%s_*.json*", dbName))
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
infoI, _ := os.Stat(files[i])
|
||||
infoJ, _ := os.Stat(files[j])
|
||||
if infoI == nil || infoJ == nil {
|
||||
return false
|
||||
}
|
||||
return infoI.ModTime().After(infoJ.ModTime())
|
||||
})
|
||||
|
||||
data, err := os.ReadFile(files[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pm.config.CompressEnabled && strings.HasSuffix(files[0], ".gz") {
|
||||
data, err = pm.decompress(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var snapshot DatabaseSnapshot
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
// compress сжимает данные с помощью gzip
|
||||
func (pm *PersistenceManager) compress(data []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return nil, fmt.Errorf("failed to write compressed data: %v", err)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, fmt.Errorf("failed to close gzip writer: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// decompress разжимает данные с помощью gzip
|
||||
func (pm *PersistenceManager) decompress(data []byte) ([]byte, error) {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.ReadFrom(reader); err != nil {
|
||||
return nil, fmt.Errorf("failed to decompress data: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// cleanupOldCheckpoints удаляет старые чекпоинты
|
||||
func (pm *PersistenceManager) cleanupOldCheckpoints() {
|
||||
pattern := filepath.Join(pm.config.DataDir, "snapshot_*.json*")
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(files) <= pm.config.MaxCheckpoints {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
infoI, _ := os.Stat(files[i])
|
||||
infoJ, _ := os.Stat(files[j])
|
||||
if infoI == nil || infoJ == nil {
|
||||
return false
|
||||
}
|
||||
return infoI.ModTime().Before(infoJ.ModTime())
|
||||
})
|
||||
|
||||
toDelete := files[:len(files)-pm.config.MaxCheckpoints]
|
||||
for _, f := range toDelete {
|
||||
os.Remove(f)
|
||||
if pm.logger != nil {
|
||||
pm.logger.Debug(fmt.Sprintf("Removed old checkpoint: %s", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLastCheckpointInfo возвращает информацию о последнем чекпоинте
|
||||
func (pm *PersistenceManager) GetLastCheckpointInfo() map[string]interface{} {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
|
||||
info := map[string]interface{}{
|
||||
"last_checkpoint_time": pm.lastCheckpoint,
|
||||
"last_checkpoint_time_str": time.UnixMilli(pm.lastCheckpoint).Format("2006-01-02 15:04:05.000"),
|
||||
"checkpoint_id": pm.checkpointID.Load(),
|
||||
"wal_path": pm.config.WalPath,
|
||||
"use_wal": pm.config.UseWalForCheckpoint,
|
||||
"atomic_writes": pm.config.AtomicWrites,
|
||||
"is_restoring": pm.isRestoring.Load(),
|
||||
}
|
||||
|
||||
if pm.recoveryInfo != nil {
|
||||
info["last_recovery"] = pm.recoveryInfo
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// GetRecoveryInfo возвращает информацию о последнем восстановлении
|
||||
func (pm *PersistenceManager) GetRecoveryInfo() *CheckpointRecoveryInfo {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
return pm.recoveryInfo
|
||||
}
|
||||
|
||||
// IsRestoring возвращает статус восстановления
|
||||
func (pm *PersistenceManager) IsRestoring() bool {
|
||||
return pm.isRestoring.Load()
|
||||
}
|
||||
223
internal/storage/runtime_limits.go
Normal file
223
internal/storage/runtime_limits.go
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/storage/runtime_limits.go
|
||||
// Назначение: Ограничения на размер коллекции/документа в рантайме
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggerInterface определяет интерфейс для логирования
|
||||
type LoggerInterface interface {
|
||||
Debug(msg string)
|
||||
Info(msg string)
|
||||
Error(msg string)
|
||||
Warn(msg string)
|
||||
}
|
||||
|
||||
// RuntimeLimitsManager управляет runtime-ограничениями
|
||||
type RuntimeLimitsManager struct {
|
||||
mu sync.RWMutex
|
||||
globalMaxDocSize int64 // Максимальный размер документа (байт)
|
||||
globalMaxCollSize int64 // Максимальный размер коллекции (байт)
|
||||
globalMaxDocsPerColl int64 // Максимальное количество документов в коллекции
|
||||
collectionOverrides map[string]*CollectionLimits // Переопределения для коллекций
|
||||
metrics *LimitMetrics
|
||||
logger LoggerInterface
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// CollectionLimits содержит лимиты для конкретной коллекции
|
||||
type CollectionLimits struct {
|
||||
MaxDocSize int64
|
||||
MaxCollectionSize int64
|
||||
MaxDocuments int64
|
||||
LastUpdated int64
|
||||
}
|
||||
|
||||
// LimitMetrics хранит метрики ограничений
|
||||
type LimitMetrics struct {
|
||||
RejectedBySize atomic.Uint64
|
||||
RejectedByDocCount atomic.Uint64
|
||||
RejectedByCollSize atomic.Uint64
|
||||
LastCheckTime atomic.Int64
|
||||
}
|
||||
|
||||
// RuntimeLimitsConfig содержит конфигурацию ограничений
|
||||
type RuntimeLimitsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
GlobalMaxDocSizeMB int `json:"global_max_doc_size_mb"`
|
||||
GlobalMaxCollSizeMB int64 `json:"global_max_coll_size_mb"`
|
||||
GlobalMaxDocsPerColl int64 `json:"global_max_docs_per_coll"`
|
||||
}
|
||||
|
||||
// DefaultRuntimeLimitsConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultRuntimeLimitsConfig() *RuntimeLimitsConfig {
|
||||
return &RuntimeLimitsConfig{
|
||||
Enabled: true,
|
||||
GlobalMaxDocSizeMB: 16, // 16 MB на документ
|
||||
GlobalMaxCollSizeMB: 10240, // 10 GB на коллекцию
|
||||
GlobalMaxDocsPerColl: 10000000, // 10 млн документов
|
||||
}
|
||||
}
|
||||
|
||||
// NewRuntimeLimitsManager создаёт новый менеджер ограничений
|
||||
func NewRuntimeLimitsManager(cfg *RuntimeLimitsConfig, logger LoggerInterface) *RuntimeLimitsManager {
|
||||
if cfg == nil {
|
||||
cfg = DefaultRuntimeLimitsConfig()
|
||||
}
|
||||
|
||||
rlm := &RuntimeLimitsManager{
|
||||
globalMaxDocSize: int64(cfg.GlobalMaxDocSizeMB) * 1024 * 1024,
|
||||
globalMaxCollSize: cfg.GlobalMaxCollSizeMB * 1024 * 1024,
|
||||
globalMaxDocsPerColl: cfg.GlobalMaxDocsPerColl,
|
||||
collectionOverrides: make(map[string]*CollectionLimits),
|
||||
metrics: &LimitMetrics{},
|
||||
logger: logger,
|
||||
enabled: cfg.Enabled,
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Debug(fmt.Sprintf("Runtime limits manager initialized: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d",
|
||||
cfg.GlobalMaxDocSizeMB, cfg.GlobalMaxCollSizeMB, cfg.GlobalMaxDocsPerColl))
|
||||
}
|
||||
|
||||
return rlm
|
||||
}
|
||||
|
||||
// ValidateDocumentSize проверяет размер документа
|
||||
func (rlm *RuntimeLimitsManager) ValidateDocumentSize(dbName, collName string, docSize int64) error {
|
||||
if !rlm.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Проверяем переопределение для коллекции
|
||||
limit := rlm.getCollectionLimit(dbName, collName)
|
||||
|
||||
maxSize := rlm.globalMaxDocSize
|
||||
if limit != nil && limit.MaxDocSize > 0 {
|
||||
maxSize = limit.MaxDocSize
|
||||
}
|
||||
|
||||
if docSize > maxSize {
|
||||
rlm.metrics.RejectedBySize.Add(1)
|
||||
return fmt.Errorf("document size %d bytes exceeds limit %d bytes", docSize, maxSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCollectionSize проверяет размер коллекции
|
||||
func (rlm *RuntimeLimitsManager) ValidateCollectionSize(coll *Collection, newDocSize int64) error {
|
||||
if !rlm.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
|
||||
|
||||
maxSize := rlm.globalMaxCollSize
|
||||
if limit != nil && limit.MaxCollectionSize > 0 {
|
||||
maxSize = limit.MaxCollectionSize
|
||||
}
|
||||
|
||||
currentSize := coll.Size()
|
||||
if currentSize+newDocSize > maxSize {
|
||||
rlm.metrics.RejectedByCollSize.Add(1)
|
||||
return fmt.Errorf("collection size would exceed limit %d bytes (current: %d, new: %d)",
|
||||
maxSize, currentSize, newDocSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDocumentCount проверяет количество документов в коллекции
|
||||
func (rlm *RuntimeLimitsManager) ValidateDocumentCount(coll *Collection) error {
|
||||
if !rlm.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
|
||||
|
||||
maxDocs := rlm.globalMaxDocsPerColl
|
||||
if limit != nil && limit.MaxDocuments > 0 {
|
||||
maxDocs = limit.MaxDocuments
|
||||
}
|
||||
|
||||
currentCount := coll.Count()
|
||||
if currentCount >= maxDocs {
|
||||
rlm.metrics.RejectedByDocCount.Add(1)
|
||||
return fmt.Errorf("collection has reached maximum document count %d", maxDocs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCollectionLimit возвращает лимиты для коллекции
|
||||
func (rlm *RuntimeLimitsManager) getCollectionLimit(dbName, collName string) *CollectionLimits {
|
||||
rlm.mu.RLock()
|
||||
defer rlm.mu.RUnlock()
|
||||
|
||||
key := fmt.Sprintf("%s.%s", dbName, collName)
|
||||
if limits, ok := rlm.collectionOverrides[key]; ok {
|
||||
return limits
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCollectionLimits устанавливает лимиты для коллекции
|
||||
func (rlm *RuntimeLimitsManager) SetCollectionLimits(dbName, collName string, maxDocSizeMB int, maxCollSizeMB int64, maxDocuments int64) {
|
||||
rlm.mu.Lock()
|
||||
defer rlm.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s.%s", dbName, collName)
|
||||
rlm.collectionOverrides[key] = &CollectionLimits{
|
||||
MaxDocSize: int64(maxDocSizeMB) * 1024 * 1024,
|
||||
MaxCollectionSize: maxCollSizeMB * 1024 * 1024,
|
||||
MaxDocuments: maxDocuments,
|
||||
LastUpdated: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
if rlm.logger != nil {
|
||||
rlm.logger.Info(fmt.Sprintf("Set limits for %s: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d",
|
||||
key, maxDocSizeMB, maxCollSizeMB, maxDocuments))
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveCollectionLimits удаляет переопределения для коллекции
|
||||
func (rlm *RuntimeLimitsManager) RemoveCollectionLimits(dbName, collName string) {
|
||||
rlm.mu.Lock()
|
||||
defer rlm.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s.%s", dbName, collName)
|
||||
delete(rlm.collectionOverrides, key)
|
||||
|
||||
if rlm.logger != nil {
|
||||
rlm.logger.Info(fmt.Sprintf("Removed limits override for %s", key))
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetrics возвращает метрики
|
||||
func (rlm *RuntimeLimitsManager) GetMetrics() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"rejected_by_size": rlm.metrics.RejectedBySize.Load(),
|
||||
"rejected_by_doc_count": rlm.metrics.RejectedByDocCount.Load(),
|
||||
"rejected_by_coll_size": rlm.metrics.RejectedByCollSize.Load(),
|
||||
"global_max_doc_size_mb": rlm.globalMaxDocSize / (1024 * 1024),
|
||||
"global_max_coll_size_mb": rlm.globalMaxCollSize / (1024 * 1024),
|
||||
"global_max_docs_per_coll": rlm.globalMaxDocsPerColl,
|
||||
"enabled": rlm.enabled,
|
||||
}
|
||||
}
|
||||
2594
internal/storage/transaction.go
Normal file
2594
internal/storage/transaction.go
Normal file
File diff suppressed because it is too large
Load Diff
816
internal/storage/trigger.go
Normal file
816
internal/storage/trigger.go
Normal file
@@ -0,0 +1,816 @@
|
||||
/*
|
||||
* Copyright 2026 Safronov Grigorii
|
||||
*
|
||||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
* https://opensource.org/licenses/CDDL-1.0
|
||||
*/
|
||||
|
||||
// Файл: internal/storage/trigger.go
|
||||
// Назначение: Реализация триггеров, похожих на MongoDB trigger syntax.
|
||||
// Поддерживает события: INSERT, UPDATE, DELETE, REPLACE.
|
||||
// Триггеры могут выполняться до или после события.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
)
|
||||
|
||||
// TriggerEvent определяет тип события для триггера
|
||||
type TriggerEvent string
|
||||
|
||||
const (
|
||||
TriggerBeforeInsert TriggerEvent = "BEFORE_INSERT"
|
||||
TriggerAfterInsert TriggerEvent = "AFTER_INSERT"
|
||||
TriggerBeforeUpdate TriggerEvent = "BEFORE_UPDATE"
|
||||
TriggerAfterUpdate TriggerEvent = "AFTER_UPDATE"
|
||||
TriggerBeforeDelete TriggerEvent = "BEFORE_DELETE"
|
||||
TriggerAfterDelete TriggerEvent = "AFTER_DELETE"
|
||||
TriggerBeforeReplace TriggerEvent = "BEFORE_REPLACE"
|
||||
TriggerAfterReplace TriggerEvent = "AFTER_REPLACE"
|
||||
)
|
||||
|
||||
// TriggerAction определяет действие триггера
|
||||
type TriggerAction string
|
||||
|
||||
const (
|
||||
ActionAbort TriggerAction = "abort" // Прервать операцию
|
||||
ActionSkip TriggerAction = "skip" // Пропустить операцию
|
||||
ActionModify TriggerAction = "modify" // Модифицировать документ
|
||||
ActionLog TriggerAction = "log" // Записать в лог
|
||||
ActionNotify TriggerAction = "notify" // Отправить уведомление
|
||||
ActionCustom TriggerAction = "custom" // Пользовательское действие
|
||||
)
|
||||
|
||||
// Trigger представляет триггер на коллекции
|
||||
type Trigger struct {
|
||||
Name string `msgpack:"name"`
|
||||
Collection string `msgpack:"collection"`
|
||||
Event TriggerEvent `msgpack:"event"`
|
||||
Action TriggerAction `msgpack:"action"`
|
||||
Condition *TriggerCondition `msgpack:"condition"`
|
||||
Operations []TriggerOperation `msgpack:"operations"`
|
||||
CreatedAt int64 `msgpack:"created_at"`
|
||||
UpdatedAt int64 `msgpack:"updated_at"`
|
||||
Enabled bool `msgpack:"enabled"`
|
||||
Description string `msgpack:"description"`
|
||||
mu sync.RWMutex `msgpack:"-"`
|
||||
}
|
||||
|
||||
// TriggerCondition определяет условие выполнения триггера
|
||||
type TriggerCondition struct {
|
||||
Field string `msgpack:"field"` // Поле для проверки
|
||||
Operator string `msgpack:"operator"` // Оператор: eq, ne, gt, lt, gte, lte, in, nin, exists, regex
|
||||
Value interface{} `msgpack:"value"` // Значение для сравнения
|
||||
Match string `msgpack:"match"` // Паттерн для regex
|
||||
}
|
||||
|
||||
// TriggerOperation определяет операцию, выполняемую триггером
|
||||
type TriggerOperation struct {
|
||||
Type string `msgpack:"type"` // set, unset, inc, mul, rename, currentDate
|
||||
Field string `msgpack:"field"` // Поле для операции
|
||||
Value interface{} `msgpack:"value"` // Значение для операции
|
||||
Params map[string]interface{} `msgpack:"params"`
|
||||
}
|
||||
|
||||
// TriggerExecution содержит контекст выполнения триггера
|
||||
type TriggerExecution struct {
|
||||
TriggerName string
|
||||
Event TriggerEvent
|
||||
Collection string
|
||||
Database string
|
||||
DocumentID string
|
||||
OldDocument *Document
|
||||
NewDocument *Document
|
||||
Operation string
|
||||
Timestamp time.Time
|
||||
TimestampMs int64 `msgpack:"timestamp_ms"` // Unix миллисекунды (добавлено)
|
||||
TimestampStr string `msgpack:"timestamp_str"` // Человекочитаемая строка (добавлено)
|
||||
User string
|
||||
Role string
|
||||
CustomData map[string]interface{}
|
||||
ActionResult string `msgpack:"action_result"` // Результат выполнения (добавлено)
|
||||
DurationMs int64 `msgpack:"duration_ms"` // Длительность выполнения (добавлено)
|
||||
}
|
||||
|
||||
// TriggerManager управляет триггерами в СУБД
|
||||
type TriggerManager struct {
|
||||
triggers sync.Map // map[string]*Trigger (ключ: collection|event|name)
|
||||
logger *log.Logger
|
||||
mu sync.RWMutex
|
||||
auditLog []*TriggerExecution
|
||||
maxLogSize int
|
||||
}
|
||||
|
||||
var (
|
||||
globalTriggerManager *TriggerManager
|
||||
triggerManagerOnce sync.Once
|
||||
)
|
||||
|
||||
// GetTriggerManager возвращает глобальный менеджер триггеров
|
||||
func GetTriggerManager() *TriggerManager {
|
||||
triggerManagerOnce.Do(func() {
|
||||
globalTriggerManager = &TriggerManager{
|
||||
maxLogSize: 10000,
|
||||
auditLog: make([]*TriggerExecution, 0),
|
||||
}
|
||||
})
|
||||
return globalTriggerManager
|
||||
}
|
||||
|
||||
// InitTriggerManager инициализирует менеджер триггеров с логгером
|
||||
func InitTriggerManager(logger *log.Logger) {
|
||||
tm := GetTriggerManager()
|
||||
tm.logger = logger
|
||||
if logger != nil {
|
||||
logger.Info("Trigger manager initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTrigger создаёт новый триггер (синтаксис MongoDB-like)
|
||||
// Пример: db.collection.createTrigger("triggerName", "BEFORE_INSERT", {
|
||||
// condition: { field: "status", operator: "eq", value: "active" },
|
||||
// action: "modify",
|
||||
// operations: [
|
||||
// { type: "set", field: "updated_at", value: "$$NOW" }
|
||||
// ]
|
||||
// })
|
||||
func (tm *TriggerManager) CreateTrigger(database, collection, name string, event TriggerEvent, config map[string]interface{}) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
key := tm.getTriggerKey(collection, event, name)
|
||||
if _, exists := tm.triggers.Load(key); exists {
|
||||
return fmt.Errorf("trigger '%s' already exists on %s for event %s", name, collection, event)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
trigger := &Trigger{
|
||||
Name: name,
|
||||
Collection: collection,
|
||||
Event: event,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Operations: make([]TriggerOperation, 0),
|
||||
}
|
||||
|
||||
// Парсим конфигурацию триггера
|
||||
if action, ok := config["action"].(string); ok {
|
||||
switch strings.ToLower(action) {
|
||||
case "abort":
|
||||
trigger.Action = ActionAbort
|
||||
case "skip":
|
||||
trigger.Action = ActionSkip
|
||||
case "modify":
|
||||
trigger.Action = ActionModify
|
||||
case "log":
|
||||
trigger.Action = ActionLog
|
||||
case "notify":
|
||||
trigger.Action = ActionNotify
|
||||
default:
|
||||
trigger.Action = ActionCustom
|
||||
}
|
||||
}
|
||||
|
||||
// Парсим условие
|
||||
if cond, ok := config["condition"].(map[string]interface{}); ok {
|
||||
trigger.Condition = &TriggerCondition{}
|
||||
if field, ok := cond["field"].(string); ok {
|
||||
trigger.Condition.Field = field
|
||||
}
|
||||
if operator, ok := cond["operator"].(string); ok {
|
||||
trigger.Condition.Operator = operator
|
||||
}
|
||||
if value, ok := cond["value"]; ok {
|
||||
trigger.Condition.Value = value
|
||||
}
|
||||
if match, ok := cond["match"].(string); ok {
|
||||
trigger.Condition.Match = match
|
||||
}
|
||||
}
|
||||
|
||||
// Парсим операции
|
||||
if ops, ok := config["operations"].([]interface{}); ok {
|
||||
for _, opRaw := range ops {
|
||||
opMap, ok := opRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
operation := TriggerOperation{}
|
||||
if opType, ok := opMap["type"].(string); ok {
|
||||
operation.Type = opType
|
||||
}
|
||||
if field, ok := opMap["field"].(string); ok {
|
||||
operation.Field = field
|
||||
}
|
||||
if value, ok := opMap["value"]; ok {
|
||||
operation.Value = value
|
||||
}
|
||||
if params, ok := opMap["params"].(map[string]interface{}); ok {
|
||||
operation.Params = params
|
||||
}
|
||||
trigger.Operations = append(trigger.Operations, operation)
|
||||
}
|
||||
}
|
||||
|
||||
if desc, ok := config["description"].(string); ok {
|
||||
trigger.Description = desc
|
||||
}
|
||||
|
||||
tm.triggers.Store(key, trigger)
|
||||
|
||||
// Аудит создания триггера
|
||||
LogAudit("CREATE", "TRIGGER", fmt.Sprintf("%s.%s.%s", database, collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
"action": trigger.Action,
|
||||
"description": trigger.Description,
|
||||
})
|
||||
|
||||
if tm.logger != nil {
|
||||
tm.logger.Info(fmt.Sprintf("Trigger '%s' created on %s.%s for event %s", name, database, collection, event))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropTrigger удаляет триггер
|
||||
func (tm *TriggerManager) DropTrigger(collection, event, name string) error {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
if _, exists := tm.triggers.LoadAndDelete(key); !exists {
|
||||
return fmt.Errorf("trigger '%s' not found on %s for event %s", name, collection, event)
|
||||
}
|
||||
|
||||
// Аудит удаления триггера
|
||||
LogAudit("DROP", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
})
|
||||
|
||||
if tm.logger != nil {
|
||||
tm.logger.Info(fmt.Sprintf("Trigger '%s' dropped from %s for event %s", name, collection, event))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTrigger возвращает триггер по имени
|
||||
func (tm *TriggerManager) GetTrigger(collection, event, name string) (*Trigger, error) {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
if val, ok := tm.triggers.Load(key); ok {
|
||||
return val.(*Trigger), nil
|
||||
}
|
||||
return nil, fmt.Errorf("trigger not found: %s", name)
|
||||
}
|
||||
|
||||
// ListTriggers возвращает список всех триггеров для коллекции
|
||||
func (tm *TriggerManager) ListTriggers(collection string) []*Trigger {
|
||||
triggers := make([]*Trigger, 0)
|
||||
tm.triggers.Range(func(key, value interface{}) bool {
|
||||
trigger := value.(*Trigger)
|
||||
if collection == "" || trigger.Collection == collection {
|
||||
triggers = append(triggers, trigger)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return triggers
|
||||
}
|
||||
|
||||
// ListTriggersByEvent возвращает триггеры для конкретного события
|
||||
func (tm *TriggerManager) ListTriggersByEvent(collection string, event TriggerEvent) []*Trigger {
|
||||
triggers := make([]*Trigger, 0)
|
||||
tm.triggers.Range(func(key, value interface{}) bool {
|
||||
trigger := value.(*Trigger)
|
||||
if trigger.Collection == collection && trigger.Event == event && trigger.Enabled {
|
||||
triggers = append(triggers, trigger)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return triggers
|
||||
}
|
||||
|
||||
// EnableTrigger включает триггер
|
||||
func (tm *TriggerManager) EnableTrigger(collection, event, name string) error {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
val, ok := tm.triggers.Load(key)
|
||||
if !ok {
|
||||
return fmt.Errorf("trigger not found: %s", name)
|
||||
}
|
||||
trigger := val.(*Trigger)
|
||||
trigger.mu.Lock()
|
||||
trigger.Enabled = true
|
||||
trigger.UpdatedAt = time.Now().UnixMilli()
|
||||
trigger.mu.Unlock()
|
||||
tm.triggers.Store(key, trigger)
|
||||
|
||||
// Аудит включения триггера
|
||||
LogAudit("ENABLE", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableTrigger выключает триггер
|
||||
func (tm *TriggerManager) DisableTrigger(collection, event, name string) error {
|
||||
key := tm.getTriggerKey(collection, TriggerEvent(event), name)
|
||||
val, ok := tm.triggers.Load(key)
|
||||
if !ok {
|
||||
return fmt.Errorf("trigger not found: %s", name)
|
||||
}
|
||||
trigger := val.(*Trigger)
|
||||
trigger.mu.Lock()
|
||||
trigger.Enabled = false
|
||||
trigger.UpdatedAt = time.Now().UnixMilli()
|
||||
trigger.mu.Unlock()
|
||||
tm.triggers.Store(key, trigger)
|
||||
|
||||
// Аудит выключения триггера
|
||||
LogAudit("DISABLE", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{
|
||||
"event": event,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteTriggers выполняет все триггеры для данного события
|
||||
// Возвращает: modifiedDocument, shouldAbort, error
|
||||
func (tm *TriggerManager) ExecuteTriggers(execCtx *TriggerExecution) (*Document, bool, error) {
|
||||
triggers := tm.ListTriggersByEvent(execCtx.Collection, execCtx.Event)
|
||||
|
||||
if len(triggers) == 0 {
|
||||
return execCtx.NewDocument, false, nil
|
||||
}
|
||||
|
||||
currentDoc := execCtx.NewDocument
|
||||
if currentDoc == nil && execCtx.OldDocument != nil {
|
||||
currentDoc = execCtx.OldDocument.Clone()
|
||||
}
|
||||
|
||||
for _, trigger := range triggers {
|
||||
if !trigger.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// Проверяем условие
|
||||
if trigger.Condition != nil {
|
||||
if !tm.evaluateCondition(execCtx, trigger.Condition) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Выполняем действие триггера
|
||||
switch trigger.Action {
|
||||
case ActionAbort:
|
||||
tm.logExecution(execCtx, trigger, "aborted", startTime)
|
||||
return currentDoc, true, fmt.Errorf("operation aborted by trigger: %s", trigger.Name)
|
||||
|
||||
case ActionSkip:
|
||||
tm.logExecution(execCtx, trigger, "skipped", startTime)
|
||||
return currentDoc, true, nil
|
||||
|
||||
case ActionModify:
|
||||
if currentDoc != nil {
|
||||
currentDoc = tm.applyOperations(currentDoc, trigger.Operations, execCtx)
|
||||
}
|
||||
tm.logExecution(execCtx, trigger, "modified", startTime)
|
||||
|
||||
case ActionLog:
|
||||
tm.logExecution(execCtx, trigger, "logged", startTime)
|
||||
if tm.logger != nil {
|
||||
tm.logger.Info(fmt.Sprintf("Trigger %s executed on %s.%s (event: %s, doc: %s)",
|
||||
trigger.Name, execCtx.Database, execCtx.Collection, execCtx.Event, execCtx.DocumentID))
|
||||
}
|
||||
|
||||
case ActionNotify:
|
||||
tm.logExecution(execCtx, trigger, "notified", startTime)
|
||||
// Здесь можно отправить уведомление через WebSocket или другой канал
|
||||
}
|
||||
}
|
||||
|
||||
return currentDoc, false, nil
|
||||
}
|
||||
|
||||
// evaluateCondition проверяет условие триггера
|
||||
func (tm *TriggerManager) evaluateCondition(execCtx *TriggerExecution, cond *TriggerCondition) bool {
|
||||
var docToCheck *Document
|
||||
if execCtx.NewDocument != nil {
|
||||
docToCheck = execCtx.NewDocument
|
||||
} else if execCtx.OldDocument != nil {
|
||||
docToCheck = execCtx.OldDocument
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
fieldValue, err := docToCheck.GetField(cond.Field)
|
||||
if err != nil {
|
||||
// Поле не существует
|
||||
if cond.Operator == "exists" {
|
||||
if existsVal, ok := cond.Value.(bool); ok && !existsVal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
switch cond.Operator {
|
||||
case "eq":
|
||||
return fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", cond.Value)
|
||||
case "ne":
|
||||
return fmt.Sprintf("%v", fieldValue) != fmt.Sprintf("%v", cond.Value)
|
||||
case "gt":
|
||||
return compareNumbers(fieldValue, cond.Value) > 0
|
||||
case "lt":
|
||||
return compareNumbers(fieldValue, cond.Value) < 0
|
||||
case "gte":
|
||||
return compareNumbers(fieldValue, cond.Value) >= 0
|
||||
case "lte":
|
||||
return compareNumbers(fieldValue, cond.Value) <= 0
|
||||
case "in":
|
||||
if arr, ok := cond.Value.([]interface{}); ok {
|
||||
for _, v := range arr {
|
||||
if fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
case "nin":
|
||||
if arr, ok := cond.Value.([]interface{}); ok {
|
||||
for _, v := range arr {
|
||||
if fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
case "exists":
|
||||
if existsVal, ok := cond.Value.(bool); ok {
|
||||
return existsVal
|
||||
}
|
||||
return true
|
||||
case "regex":
|
||||
if pattern, ok := cond.Value.(string); ok {
|
||||
matched, _ := regexp.MatchString(pattern, fmt.Sprintf("%v", fieldValue))
|
||||
return matched
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// applyOperations применяет операции к документу
|
||||
func (tm *TriggerManager) applyOperations(doc *Document, ops []TriggerOperation, execCtx *TriggerExecution) *Document {
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := doc.Clone()
|
||||
|
||||
for _, op := range ops {
|
||||
switch op.Type {
|
||||
case "set":
|
||||
value := tm.resolveValue(op.Value, execCtx)
|
||||
result.SetField(op.Field, value)
|
||||
|
||||
case "unset":
|
||||
result.DeleteField(op.Field)
|
||||
|
||||
case "inc":
|
||||
if incVal, ok := toFloat64(op.Value); ok {
|
||||
if current, err := result.GetField(op.Field); err == nil {
|
||||
if currVal, ok := toFloat64(current); ok {
|
||||
result.SetField(op.Field, currVal+incVal)
|
||||
}
|
||||
} else {
|
||||
result.SetField(op.Field, incVal)
|
||||
}
|
||||
}
|
||||
|
||||
case "mul":
|
||||
if mulVal, ok := toFloat64(op.Value); ok {
|
||||
if current, err := result.GetField(op.Field); err == nil {
|
||||
if currVal, ok := toFloat64(current); ok {
|
||||
result.SetField(op.Field, currVal*mulVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "rename":
|
||||
if newName, ok := op.Value.(string); ok {
|
||||
if val, err := result.GetField(op.Field); err == nil {
|
||||
result.SetField(newName, val)
|
||||
result.DeleteField(op.Field)
|
||||
}
|
||||
}
|
||||
|
||||
case "currentDate":
|
||||
result.SetField(op.Field, time.Now().UnixMilli())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// resolveValue разрешает специальные значения типа $$NOW, $$USER
|
||||
func (tm *TriggerManager) resolveValue(value interface{}, execCtx *TriggerExecution) interface{} {
|
||||
if strVal, ok := value.(string); ok {
|
||||
switch strVal {
|
||||
case "$$NOW":
|
||||
return time.Now().UnixMilli()
|
||||
case "$$USER":
|
||||
if execCtx.User != "" {
|
||||
return execCtx.User
|
||||
}
|
||||
return "anonymous"
|
||||
case "$$ROLE":
|
||||
if execCtx.Role != "" {
|
||||
return execCtx.Role
|
||||
}
|
||||
return "anonymous"
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// logExecution логирует выполнение триггера (модифицировано с добавлением временных меток)
|
||||
func (tm *TriggerManager) logExecution(execCtx *TriggerExecution, trigger *Trigger, result string, startTime time.Time) {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
duration := time.Since(startTime)
|
||||
now := time.Now()
|
||||
nowMs := now.UnixMilli()
|
||||
nowStr := now.Format("2006-01-02 15:04:05.000")
|
||||
|
||||
execCtx.TriggerName = trigger.Name
|
||||
execCtx.Timestamp = now
|
||||
execCtx.TimestampMs = nowMs
|
||||
execCtx.TimestampStr = nowStr
|
||||
execCtx.ActionResult = result
|
||||
execCtx.DurationMs = duration.Milliseconds()
|
||||
|
||||
if len(tm.auditLog) >= tm.maxLogSize {
|
||||
tm.auditLog = tm.auditLog[1:]
|
||||
}
|
||||
tm.auditLog = append(tm.auditLog, execCtx)
|
||||
|
||||
// Аудит выполнения триггера
|
||||
LogAudit("TRIGGER_EXECUTE", "TRIGGER", fmt.Sprintf("%s.%s", trigger.Collection, trigger.Name), map[string]interface{}{
|
||||
"event": execCtx.Event,
|
||||
"result": result,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
"document_id": execCtx.DocumentID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetTriggerExecutionLog возвращает лог выполнения триггеров
|
||||
func (tm *TriggerManager) GetTriggerExecutionLog() []*TriggerExecution {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
result := make([]*TriggerExecution, len(tm.auditLog))
|
||||
copy(result, tm.auditLog)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTriggerExecutionLogFiltered возвращает отфильтрованный лог выполнения триггеров (добавлено)
|
||||
func (tm *TriggerManager) GetTriggerExecutionLogFiltered(triggerName, collection string, fromTime, toTime int64) []*TriggerExecution {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
result := make([]*TriggerExecution, 0)
|
||||
for _, entry := range tm.auditLog {
|
||||
if triggerName != "" && entry.TriggerName != triggerName {
|
||||
continue
|
||||
}
|
||||
if collection != "" && entry.Collection != collection {
|
||||
continue
|
||||
}
|
||||
if fromTime > 0 && entry.TimestampMs < fromTime {
|
||||
continue
|
||||
}
|
||||
if toTime > 0 && entry.TimestampMs > toTime {
|
||||
continue
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearTriggerLog очищает лог выполнения триггеров (добавлено)
|
||||
func (tm *TriggerManager) ClearTriggerLog() {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
tm.auditLog = make([]*TriggerExecution, 0)
|
||||
LogAudit("CLEAR", "TRIGGER_LOG", "all", nil)
|
||||
}
|
||||
|
||||
// GetTriggerExecutionStats возвращает статистику выполнения триггеров (добавлено)
|
||||
func (tm *TriggerManager) GetTriggerExecutionStats() map[string]interface{} {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
triggerStats := make(map[string]map[string]interface{})
|
||||
for _, entry := range tm.auditLog {
|
||||
if _, ok := triggerStats[entry.TriggerName]; !ok {
|
||||
triggerStats[entry.TriggerName] = map[string]interface{}{
|
||||
"total_executions": 0,
|
||||
"aborted": 0,
|
||||
"skipped": 0,
|
||||
"modified": 0,
|
||||
"logged": 0,
|
||||
"notified": 0,
|
||||
"total_duration_ms": int64(0),
|
||||
"avg_duration_ms": float64(0),
|
||||
}
|
||||
}
|
||||
|
||||
stats := triggerStats[entry.TriggerName]
|
||||
stats["total_executions"] = stats["total_executions"].(int) + 1
|
||||
stats["total_duration_ms"] = stats["total_duration_ms"].(int64) + entry.DurationMs
|
||||
|
||||
switch entry.ActionResult {
|
||||
case "aborted":
|
||||
stats["aborted"] = stats["aborted"].(int) + 1
|
||||
case "skipped":
|
||||
stats["skipped"] = stats["skipped"].(int) + 1
|
||||
case "modified":
|
||||
stats["modified"] = stats["modified"].(int) + 1
|
||||
case "logged":
|
||||
stats["logged"] = stats["logged"].(int) + 1
|
||||
case "notified":
|
||||
stats["notified"] = stats["notified"].(int) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Вычисляем среднюю длительность
|
||||
for _, stats := range triggerStats {
|
||||
if totalExec, ok := stats["total_executions"].(int); ok && totalExec > 0 {
|
||||
if totalDuration, ok := stats["total_duration_ms"].(int64); ok {
|
||||
stats["avg_duration_ms"] = float64(totalDuration) / float64(totalExec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_trigger_executions": len(tm.auditLog),
|
||||
"triggers_stats": triggerStats,
|
||||
"log_size": len(tm.auditLog),
|
||||
"max_log_size": tm.maxLogSize,
|
||||
}
|
||||
}
|
||||
|
||||
// getTriggerKey возвращает ключ для хранения триггера
|
||||
func (tm *TriggerManager) getTriggerKey(collection string, event TriggerEvent, name string) string {
|
||||
return fmt.Sprintf("%s|%s|%s", collection, event, name)
|
||||
}
|
||||
|
||||
// compareNumbers сравнивает два числа
|
||||
func compareNumbers(a, b interface{}) int {
|
||||
aVal, aOk := toFloat64(a)
|
||||
bVal, bOk := toFloat64(b)
|
||||
|
||||
if aOk && bOk {
|
||||
if aVal < bVal {
|
||||
return -1
|
||||
}
|
||||
if aVal > bVal {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// MongoDBLikeTriggerConfig создаёт конфигурацию триггера в стиле MongoDB
|
||||
// Пример использования:
|
||||
// config := MongoDBLikeTriggerConfig().
|
||||
// On("BEFORE_INSERT").
|
||||
// Condition("status", "eq", "active").
|
||||
// Set("updated_at", "$$NOW").
|
||||
// Build()
|
||||
func MongoDBLikeTriggerConfig() *TriggerConfigBuilder {
|
||||
return &TriggerConfigBuilder{
|
||||
config: make(map[string]interface{}),
|
||||
ops: make([]interface{}, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerConfigBuilder строитель конфигурации триггера
|
||||
type TriggerConfigBuilder struct {
|
||||
config map[string]interface{}
|
||||
ops []interface{}
|
||||
}
|
||||
|
||||
// On устанавливает событие триггера
|
||||
func (b *TriggerConfigBuilder) On(event string) *TriggerConfigBuilder {
|
||||
b.config["event"] = event
|
||||
return b
|
||||
}
|
||||
|
||||
// Condition добавляет условие
|
||||
func (b *TriggerConfigBuilder) Condition(field, operator string, value interface{}) *TriggerConfigBuilder {
|
||||
b.config["condition"] = map[string]interface{}{
|
||||
"field": field,
|
||||
"operator": operator,
|
||||
"value": value,
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ConditionRegex добавляет regex условие
|
||||
func (b *TriggerConfigBuilder) ConditionRegex(field, pattern string) *TriggerConfigBuilder {
|
||||
b.config["condition"] = map[string]interface{}{
|
||||
"field": field,
|
||||
"operator": "regex",
|
||||
"value": pattern,
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Set добавляет операцию установки поля
|
||||
func (b *TriggerConfigBuilder) Set(field string, value interface{}) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "set",
|
||||
"field": field,
|
||||
"value": value,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Unset добавляет операцию удаления поля
|
||||
func (b *TriggerConfigBuilder) Unset(field string) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "unset",
|
||||
"field": field,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Inc добавляет операцию инкремента
|
||||
func (b *TriggerConfigBuilder) Inc(field string, value float64) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "inc",
|
||||
"field": field,
|
||||
"value": value,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Mul добавляет операцию умножения
|
||||
func (b *TriggerConfigBuilder) Mul(field string, value float64) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "mul",
|
||||
"field": field,
|
||||
"value": value,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Rename добавляет операцию переименования поля
|
||||
func (b *TriggerConfigBuilder) Rename(oldName, newName string) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "rename",
|
||||
"field": oldName,
|
||||
"value": newName,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// CurrentDate добавляет операцию установки текущей даты
|
||||
func (b *TriggerConfigBuilder) CurrentDate(field string) *TriggerConfigBuilder {
|
||||
b.ops = append(b.ops, map[string]interface{}{
|
||||
"type": "currentDate",
|
||||
"field": field,
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// Action устанавливает действие триггера
|
||||
func (b *TriggerConfigBuilder) Action(action string) *TriggerConfigBuilder {
|
||||
b.config["action"] = action
|
||||
return b
|
||||
}
|
||||
|
||||
// Description устанавливает описание триггера
|
||||
func (b *TriggerConfigBuilder) Description(desc string) *TriggerConfigBuilder {
|
||||
b.config["description"] = desc
|
||||
return b
|
||||
}
|
||||
|
||||
// Build собирает конфигурацию
|
||||
func (b *TriggerConfigBuilder) Build() map[string]interface{} {
|
||||
b.config["operations"] = b.ops
|
||||
return b.config
|
||||
}
|
||||
Reference in New Issue
Block a user