2PC released
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, "*"}
|
||||
}
|
||||
1730
internal/api/http.go
Normal file
1730
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
3113
internal/api/webui.go
Normal file
3113
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
429
internal/cluster/network_replication.go
Normal file
429
internal/cluster/network_replication.go
Normal file
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* 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/network_replication.go
|
||||
// Назначение: Реальная сетевая репликация с повторными попытками и бэкоффом
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReplicationRetryConfig содержит конфигурацию повторных попыток
|
||||
type ReplicationRetryConfig struct {
|
||||
MaxRetries int
|
||||
InitialBackoff time.Duration
|
||||
MaxBackoff time.Duration
|
||||
BackoffFactor float64
|
||||
JitterEnabled bool
|
||||
}
|
||||
|
||||
// NetworkReplicationStats содержит статистику репликации
|
||||
type NetworkReplicationStats struct {
|
||||
TotalRequests atomic.Uint64
|
||||
SuccessfulReqs atomic.Uint64
|
||||
FailedReqs atomic.Uint64
|
||||
RetriedReqs atomic.Uint64
|
||||
AvgLatencyMs atomic.Uint64
|
||||
BytesSent atomic.Uint64
|
||||
BytesReceived atomic.Uint64
|
||||
}
|
||||
|
||||
// DefaultReplicationRetryConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultReplicationRetryConfig() *ReplicationRetryConfig {
|
||||
return &ReplicationRetryConfig{
|
||||
MaxRetries: 5,
|
||||
InitialBackoff: 100 * time.Millisecond,
|
||||
MaxBackoff: 10 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
JitterEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// validateConfig проверяет конфигурацию
|
||||
func validateConfig(c *ReplicationRetryConfig) error {
|
||||
if c.MaxRetries < 0 {
|
||||
return fmt.Errorf("max_retries cannot be negative")
|
||||
}
|
||||
if c.InitialBackoff < 0 {
|
||||
return fmt.Errorf("initial_backoff cannot be negative")
|
||||
}
|
||||
if c.MaxBackoff < c.InitialBackoff {
|
||||
return fmt.Errorf("max_backoff must be >= initial_backoff")
|
||||
}
|
||||
if c.BackoffFactor < 1.0 {
|
||||
return fmt.Errorf("backoff_factor must be >= 1.0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplicatedConnection представляет соединение с узлом
|
||||
type ReplicatedConnection struct {
|
||||
NodeID string
|
||||
Address string
|
||||
Conn net.Conn
|
||||
LastUsed atomic.Int64
|
||||
Failures atomic.Uint32
|
||||
IsHealthy atomic.Bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NetworkReplicator управляет сетевой репликацией
|
||||
type NetworkReplicator struct {
|
||||
config *ReplicationRetryConfig
|
||||
workerPool *WorkerPool
|
||||
dialTimeout time.Duration
|
||||
mu sync.RWMutex
|
||||
connections map[string]*ReplicatedConnection
|
||||
logger LoggerInterface
|
||||
stats NetworkReplicationStats
|
||||
}
|
||||
|
||||
// NewNetworkReplicator создаёт новый сетевой репликатор
|
||||
func NewNetworkReplicator(config *ReplicationRetryConfig, workerPool *WorkerPool, logger LoggerInterface) *NetworkReplicator {
|
||||
if config == nil {
|
||||
config = DefaultReplicationRetryConfig()
|
||||
}
|
||||
// Игнорируем ошибку валидации в production
|
||||
validateConfig(config)
|
||||
|
||||
return &NetworkReplicator{
|
||||
config: config,
|
||||
workerPool: workerPool,
|
||||
dialTimeout: 10 * time.Second,
|
||||
connections: make(map[string]*ReplicatedConnection),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// calculateBackoff вычисляет время задержки с бэкоффом
|
||||
func (nr *NetworkReplicator) calculateBackoff(attempt int) time.Duration {
|
||||
backoff := float64(nr.config.InitialBackoff) * math.Pow(nr.config.BackoffFactor, float64(attempt))
|
||||
if backoff > float64(nr.config.MaxBackoff) {
|
||||
backoff = float64(nr.config.MaxBackoff)
|
||||
}
|
||||
|
||||
duration := time.Duration(backoff)
|
||||
|
||||
if nr.config.JitterEnabled {
|
||||
// Добавляем случайный джиттер ±25%
|
||||
jitter := time.Duration(float64(duration) * 0.25)
|
||||
jitterDelta := int64(jitter) - int64(jitter/2)
|
||||
if jitterDelta < 0 {
|
||||
jitterDelta = 0
|
||||
}
|
||||
duration = duration + time.Duration(jitterDelta)
|
||||
}
|
||||
|
||||
return duration
|
||||
}
|
||||
|
||||
// randUint64 возвращает случайное 64-битное число
|
||||
func randUint64() uint64 {
|
||||
var buf [8]byte
|
||||
rand.Read(buf[:])
|
||||
return binary.LittleEndian.Uint64(buf[:])
|
||||
}
|
||||
|
||||
// randInt64 возвращает случайное число между min и max
|
||||
func randInt64(min, max int64) int64 {
|
||||
if min >= max {
|
||||
return min
|
||||
}
|
||||
delta := max - min
|
||||
if delta == 0 {
|
||||
return min
|
||||
}
|
||||
n := int64(randUint64() % uint64(delta))
|
||||
return min + n
|
||||
}
|
||||
|
||||
// randInt возвращает случайное целое
|
||||
func randInt(min, max int) int {
|
||||
if min >= max {
|
||||
return min
|
||||
}
|
||||
delta := max - min
|
||||
if delta == 0 {
|
||||
return min
|
||||
}
|
||||
n := int(randUint64() % uint64(delta))
|
||||
return min + n
|
||||
}
|
||||
|
||||
// Connect устанавливает соединение с узлом
|
||||
func (nr *NetworkReplicator) Connect(nodeID, address string) error {
|
||||
nr.mu.Lock()
|
||||
defer nr.mu.Unlock()
|
||||
|
||||
if conn, exists := nr.connections[nodeID]; exists {
|
||||
if conn.IsHealthy.Load() {
|
||||
return nil
|
||||
}
|
||||
conn.Conn.Close()
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", address, nr.dialTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to %s: %v", address, err)
|
||||
}
|
||||
|
||||
replicatedConn := &ReplicatedConnection{
|
||||
NodeID: nodeID,
|
||||
Address: address,
|
||||
Conn: conn,
|
||||
IsHealthy: atomic.Bool{},
|
||||
}
|
||||
replicatedConn.IsHealthy.Store(true)
|
||||
replicatedConn.LastUsed.Store(time.Now().UnixMilli())
|
||||
|
||||
nr.connections[nodeID] = replicatedConn
|
||||
|
||||
if nr.logger != nil {
|
||||
nr.logger.Debug(fmt.Sprintf("Connected to node %s at %s", nodeID, address))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendReplicationRequest отправляет запрос на репликацию
|
||||
func (nr *NetworkReplicator) sendReplicationRequest(conn net.Conn, data []byte) error {
|
||||
// Формируем сообщение: длина + данные
|
||||
lenBuf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(lenBuf, uint32(len(data)))
|
||||
|
||||
if _, err := conn.Write(lenBuf); err != nil {
|
||||
return fmt.Errorf("failed to write length: %v", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(data); err != nil {
|
||||
return fmt.Errorf("failed to write data: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readReplicationAck читает подтверждение репликации
|
||||
func (nr *NetworkReplicator) readReplicationAck(conn net.Conn) error {
|
||||
lenBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, lenBuf); err != nil {
|
||||
return fmt.Errorf("failed to read ack length: %v", err)
|
||||
}
|
||||
|
||||
ackLen := binary.BigEndian.Uint32(lenBuf)
|
||||
if ackLen > 1024 {
|
||||
return fmt.Errorf("ack too large: %d", ackLen)
|
||||
}
|
||||
|
||||
ackData := make([]byte, ackLen)
|
||||
if _, err := io.ReadFull(conn, ackData); err != nil {
|
||||
return fmt.Errorf("failed to read ack data: %v", err)
|
||||
}
|
||||
|
||||
var ack struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(ackData, &ack); err != nil {
|
||||
return fmt.Errorf("failed to parse ack: %v", err)
|
||||
}
|
||||
|
||||
if ack.Status != "ok" {
|
||||
return fmt.Errorf("negative ack: %s", ack.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doSendReplication выполняет отправку репликации
|
||||
func (nr *NetworkReplicator) doSendReplication(nodeID, address string, data []byte) error {
|
||||
conn := nr.getConnection(nodeID)
|
||||
if conn == nil {
|
||||
// Пытаемся подключиться
|
||||
if err := nr.Connect(nodeID, address); err != nil {
|
||||
return err
|
||||
}
|
||||
conn = nr.getConnection(nodeID)
|
||||
if conn == nil {
|
||||
return fmt.Errorf("failed to get connection for node %s", nodeID)
|
||||
}
|
||||
}
|
||||
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
|
||||
if conn.Conn == nil {
|
||||
return fmt.Errorf("connection is nil for node %s", nodeID)
|
||||
}
|
||||
|
||||
// Устанавливаем таймаут
|
||||
if err := conn.Conn.SetWriteDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
||||
return fmt.Errorf("failed to set write deadline: %v", err)
|
||||
}
|
||||
|
||||
// Отправляем данные
|
||||
if err := nr.sendReplicationRequest(conn.Conn, data); err != nil {
|
||||
conn.IsHealthy.Store(false)
|
||||
conn.Conn.Close()
|
||||
conn.Conn = nil
|
||||
return fmt.Errorf("write failed: %v", err)
|
||||
}
|
||||
|
||||
// Читаем подтверждение
|
||||
if err := conn.Conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||
return fmt.Errorf("failed to set read deadline: %v", err)
|
||||
}
|
||||
|
||||
if err := nr.readReplicationAck(conn.Conn); err != nil {
|
||||
conn.IsHealthy.Store(false)
|
||||
return fmt.Errorf("read ack failed: %v", err)
|
||||
}
|
||||
|
||||
conn.IsHealthy.Store(true)
|
||||
conn.LastUsed.Store(time.Now().UnixMilli())
|
||||
conn.Failures.Store(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Replicate отправляет запрос на репликацию с повторными попытками
|
||||
func (nr *NetworkReplicator) Replicate(targetNodeID, targetAddress string, data []byte) error {
|
||||
startTime := time.Now()
|
||||
nr.stats.TotalRequests.Add(1)
|
||||
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt <= nr.config.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
nr.stats.RetriedReqs.Add(1)
|
||||
backoff := nr.calculateBackoff(attempt - 1)
|
||||
if nr.logger != nil {
|
||||
nr.logger.Debug(fmt.Sprintf("Replication retry %d for node %s after %v", attempt, targetNodeID, backoff))
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
err := nr.doSendReplication(targetNodeID, targetAddress, data)
|
||||
if err == nil {
|
||||
latency := time.Since(startTime).Milliseconds()
|
||||
nr.stats.SuccessfulReqs.Add(1)
|
||||
nr.updateAvgLatency(latency)
|
||||
nr.stats.BytesSent.Add(uint64(len(data)))
|
||||
|
||||
if nr.logger != nil {
|
||||
nr.logger.Debug(fmt.Sprintf("Replication to %s succeeded after %d attempts, latency: %dms",
|
||||
targetNodeID, attempt+1, latency))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if nr.logger != nil {
|
||||
nr.logger.Warn(fmt.Sprintf("Replication attempt %d to %s failed: %v", attempt+1, targetNodeID, err))
|
||||
}
|
||||
|
||||
// Помечаем соединение как нездоровое
|
||||
nr.markUnhealthy(targetNodeID)
|
||||
}
|
||||
|
||||
nr.stats.FailedReqs.Add(1)
|
||||
return fmt.Errorf("replication failed after %d attempts: %v", nr.config.MaxRetries+1, lastErr)
|
||||
}
|
||||
|
||||
// getConnection возвращает соединение с узлом
|
||||
func (nr *NetworkReplicator) getConnection(nodeID string) *ReplicatedConnection {
|
||||
nr.mu.RLock()
|
||||
defer nr.mu.RUnlock()
|
||||
return nr.connections[nodeID]
|
||||
}
|
||||
|
||||
// markUnhealthy помечает соединение как нездоровое
|
||||
func (nr *NetworkReplicator) markUnhealthy(nodeID string) {
|
||||
nr.mu.RLock()
|
||||
conn, exists := nr.connections[nodeID]
|
||||
nr.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
conn.IsHealthy.Store(false)
|
||||
failures := conn.Failures.Add(1)
|
||||
if failures > 3 {
|
||||
if nr.logger != nil {
|
||||
nr.logger.Warn(fmt.Sprintf("Node %s marked as unhealthy after %d failures", nodeID, failures))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateAvgLatency обновляет среднюю задержку
|
||||
func (nr *NetworkReplicator) updateAvgLatency(latencyMs int64) {
|
||||
current := nr.stats.AvgLatencyMs.Load()
|
||||
successful := nr.stats.SuccessfulReqs.Load()
|
||||
if successful > 0 {
|
||||
newAvg := (current*uint64(successful-1) + uint64(latencyMs)) / uint64(successful)
|
||||
nr.stats.AvgLatencyMs.Store(newAvg)
|
||||
} else {
|
||||
nr.stats.AvgLatencyMs.Store(uint64(latencyMs))
|
||||
}
|
||||
}
|
||||
|
||||
// ReplicateDocumentAsync асинхронно реплицирует документ
|
||||
func (nr *NetworkReplicator) ReplicateDocumentAsync(docID string, targetNodes []*NodeInfo, data []byte) {
|
||||
for _, node := range targetNodes {
|
||||
targetNodeID := node.ID
|
||||
targetAddress := fmt.Sprintf("%s:%d", node.IP, node.Port)
|
||||
|
||||
err := nr.workerPool.SubmitFunc(fmt.Sprintf("replicate_%s_to_%s", docID, targetNodeID), func() error {
|
||||
return nr.Replicate(targetNodeID, targetAddress, data)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if nr.logger != nil {
|
||||
nr.logger.Error(fmt.Sprintf("Failed to submit replication task for %s to %s: %v", docID, targetNodeID, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику репликации
|
||||
func (nr *NetworkReplicator) GetStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"total_requests": nr.stats.TotalRequests.Load(),
|
||||
"successful": nr.stats.SuccessfulReqs.Load(),
|
||||
"failed": nr.stats.FailedReqs.Load(),
|
||||
"retried": nr.stats.RetriedReqs.Load(),
|
||||
"avg_latency_ms": nr.stats.AvgLatencyMs.Load(),
|
||||
"bytes_sent": nr.stats.BytesSent.Load(),
|
||||
"bytes_received": nr.stats.BytesReceived.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
// Close закрывает все соединения
|
||||
func (nr *NetworkReplicator) Close() error {
|
||||
nr.mu.Lock()
|
||||
defer nr.mu.Unlock()
|
||||
|
||||
for _, conn := range nr.connections {
|
||||
if conn.Conn != nil {
|
||||
conn.Conn.Close()
|
||||
}
|
||||
}
|
||||
nr.connections = make(map[string]*ReplicatedConnection)
|
||||
return nil
|
||||
}
|
||||
974
internal/cluster/node.go
Normal file
974
internal/cluster/node.go
Normal file
@@ -0,0 +1,974 @@
|
||||
/*
|
||||
* 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/node.go
|
||||
// Назначение: Реализация узла кластера (node) для распределённой СУБД с поддержкой временных меток.
|
||||
// Полностью lock-free с использованием атомарных операций.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/storage"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// NodeStatus представляет состояние узла кластера
|
||||
type NodeStatus int32
|
||||
|
||||
const (
|
||||
StatusOffline NodeStatus = iota
|
||||
StatusActive
|
||||
StatusSyncing
|
||||
StatusFailed
|
||||
)
|
||||
|
||||
// Node представляет отдельный узел в распределённой системе
|
||||
type Node struct {
|
||||
ID string // Уникальный идентификатор узла
|
||||
IP string // IP-адрес узла
|
||||
Port int // Порт для коммуникации
|
||||
Status atomic.Int32 // Атомарный статус узла (NodeStatus)
|
||||
Storage *storage.Storage
|
||||
logger *log.Logger
|
||||
coordinator *RaftCoordinator // Ссылка на координатора (теперь RaftCoordinator)
|
||||
lastSeen atomic.Int64 // Время последнего heartbeat (Unix миллисекунды)
|
||||
joinedAt atomic.Int64 // Время присоединения к кластеру
|
||||
createdAt int64 // Время создания узла
|
||||
startedAt int64 // Время последнего старта узла
|
||||
stoppedAt int64 // Время остановки узла
|
||||
incomingConn chan net.Conn // Канал для входящих соединений (wait-free)
|
||||
stopChan chan struct{}
|
||||
requestCount atomic.Uint64 // Счётчик обработанных запросов
|
||||
bytesRx atomic.Uint64 // Получено байт
|
||||
bytesTx atomic.Uint64 // Отправлено байт
|
||||
mu sync.RWMutex // Для синхронизации изменений статуса
|
||||
|
||||
// Новые компоненты для production-ready реализации
|
||||
workerPool *WorkerPool // Пул воркеров для ограничения горутин
|
||||
replicator *NetworkReplicator // Сетевой репликатор с retry и backoff
|
||||
connPool sync.Map // Пул соединений к другим узлам
|
||||
}
|
||||
|
||||
// NodeConfig содержит конфигурацию для создания узла
|
||||
type NodeConfig struct {
|
||||
IP string
|
||||
Port int
|
||||
Storage *storage.Storage
|
||||
Logger *log.Logger
|
||||
Coordinator *RaftCoordinator
|
||||
}
|
||||
|
||||
// NewNode создаёт новый экземпляр узла кластера
|
||||
func NewNode(ip string, port int, store *storage.Storage, logger *log.Logger) *Node {
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
// Создаём пул воркеров с ограничением (максимум 500 одновременных горутин)
|
||||
workerPool := NewWorkerPool(500, logger)
|
||||
|
||||
// Создаём сетевой репликатор с настройками retry и backoff
|
||||
replicator := NewNetworkReplicator(DefaultReplicationRetryConfig(), workerPool, logger)
|
||||
|
||||
node := &Node{
|
||||
ID: uuid.New().String(),
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Storage: store,
|
||||
logger: logger,
|
||||
incomingConn: make(chan net.Conn, 10000), // Увеличенный буфер
|
||||
stopChan: make(chan struct{}),
|
||||
createdAt: now,
|
||||
startedAt: now,
|
||||
workerPool: workerPool,
|
||||
replicator: replicator,
|
||||
}
|
||||
node.Status.Store(int32(StatusActive))
|
||||
node.lastSeen.Store(now)
|
||||
node.joinedAt.Store(0)
|
||||
|
||||
// Безопасный запуск сервера с обработкой паник
|
||||
SafeGoWithLogger(node.startTCPServer, logger, "TCPServer")
|
||||
SafeGoWithLogger(node.handleIncomingConnections, logger, "IncomingConnections")
|
||||
SafeGoWithLogger(node.heartbeatLoop, logger, "HeartbeatLoop")
|
||||
SafeGoWithLogger(node.connectionHealthMonitor, logger, "ConnectionHealthMonitor")
|
||||
|
||||
if logger != nil {
|
||||
logger.Info(fmt.Sprintf("Node %s created at %s with worker pool (max: 500)", node.ID, node.GetCreatedAtStr()))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// startTCPServer запускает TCP-сервер для приёма запросов от других узлов
|
||||
func (n *Node) startTCPServer() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("TCP server panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
// Перезапускаем сервер
|
||||
time.Sleep(5 * time.Second)
|
||||
SafeGoWithLogger(n.startTCPServer, n.logger, "TCPServer")
|
||||
}
|
||||
}()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", n.IP, n.Port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to start TCP server: %v", n.ID, err))
|
||||
}
|
||||
n.Status.Store(int32(StatusFailed))
|
||||
return
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s listening on %s (started at %s)", n.ID, addr, n.GetStartedAtStr()))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s TCP server stopped", n.ID))
|
||||
}
|
||||
return
|
||||
default:
|
||||
// Устанавливаем таймаут для Accept
|
||||
if err := listener.(*net.TCPListener).SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s accept error: %v", n.ID, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Устанавливаем таймауты на соединение
|
||||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
select {
|
||||
case n.incomingConn <- conn:
|
||||
n.bytesRx.Add(1)
|
||||
default:
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Node %s incoming connection queue full, dropping connection", n.ID))
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleIncomingConnections обрабатывает входящие соединения с использованием пула воркеров
|
||||
func (n *Node) handleIncomingConnections() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Incoming connections handler panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
// Перезапускаем обработчик
|
||||
SafeGoWithLogger(n.handleIncomingConnections, n.logger, "IncomingConnections")
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
case conn := <-n.incomingConn:
|
||||
n.requestCount.Add(1)
|
||||
// Используем пул воркеров вместо создания неограниченного количества горутин
|
||||
taskID := fmt.Sprintf("handle_conn_%d_%s", time.Now().UnixNano(), conn.RemoteAddr().String())
|
||||
err := n.workerPool.SubmitFunc(taskID, func() error {
|
||||
n.handleNodeRequest(conn)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Failed to submit connection task: %v", err))
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleNodeRequest обрабатывает конкретный запрос от другого узла
|
||||
func (n *Node) handleNodeRequest(conn net.Conn) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Request handler panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
}
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Устанавливаем таймаут на чтение
|
||||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
decoder := json.NewDecoder(conn)
|
||||
var req NodeRequest
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
if err != io.EOF && n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to decode request: %v", n.ID, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
n.lastSeen.Store(time.Now().UnixMilli())
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Node %s received request type %s from %s at %s",
|
||||
n.ID, req.Type, req.FromNode, time.UnixMilli(req.Timestamp).Format("15:04:05.000")))
|
||||
}
|
||||
|
||||
switch req.Type {
|
||||
case "replicate":
|
||||
n.handleReplicateRequest(req.Data)
|
||||
case "query":
|
||||
n.handleQueryRequest(req.Data, conn)
|
||||
case "sync":
|
||||
n.handleSyncRequest(req.Data, conn)
|
||||
case "heartbeat":
|
||||
n.handleHeartbeatRequest(req, conn)
|
||||
case "status_sync":
|
||||
n.handleStatusSyncRequest(req, conn)
|
||||
default:
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Node %s unknown request type: %s", n.ID, req.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleReplicateRequest обрабатывает запрос на репликацию документа
|
||||
func (n *Node) handleReplicateRequest(data []byte) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Replicate request handler panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
startTime := time.Now().UnixMilli()
|
||||
|
||||
var repData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
Document map[string]interface{} `json:"document"`
|
||||
SourceNode string `json:"source_node"`
|
||||
ReplicaID string `json:"replica_id"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &repData); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to unmarshal replicate data: %v", n.ID, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
db, err := n.Storage.GetDatabase(repData.Database)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s database not found for replication: %s", n.ID, repData.Database))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(repData.Collection)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s collection not found for replication: %s", n.ID, repData.Collection))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Безопасное получение ID документа
|
||||
docID, ok := repData.Document["_id"].(string)
|
||||
if !ok {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s document missing _id field", n.ID))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
doc := &storage.Document{
|
||||
ID: docID,
|
||||
Fields: repData.Document,
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to replicate document: %v", n.ID, err))
|
||||
}
|
||||
} else {
|
||||
duration := time.Now().UnixMilli() - startTime
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Node %s replicated document %s from %s (took %d ms)",
|
||||
n.ID, doc.ID, repData.SourceNode, duration))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleQueryRequest обрабатывает запрос на чтение данных с узла
|
||||
func (n *Node) handleQueryRequest(data []byte, conn net.Conn) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Query request handler panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
n.sendErrorResponse(conn, "Internal server error")
|
||||
}
|
||||
}()
|
||||
|
||||
startTime := time.Now().UnixMilli()
|
||||
|
||||
var queryData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
DocumentID string `json:"document_id"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &queryData); err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
db, err := n.Storage.GetDatabase(queryData.Database)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(queryData.Collection)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := coll.Find(queryData.DocumentID)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
duration := time.Now().UnixMilli() - startTime
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
"data": doc,
|
||||
"node_id": n.ID,
|
||||
"request_id": queryData.RequestID,
|
||||
"duration_ms": duration,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
encoder := json.NewEncoder(conn)
|
||||
if err := encoder.Encode(response); err == nil {
|
||||
responseData, _ := json.Marshal(response)
|
||||
n.bytesTx.Add(uint64(len(responseData)))
|
||||
}
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Node %s handled query for %s.%s:%s (took %d ms)",
|
||||
n.ID, queryData.Database, queryData.Collection, queryData.DocumentID, duration))
|
||||
}
|
||||
}
|
||||
|
||||
// handleSyncRequest обрабатывает запрос на синхронизацию всей коллекции
|
||||
func (n *Node) handleSyncRequest(data []byte, conn net.Conn) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Sync request handler panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
n.sendErrorResponse(conn, "Internal server error")
|
||||
}
|
||||
}()
|
||||
|
||||
startTime := time.Now().UnixMilli()
|
||||
|
||||
var syncData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
RequestID string `json:"request_id"`
|
||||
Since int64 `json:"since"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &syncData); err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
db, err := n.Storage.GetDatabase(syncData.Database)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(syncData.Collection)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
docs := coll.GetAllDocuments()
|
||||
|
||||
if syncData.Since > 0 {
|
||||
filtered := make([]*storage.Document, 0)
|
||||
for _, doc := range docs {
|
||||
if doc.UpdatedAt > syncData.Since {
|
||||
filtered = append(filtered, doc)
|
||||
}
|
||||
}
|
||||
docs = filtered
|
||||
}
|
||||
|
||||
duration := time.Now().UnixMilli() - startTime
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
"docs": docs,
|
||||
"count": len(docs),
|
||||
"node_id": n.ID,
|
||||
"request_id": syncData.RequestID,
|
||||
"duration_ms": duration,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
"sync_duration_ms": duration,
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
|
||||
encoder := json.NewEncoder(conn)
|
||||
if err := encoder.Encode(response); err != nil && n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Failed to send sync response: %v", err))
|
||||
}
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s synced %d documents from %s.%s (took %d ms)",
|
||||
n.ID, len(docs), syncData.Database, syncData.Collection, duration))
|
||||
}
|
||||
}
|
||||
|
||||
// handleHeartbeatRequest обрабатывает heartbeat запрос
|
||||
func (n *Node) handleHeartbeatRequest(req NodeRequest, conn net.Conn) {
|
||||
n.lastSeen.Store(time.Now().UnixMilli())
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "alive",
|
||||
"node_id": n.ID,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
"uptime_ms": time.Now().UnixMilli() - n.startedAt,
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// handleStatusSyncRequest обрабатывает запрос синхронизации статуса (для split-brain recovery)
|
||||
func (n *Node) handleStatusSyncRequest(req NodeRequest, conn net.Conn) {
|
||||
var syncStatus struct {
|
||||
LeaderID string `json:"leader_id"`
|
||||
Term uint64 `json:"term"`
|
||||
ClusterSize int `json:"cluster_size"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(req.Data, &syncStatus); err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if n.coordinator != nil {
|
||||
n.coordinator.HandleStatusSync(syncStatus.LeaderID, syncStatus.Term, syncStatus.ClusterSize)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "synced",
|
||||
"node_id": n.ID,
|
||||
"term": n.coordinator.GetCurrentTerm(),
|
||||
"is_leader": n.coordinator.IsLeader(),
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// sendErrorResponse отправляет ошибку в ответ на запрос
|
||||
func (n *Node) sendErrorResponse(conn net.Conn, errMsg string) {
|
||||
response := map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": errMsg,
|
||||
"node_id": n.ID,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// heartbeatLoop отправляет периодические сигналы жизни координатору
|
||||
func (n *Node) heartbeatLoop() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Heartbeat loop panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
// Перезапускаем heartbeat loop
|
||||
time.Sleep(5 * time.Second)
|
||||
SafeGoWithLogger(n.heartbeatLoop, n.logger, "HeartbeatLoop")
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if n.coordinator != nil {
|
||||
n.coordinator.SendHeartbeat(n.ID)
|
||||
n.lastSeen.Store(time.Now().UnixMilli())
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Node %s sent heartbeat at %s", n.ID, n.GetLastSeenStr()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connectionHealthMonitor мониторит здоровье соединений
|
||||
func (n *Node) connectionHealthMonitor() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Connection health monitor panicked: %v\n%s", r, debug.Stack()))
|
||||
}
|
||||
SafeGoWithLogger(n.connectionHealthMonitor, n.logger, "ConnectionHealthMonitor")
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
n.cleanupStaleConnections()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupStaleConnections очищает старые соединения
|
||||
func (n *Node) cleanupStaleConnections() {
|
||||
n.connPool.Range(func(key, value interface{}) bool {
|
||||
if conn, ok := value.(net.Conn); ok {
|
||||
// Проверяем, активно ли соединение
|
||||
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||
buf := make([]byte, 1)
|
||||
_, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
n.connPool.Delete(key)
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Cleaned up stale connection for %v", key))
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// GetNodeStatus возвращает текущий статус узла (атомарно)
|
||||
func (n *Node) GetNodeStatus() NodeStatus {
|
||||
return NodeStatus(n.Status.Load())
|
||||
}
|
||||
|
||||
// IsActive проверяет, активен ли узел
|
||||
func (n *Node) IsActive() bool {
|
||||
return NodeStatus(n.Status.Load()) == StatusActive
|
||||
}
|
||||
|
||||
// SetStatus устанавливает статус узла с синхронизацией через Raft
|
||||
func (n *Node) SetStatus(status NodeStatus) error {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
oldStatus := n.Status.Load()
|
||||
if oldStatus == int32(status) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if n.coordinator != nil && n.coordinator.IsLeader() {
|
||||
if err := n.coordinator.UpdateNodeStatus(n.ID, status); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Failed to update node status via Raft: %v", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
n.Status.Store(int32(status))
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s status changed from %d to %d at %s",
|
||||
n.ID, oldStatus, status, time.Now().Format("2006-01-02 15:04:05.000")))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCoordinator устанавливает координатора для узла
|
||||
func (n *Node) SetCoordinator(coord *RaftCoordinator) {
|
||||
n.coordinator = coord
|
||||
now := time.Now().UnixMilli()
|
||||
n.joinedAt.Store(now)
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s joined cluster at %s", n.ID, n.GetJoinedAtStr()))
|
||||
}
|
||||
}
|
||||
|
||||
// JoinCluster присоединяет узел к кластеру
|
||||
func (n *Node) JoinCluster(coord *RaftCoordinator) error {
|
||||
if n.coordinator != nil {
|
||||
return fmt.Errorf("node already joined to cluster")
|
||||
}
|
||||
|
||||
n.SetCoordinator(coord)
|
||||
|
||||
if err := coord.RegisterNode(n); err != nil {
|
||||
return fmt.Errorf("failed to register node: %v", err)
|
||||
}
|
||||
|
||||
if err := n.SetStatus(StatusActive); err != nil {
|
||||
return fmt.Errorf("failed to set active status: %v", err)
|
||||
}
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s successfully joined cluster at %s", n.ID, n.GetJoinedAtStr()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LeaveCluster покидает кластер
|
||||
func (n *Node) LeaveCluster() error {
|
||||
if n.coordinator == nil {
|
||||
return fmt.Errorf("node not in cluster")
|
||||
}
|
||||
|
||||
if err := n.SetStatus(StatusOffline); err != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Failed to set offline status: %v", err))
|
||||
}
|
||||
|
||||
if err := n.coordinator.RemoveNode(n.ID); err != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Failed to remove node from coordinator: %v", err))
|
||||
}
|
||||
|
||||
n.coordinator = nil
|
||||
n.joinedAt.Store(0)
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s left cluster at %s", n.ID, time.Now().Format("2006-01-02 15:04:05.000")))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLastSeen возвращает время последнего контакта
|
||||
func (n *Node) GetLastSeen() int64 {
|
||||
return n.lastSeen.Load()
|
||||
}
|
||||
|
||||
// GetLastSeenStr возвращает человекочитаемое время последнего контакта
|
||||
func (n *Node) GetLastSeenStr() string {
|
||||
lastSeen := n.lastSeen.Load()
|
||||
if lastSeen == 0 {
|
||||
return "never"
|
||||
}
|
||||
return time.UnixMilli(lastSeen).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetJoinedAt возвращает время присоединения к кластеру
|
||||
func (n *Node) GetJoinedAt() int64 {
|
||||
return n.joinedAt.Load()
|
||||
}
|
||||
|
||||
// GetJoinedAtStr возвращает человекочитаемое время присоединения
|
||||
func (n *Node) GetJoinedAtStr() string {
|
||||
joinedAt := n.joinedAt.Load()
|
||||
if joinedAt == 0 {
|
||||
return "not joined"
|
||||
}
|
||||
return time.UnixMilli(joinedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetStartedAt возвращает время старта узла
|
||||
func (n *Node) GetStartedAt() int64 {
|
||||
return n.startedAt
|
||||
}
|
||||
|
||||
// GetStartedAtStr возвращает человекочитаемое время старта
|
||||
func (n *Node) GetStartedAtStr() string {
|
||||
return time.UnixMilli(n.startedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetCreatedAt возвращает время создания узла
|
||||
func (n *Node) GetCreatedAt() int64 {
|
||||
return n.createdAt
|
||||
}
|
||||
|
||||
// GetCreatedAtStr возвращает человекочитаемое время создания
|
||||
func (n *Node) GetCreatedAtStr() string {
|
||||
return time.UnixMilli(n.createdAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetUptime возвращает время работы узла
|
||||
func (n *Node) GetUptime() time.Duration {
|
||||
if n.startedAt == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(time.Now().UnixMilli()-n.startedAt) * time.Millisecond
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику узла
|
||||
func (n *Node) GetStats() map[string]interface{} {
|
||||
stats := map[string]interface{}{
|
||||
"id": n.ID,
|
||||
"ip": n.IP,
|
||||
"port": n.Port,
|
||||
"status": n.GetNodeStatus(),
|
||||
"created_at": n.GetCreatedAtStr(),
|
||||
"started_at": n.GetStartedAtStr(),
|
||||
"joined_at": n.GetJoinedAtStr(),
|
||||
"last_seen": n.GetLastSeenStr(),
|
||||
"uptime": n.GetUptime().String(),
|
||||
"request_count": n.requestCount.Load(),
|
||||
"bytes_rx": n.bytesRx.Load(),
|
||||
"bytes_tx": n.bytesTx.Load(),
|
||||
}
|
||||
|
||||
if n.workerPool != nil {
|
||||
stats["worker_pool"] = n.workerPool.GetStats()
|
||||
}
|
||||
|
||||
if n.replicator != nil {
|
||||
stats["replication"] = n.replicator.GetStats()
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// Stop останавливает работу узла
|
||||
func (n *Node) Stop() {
|
||||
if n.coordinator != nil && n.coordinator.IsLeader() {
|
||||
n.SetStatus(StatusOffline)
|
||||
}
|
||||
n.Status.Store(int32(StatusOffline))
|
||||
n.stoppedAt = time.Now().UnixMilli()
|
||||
|
||||
close(n.stopChan)
|
||||
|
||||
if n.workerPool != nil {
|
||||
n.workerPool.Stop()
|
||||
}
|
||||
|
||||
if n.replicator != nil {
|
||||
n.replicator.Close()
|
||||
}
|
||||
|
||||
// Закрываем все соединения в пуле
|
||||
n.connPool.Range(func(key, value interface{}) bool {
|
||||
if conn, ok := value.(net.Conn); ok {
|
||||
conn.Close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s stopped at %s", n.ID, time.UnixMilli(n.stoppedAt).Format("2006-01-02 15:04:05.000")))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAddress возвращает адрес узла в формате "ip:port"
|
||||
func (n *Node) GetAddress() string {
|
||||
return fmt.Sprintf("%s:%d", n.IP, n.Port)
|
||||
}
|
||||
|
||||
// generateReplicationID генерирует уникальный ID для репликации
|
||||
func generateReplicationID() string {
|
||||
bytes := make([]byte, 16)
|
||||
rand.Read(bytes)
|
||||
return base64.URLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// ReplicateDocument отправляет документ на репликацию всем активным узлам с retry и backoff
|
||||
func (n *Node) ReplicateDocument(database, collection string, doc *storage.Document) error {
|
||||
if n.coordinator == nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Warn("No coordinator set, skipping replication")
|
||||
}
|
||||
return fmt.Errorf("no coordinator set")
|
||||
}
|
||||
|
||||
nodes := n.coordinator.GetActiveNodes()
|
||||
if len(nodes) <= 1 {
|
||||
if n.logger != nil {
|
||||
n.logger.Debug("No other nodes for replication")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Подготавливаем данные для репликации
|
||||
repData := struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
Document map[string]interface{} `json:"document"`
|
||||
SourceNode string `json:"source_node"`
|
||||
ReplicaID string `json:"replica_id"`
|
||||
}{
|
||||
Database: database,
|
||||
Collection: collection,
|
||||
Document: doc.GetFields(),
|
||||
SourceNode: n.ID,
|
||||
ReplicaID: generateReplicationID(),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(repData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal replication data: %v", err)
|
||||
}
|
||||
|
||||
startTime := time.Now().UnixMilli()
|
||||
|
||||
// Отправляем на все узлы, кроме себя
|
||||
var wg sync.WaitGroup
|
||||
var failedCount atomic.Int32
|
||||
var successCount atomic.Int32
|
||||
|
||||
for _, nodeInfo := range nodes {
|
||||
if nodeInfo.ID == n.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
targetNodeID := nodeInfo.ID
|
||||
targetAddress := fmt.Sprintf("%s:%d", nodeInfo.IP, nodeInfo.Port)
|
||||
|
||||
// Асинхронная репликация с повторными попытками через пул воркеров
|
||||
taskID := fmt.Sprintf("replicate_%s_to_%s_%s", doc.ID, targetNodeID, repData.ReplicaID)
|
||||
err := n.workerPool.SubmitFunc(taskID, func() error {
|
||||
defer wg.Done()
|
||||
|
||||
if n.replicator == nil {
|
||||
failedCount.Add(1)
|
||||
return fmt.Errorf("replicator not initialized")
|
||||
}
|
||||
|
||||
err := n.replicator.Replicate(targetNodeID, targetAddress, data)
|
||||
if err != nil {
|
||||
failedCount.Add(1)
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Failed to replicate document %s to node %s after retries: %v",
|
||||
doc.ID, targetNodeID, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
successCount.Add(1)
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Successfully replicated document %s to node %s", doc.ID, targetNodeID))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
wg.Done()
|
||||
failedCount.Add(1)
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Failed to submit replication task for %s to %s: %v",
|
||||
doc.ID, targetNodeID, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ждём завершения всех репликаций с таймаутом
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
duration := time.Now().UnixMilli() - startTime
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Replicated document %s to %d/%d nodes (took %d ms)",
|
||||
doc.ID, successCount.Load(), len(nodes)-1, duration))
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Replication timeout for document %s after %d ms", doc.ID, time.Now().UnixMilli()-startTime))
|
||||
}
|
||||
return fmt.Errorf("replication timeout")
|
||||
}
|
||||
|
||||
if failedCount.Load() > 0 {
|
||||
return fmt.Errorf("replication partially failed: %d of %d nodes failed", failedCount.Load(), len(nodes)-1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWorkerPoolStats возвращает статистику пула воркеров
|
||||
func (n *Node) GetWorkerPoolStats() map[string]interface{} {
|
||||
if n.workerPool == nil {
|
||||
return map[string]interface{}{"enabled": false}
|
||||
}
|
||||
return n.workerPool.GetStats()
|
||||
}
|
||||
|
||||
// GetReplicationStats возвращает статистику репликации
|
||||
func (n *Node) GetReplicationStats() map[string]interface{} {
|
||||
if n.replicator == nil {
|
||||
return map[string]interface{}{"enabled": false}
|
||||
}
|
||||
return n.replicator.GetStats()
|
||||
}
|
||||
2947
internal/cluster/raft_coordinator.go
Normal file
2947
internal/cluster/raft_coordinator.go
Normal file
File diff suppressed because it is too large
Load Diff
289
internal/cluster/recovery.go
Normal file
289
internal/cluster/recovery.go
Normal file
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 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/recovery.go
|
||||
// Назначение: Автоматическое восстановление узлов после сбоя
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NodeState представляет состояние узла для восстановления
|
||||
type NodeState struct {
|
||||
NodeID string `json:"node_id"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
LastLogIndex uint64 `json:"last_log_index"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
IsRecovering bool `json:"is_recovering"`
|
||||
}
|
||||
|
||||
// RecoveryManager управляет восстановлением узлов
|
||||
type RecoveryManager struct {
|
||||
coordinator *RaftCoordinator
|
||||
logger LoggerInterface
|
||||
states sync.Map
|
||||
maxFailures int
|
||||
recoveryDelay time.Duration
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
isActive atomic.Bool
|
||||
}
|
||||
|
||||
// NewRecoveryManager создаёт новый менеджер восстановления
|
||||
func NewRecoveryManager(coordinator *RaftCoordinator, logger LoggerInterface) *RecoveryManager {
|
||||
rm := &RecoveryManager{
|
||||
coordinator: coordinator,
|
||||
logger: logger,
|
||||
maxFailures: 3,
|
||||
recoveryDelay: 30 * time.Second,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
return rm
|
||||
}
|
||||
|
||||
// Start запускает мониторинг восстановления
|
||||
func (rm *RecoveryManager) Start() {
|
||||
rm.isActive.Store(true)
|
||||
rm.wg.Add(2)
|
||||
go rm.monitorLoop()
|
||||
go rm.recoveryLoop()
|
||||
|
||||
if rm.logger != nil {
|
||||
rm.logger.Info("Recovery manager started")
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает менеджер восстановления
|
||||
func (rm *RecoveryManager) Stop() {
|
||||
rm.isActive.Store(false)
|
||||
close(rm.stopChan)
|
||||
rm.wg.Wait()
|
||||
|
||||
if rm.logger != nil {
|
||||
rm.logger.Info("Recovery manager stopped")
|
||||
}
|
||||
}
|
||||
|
||||
// monitorLoop отслеживает состояние узлов
|
||||
func (rm *RecoveryManager) monitorLoop() {
|
||||
defer rm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rm.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
rm.checkNodesHealth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkNodesHealth проверяет здоровье узлов
|
||||
func (rm *RecoveryManager) checkNodesHealth() {
|
||||
if rm.coordinator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
nodes := rm.coordinator.GetAllNodes()
|
||||
now := time.Now()
|
||||
|
||||
for _, node := range nodes {
|
||||
stateVal, ok := rm.states.Load(node.ID)
|
||||
var state *NodeState
|
||||
if ok {
|
||||
state = stateVal.(*NodeState)
|
||||
} else {
|
||||
state = &NodeState{
|
||||
NodeID: node.ID,
|
||||
LastSeen: now,
|
||||
LastLogIndex: 0,
|
||||
FailureCount: 0,
|
||||
IsRecovering: false,
|
||||
}
|
||||
rm.states.Store(node.ID, state)
|
||||
}
|
||||
|
||||
// Проверяем, был ли недавно heartbeat
|
||||
lastSeen := time.UnixMilli(node.LastSeen)
|
||||
if now.Sub(lastSeen) > 30*time.Second {
|
||||
state.FailureCount++
|
||||
if rm.logger != nil {
|
||||
rm.logger.Warn(fmt.Sprintf("Node %s appears unhealthy, failure count: %d", node.ID, state.FailureCount))
|
||||
}
|
||||
|
||||
if state.FailureCount >= rm.maxFailures && !state.IsRecovering {
|
||||
rm.triggerRecovery(node.ID)
|
||||
}
|
||||
} else {
|
||||
if state.FailureCount > 0 {
|
||||
state.FailureCount = 0
|
||||
if rm.logger != nil {
|
||||
rm.logger.Info(fmt.Sprintf("Node %s recovered", node.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.LastSeen = now
|
||||
rm.states.Store(node.ID, state)
|
||||
}
|
||||
}
|
||||
|
||||
// triggerRecovery запускает восстановление узла
|
||||
func (rm *RecoveryManager) triggerRecovery(nodeID string) {
|
||||
stateVal, ok := rm.states.Load(nodeID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
state := stateVal.(*NodeState)
|
||||
if state.IsRecovering {
|
||||
return
|
||||
}
|
||||
|
||||
state.IsRecovering = true
|
||||
rm.states.Store(nodeID, state)
|
||||
|
||||
if rm.logger != nil {
|
||||
rm.logger.Info(fmt.Sprintf("Triggering recovery for node %s", nodeID))
|
||||
}
|
||||
|
||||
// Запускаем асинхронное восстановление
|
||||
go rm.recoverNode(nodeID)
|
||||
}
|
||||
|
||||
// recoverNode восстанавливает узел
|
||||
func (rm *RecoveryManager) recoverNode(nodeID string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if rm.logger != nil {
|
||||
rm.logger.Error(fmt.Sprintf("Recovery for node %s panicked: %v", nodeID, r))
|
||||
}
|
||||
}
|
||||
|
||||
// Сбрасываем флаг восстановления
|
||||
if stateVal, ok := rm.states.Load(nodeID); ok {
|
||||
state := stateVal.(*NodeState)
|
||||
state.IsRecovering = false
|
||||
rm.states.Store(nodeID, state)
|
||||
}
|
||||
}()
|
||||
|
||||
// Ждём перед попыткой восстановления
|
||||
time.Sleep(rm.recoveryDelay)
|
||||
|
||||
if rm.coordinator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, не восстановился ли узел самостоятельно
|
||||
node := rm.coordinator.GetNodeByID(nodeID)
|
||||
if node != nil && time.Now().UnixMilli()-node.LastSeen < 30000 {
|
||||
if rm.logger != nil {
|
||||
rm.logger.Info(fmt.Sprintf("Node %s recovered on its own", nodeID))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Пытаемся переподключить узел
|
||||
if rm.logger != nil {
|
||||
rm.logger.Info(fmt.Sprintf("Attempting to reconnect node %s", nodeID))
|
||||
}
|
||||
|
||||
// Обновляем статус узла
|
||||
if err := rm.coordinator.UpdateNodeStatus(nodeID, StatusActive); err != nil {
|
||||
if rm.logger != nil {
|
||||
rm.logger.Error(fmt.Sprintf("Failed to update node %s status: %v", nodeID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Запускаем синхронизацию данных
|
||||
if err := rm.syncNodeData(nodeID); err != nil {
|
||||
if rm.logger != nil {
|
||||
rm.logger.Error(fmt.Sprintf("Failed to sync node %s data: %v", nodeID, err))
|
||||
}
|
||||
}
|
||||
|
||||
if rm.logger != nil {
|
||||
rm.logger.Info(fmt.Sprintf("Recovery completed for node %s", nodeID))
|
||||
}
|
||||
}
|
||||
|
||||
// syncNodeData синхронизирует данные с узлом
|
||||
func (rm *RecoveryManager) syncNodeData(nodeID string) error {
|
||||
// Получаем информацию об узле
|
||||
node := rm.coordinator.GetNodeByID(nodeID)
|
||||
if node == nil {
|
||||
return fmt.Errorf("node not found: %s", nodeID)
|
||||
}
|
||||
|
||||
if rm.logger != nil {
|
||||
rm.logger.Debug(fmt.Sprintf("Syncing data with node %s at %s:%d", nodeID, node.IP, node.Port))
|
||||
}
|
||||
|
||||
// В реальной реализации здесь была бы синхронизация данных
|
||||
// Например, отправка всех изменений с момента последнего успешного heartbeat
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recoveryLoop периодически проверяет и восстанавливает узлы
|
||||
func (rm *RecoveryManager) recoveryLoop() {
|
||||
defer rm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rm.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
rm.attemptRecoveryAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attemptRecoveryAll пытается восстановить все проблемные узлы
|
||||
func (rm *RecoveryManager) attemptRecoveryAll() {
|
||||
rm.states.Range(func(key, value interface{}) bool {
|
||||
state := value.(*NodeState)
|
||||
if state.FailureCount >= rm.maxFailures && !state.IsRecovering {
|
||||
go rm.recoverNode(state.NodeID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// GetNodeState возвращает состояние узла
|
||||
func (rm *RecoveryManager) GetNodeState(nodeID string) *NodeState {
|
||||
if val, ok := rm.states.Load(nodeID); ok {
|
||||
return val.(*NodeState)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllStates возвращает все состояния узлов
|
||||
func (rm *RecoveryManager) GetAllStates() map[string]*NodeState {
|
||||
states := make(map[string]*NodeState)
|
||||
rm.states.Range(func(key, value interface{}) bool {
|
||||
states[key.(string)] = value.(*NodeState)
|
||||
return true
|
||||
})
|
||||
return states
|
||||
}
|
||||
301
internal/cluster/types.go
Normal file
301
internal/cluster/types.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/types.go
|
||||
// Назначение: Общие типы данных для кластерных операций с поддержкой временных меток
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NodeInfo представляет информацию об узле для координатора
|
||||
type NodeInfo struct {
|
||||
ID string `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
Status string `json:"status"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
JoinedAt int64 `json:"joined_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
Version uint64 `json:"version"`
|
||||
}
|
||||
|
||||
// ClusterStatus представляет статус кластера
|
||||
type ClusterStatus struct {
|
||||
Name string `json:"name"`
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
ActiveNodes int `json:"active_nodes"`
|
||||
SyncingNodes int `json:"syncing_nodes"`
|
||||
FailedNodes int `json:"failed_nodes"`
|
||||
ReplicationFactor int `json:"replication_factor"`
|
||||
LeaderID string `json:"leader_id"`
|
||||
Health string `json:"health"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
PipelineEnabled bool `json:"pipeline_enabled"`
|
||||
BatchCommitEnabled bool `json:"batch_commit_enabled"`
|
||||
ReshardingEnabled bool `json:"resharding_enabled"`
|
||||
JointConsensusActive bool `json:"joint_consensus_active"`
|
||||
}
|
||||
|
||||
// ClusterHealth представляет информацию о здоровье кластера
|
||||
type ClusterHealth struct {
|
||||
Nodes map[string]*NodeHealth `json:"nodes"`
|
||||
OverallScore float64 `json:"overall_score"`
|
||||
Recommendations string `json:"recommendations"`
|
||||
CheckedAt int64 `json:"checked_at"`
|
||||
}
|
||||
|
||||
// NodeHealth представляет здоровье отдельного узла
|
||||
type NodeHealth struct {
|
||||
Status string `json:"status"`
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
LastCheck int64 `json:"last_check"`
|
||||
LastSuccess int64 `json:"last_success"`
|
||||
LastFailure int64 `json:"last_failure"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
}
|
||||
|
||||
// NodeRequest представляет запрос от одного узла к другому
|
||||
type NodeRequest struct {
|
||||
Type string `json:"type"`
|
||||
Data []byte `json:"data"`
|
||||
FromNode string `json:"from_node"`
|
||||
RequestID string `json:"request_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ShardInfo представляет информацию о шарде
|
||||
type ShardInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Nodes []string `json:"nodes"`
|
||||
LeaderNode string `json:"leader_node"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
LastRebalanced int64 `json:"last_rebalanced"`
|
||||
DocumentCount int64 `json:"document_count"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// ReplicationLogEntry представляет запись в журнале репликации
|
||||
type ReplicationLogEntry struct {
|
||||
ID string `json:"id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
SourceNode string `json:"source_node"`
|
||||
TargetNode string `json:"target_node"`
|
||||
Operation string `json:"operation"`
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
DocumentID string `json:"document_id"`
|
||||
Status string `json:"status"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// ========== СТРУКТУРЫ ДЛЯ PRODUCTION-READY РЕАЛИЗАЦИИ ==========
|
||||
|
||||
// ConnectionInfo содержит информацию о соединении с узлом
|
||||
type ConnectionInfo struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Address string `json:"address"`
|
||||
IsHealthy bool `json:"is_healthy"`
|
||||
LastUsed int64 `json:"last_used"`
|
||||
Failures uint32 `json:"failures"`
|
||||
ConnectedAt int64 `json:"connected_at"`
|
||||
}
|
||||
|
||||
// ClusterOperation представляет операцию в кластере
|
||||
type ClusterOperation struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "add_node", "remove_node", "rebalance", "reshard"
|
||||
Status string `json:"status"` // "pending", "in_progress", "completed", "failed"
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
StartedAt int64 `json:"started_at"`
|
||||
CompletedAt int64 `json:"completed_at"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// WorkerPoolStats содержит статистику пула воркеров
|
||||
type WorkerPoolStats struct {
|
||||
MaxWorkers int `json:"max_workers"`
|
||||
ActiveTasks int32 `json:"active_tasks"`
|
||||
TotalTasks uint64 `json:"total_tasks"`
|
||||
FailedTasks uint64 `json:"failed_tasks"`
|
||||
QueueSize int `json:"queue_size"`
|
||||
QueueCapacity int `json:"queue_capacity"`
|
||||
}
|
||||
|
||||
// ========== МЕТОДЫ ДЛЯ NodeInfo ==========
|
||||
|
||||
// NodeJoinedAt возвращает человекочитаемое время присоединения узла
|
||||
func (n *NodeInfo) NodeJoinedAt() string {
|
||||
if n.JoinedAt == 0 {
|
||||
return "not joined"
|
||||
}
|
||||
return time.UnixMilli(n.JoinedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// LastSeenAt возвращает человекочитаемое время последнего контакта
|
||||
func (n *NodeInfo) LastSeenAt() string {
|
||||
if n.LastSeen == 0 {
|
||||
return "never"
|
||||
}
|
||||
return time.UnixMilli(n.LastSeen).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetUptime возвращает время жизни узла в кластере
|
||||
func (n *NodeInfo) GetUptime() time.Duration {
|
||||
if n.JoinedAt == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(time.Now().UnixMilli()-n.JoinedAt) * time.Millisecond
|
||||
}
|
||||
|
||||
// IsHealthy проверяет, здоров ли узел
|
||||
func (n *NodeInfo) IsHealthy() bool {
|
||||
return n.Status == "active" && time.Now().UnixMilli()-n.LastSeen < 30000
|
||||
}
|
||||
|
||||
// GetAddress возвращает адрес узла
|
||||
func (n *NodeInfo) GetAddress() string {
|
||||
return fmt.Sprintf("%s:%d", n.IP, n.Port)
|
||||
}
|
||||
|
||||
// ========== МЕТОДЫ ДЛЯ ShardInfo ==========
|
||||
|
||||
// GetUptime возвращает время жизни шарда
|
||||
func (s *ShardInfo) GetUptime() time.Duration {
|
||||
if s.CreatedAt == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(time.Now().UnixMilli()-s.CreatedAt) * time.Millisecond
|
||||
}
|
||||
|
||||
// IsHealthy проверяет, здоров ли шард
|
||||
func (s *ShardInfo) IsHealthy() bool {
|
||||
return s.Status == "active"
|
||||
}
|
||||
|
||||
// GetLastRebalancedStr возвращает человекочитаемое время последней ребалансировки
|
||||
func (s *ShardInfo) GetLastRebalancedStr() string {
|
||||
if s.LastRebalanced == 0 {
|
||||
return "never"
|
||||
}
|
||||
return time.UnixMilli(s.LastRebalanced).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetCreatedAtStr возвращает человекочитаемое время создания
|
||||
func (s *ShardInfo) GetCreatedAtStr() string {
|
||||
if s.CreatedAt == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
return time.UnixMilli(s.CreatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetUpdatedAtStr возвращает человекочитаемое время обновления
|
||||
func (s *ShardInfo) GetUpdatedAtStr() string {
|
||||
if s.UpdatedAt == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
return time.UnixMilli(s.UpdatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetLeaderNode возвращает лидера шарда
|
||||
func (s *ShardInfo) GetLeaderNode() string {
|
||||
return s.LeaderNode
|
||||
}
|
||||
|
||||
// GetNodeCount возвращает количество узлов в шарде
|
||||
func (s *ShardInfo) GetNodeCount() int {
|
||||
return len(s.Nodes)
|
||||
}
|
||||
|
||||
// GetReplicationFactor возвращает фактор репликации шарда
|
||||
func (s *ShardInfo) GetReplicationFactor() int {
|
||||
return len(s.Nodes)
|
||||
}
|
||||
|
||||
// ========== МЕТОДЫ ДЛЯ ReplicationLogEntry ==========
|
||||
|
||||
// GetTimestampStr возвращает человекочитаемое время записи репликации
|
||||
func (r *ReplicationLogEntry) GetTimestampStr() string {
|
||||
if r.Timestamp == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
return time.UnixMilli(r.Timestamp).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// IsSuccess проверяет, успешна ли операция репликации
|
||||
func (r *ReplicationLogEntry) IsSuccess() bool {
|
||||
return r.Status == "success"
|
||||
}
|
||||
|
||||
// GetDuration возвращает длительность операции
|
||||
func (r *ReplicationLogEntry) GetDuration() time.Duration {
|
||||
return time.Duration(r.DurationMs) * time.Millisecond
|
||||
}
|
||||
|
||||
// ========== МЕТОДЫ ДЛЯ ClusterHealth ==========
|
||||
|
||||
// IsHealthy проверяет, здоров ли кластер в целом
|
||||
func (ch *ClusterHealth) IsHealthy() bool {
|
||||
return ch.OverallScore >= 80
|
||||
}
|
||||
|
||||
// IsDegraded проверяет, деградирован ли кластер
|
||||
func (ch *ClusterHealth) IsDegraded() bool {
|
||||
return ch.OverallScore >= 50 && ch.OverallScore < 80
|
||||
}
|
||||
|
||||
// IsCritical проверяет, находится ли кластер в критическом состоянии
|
||||
func (ch *ClusterHealth) IsCritical() bool {
|
||||
return ch.OverallScore < 50
|
||||
}
|
||||
|
||||
// GetHealthyNodes возвращает количество здоровых узлов
|
||||
func (ch *ClusterHealth) GetHealthyNodes() int {
|
||||
count := 0
|
||||
for _, node := range ch.Nodes {
|
||||
if node.Status == "active" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
|
||||
|
||||
// GetTimestamp возвращает текущий timestamp в миллисекундах
|
||||
func GetTimestamp() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// FormatTimestamp форматирует timestamp в человекочитаемый вид
|
||||
func FormatTimestamp(ts int64) string {
|
||||
if ts == 0 {
|
||||
return "never"
|
||||
}
|
||||
return time.UnixMilli(ts).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// IsTimestampExpired проверяет, истёк ли timestamp
|
||||
func IsTimestampExpired(ts int64, ttl time.Duration) bool {
|
||||
if ts == 0 {
|
||||
return true
|
||||
}
|
||||
return time.Now().UnixMilli()-ts > ttl.Milliseconds()
|
||||
}
|
||||
216
internal/cluster/worker_pool.go
Normal file
216
internal/cluster/worker_pool.go
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* 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
|
||||
// Назначение: Пул воркеров для ограничения количества горутин
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggerInterface определяет интерфейс для логирования
|
||||
type LoggerInterface interface {
|
||||
Debug(msg string)
|
||||
Info(msg string)
|
||||
Error(msg string)
|
||||
Warn(msg string)
|
||||
}
|
||||
|
||||
// WorkerTask представляет задачу для выполнения в пуле
|
||||
type WorkerTask struct {
|
||||
ID string
|
||||
Handler func() error
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
// WorkerPool управляет пулом воркеров
|
||||
type WorkerPool struct {
|
||||
maxWorkers int
|
||||
taskQueue chan *WorkerTask
|
||||
wg sync.WaitGroup
|
||||
stopChan chan struct{}
|
||||
activeTasks atomic.Int32
|
||||
totalTasks atomic.Uint64
|
||||
failedTasks atomic.Uint64
|
||||
logger LoggerInterface
|
||||
panicHandler func(interface{})
|
||||
}
|
||||
|
||||
// NewWorkerPool создаёт новый пул воркеров
|
||||
func NewWorkerPool(maxWorkers int, logger LoggerInterface) *WorkerPool {
|
||||
if maxWorkers <= 0 {
|
||||
maxWorkers = 100 // ограничение по умолчанию
|
||||
}
|
||||
|
||||
pool := &WorkerPool{
|
||||
maxWorkers: maxWorkers,
|
||||
taskQueue: make(chan *WorkerTask, maxWorkers*10),
|
||||
stopChan: make(chan struct{}),
|
||||
logger: logger,
|
||||
panicHandler: defaultPanicHandler,
|
||||
}
|
||||
|
||||
for i := 0; i < maxWorkers; i++ {
|
||||
pool.wg.Add(1)
|
||||
go pool.worker(i)
|
||||
}
|
||||
|
||||
return pool
|
||||
}
|
||||
|
||||
// defaultPanicHandler обрабатывает панику в горутине
|
||||
func defaultPanicHandler(r interface{}) {
|
||||
fmt.Printf("PANIC recovered: %v\n%s\n", r, debug.Stack())
|
||||
}
|
||||
|
||||
// SetPanicHandler устанавливает обработчик паник
|
||||
func (wp *WorkerPool) SetPanicHandler(handler func(interface{})) {
|
||||
if handler != nil {
|
||||
wp.panicHandler = handler
|
||||
}
|
||||
}
|
||||
|
||||
// worker запускает воркер
|
||||
func (wp *WorkerPool) worker(id int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if wp.panicHandler != nil {
|
||||
wp.panicHandler(r)
|
||||
}
|
||||
if wp.logger != nil {
|
||||
wp.logger.Error(fmt.Sprintf("Worker %d panicked: %v", id, r))
|
||||
}
|
||||
// Перезапускаем воркер
|
||||
wp.wg.Add(1)
|
||||
go wp.worker(id)
|
||||
}
|
||||
wp.wg.Done()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-wp.stopChan:
|
||||
return
|
||||
case task, ok := <-wp.taskQueue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
wp.activeTasks.Add(1)
|
||||
wp.totalTasks.Add(1)
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
wp.failedTasks.Add(1)
|
||||
if wp.panicHandler != nil {
|
||||
wp.panicHandler(r)
|
||||
}
|
||||
if wp.logger != nil {
|
||||
wp.logger.Error(fmt.Sprintf("Task %s panicked: %v", task.ID, r))
|
||||
}
|
||||
}
|
||||
wp.activeTasks.Add(-1)
|
||||
}()
|
||||
|
||||
if err := task.Handler(); err != nil {
|
||||
wp.failedTasks.Add(1)
|
||||
if wp.logger != nil {
|
||||
wp.logger.Error(fmt.Sprintf("Task %s failed: %v", task.ID, err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit отправляет задачу в пул
|
||||
func (wp *WorkerPool) Submit(task *WorkerTask) error {
|
||||
select {
|
||||
case wp.taskQueue <- task:
|
||||
return nil
|
||||
case <-time.After(5 * time.Second):
|
||||
return fmt.Errorf("task queue full, task rejected: %s", task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitFunc отправляет функцию как задачу
|
||||
func (wp *WorkerPool) SubmitFunc(id string, handler func() error) error {
|
||||
return wp.Submit(&WorkerTask{
|
||||
ID: id,
|
||||
Handler: handler,
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats возвращает статистику пула
|
||||
func (wp *WorkerPool) GetStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"max_workers": wp.maxWorkers,
|
||||
"active_tasks": wp.activeTasks.Load(),
|
||||
"total_tasks": wp.totalTasks.Load(),
|
||||
"failed_tasks": wp.failedTasks.Load(),
|
||||
"queue_size": len(wp.taskQueue),
|
||||
"queue_capacity": cap(wp.taskQueue),
|
||||
}
|
||||
}
|
||||
|
||||
// GetWorkerPoolStats возвращает статистику в структурированном виде
|
||||
func (wp *WorkerPool) GetWorkerPoolStats() *WorkerPoolStats {
|
||||
return &WorkerPoolStats{
|
||||
MaxWorkers: wp.maxWorkers,
|
||||
ActiveTasks: wp.activeTasks.Load(),
|
||||
TotalTasks: wp.totalTasks.Load(),
|
||||
FailedTasks: wp.failedTasks.Load(),
|
||||
QueueSize: len(wp.taskQueue),
|
||||
QueueCapacity: cap(wp.taskQueue),
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает пул воркеров
|
||||
func (wp *WorkerPool) Stop() {
|
||||
close(wp.stopChan)
|
||||
close(wp.taskQueue)
|
||||
wp.wg.Wait()
|
||||
}
|
||||
|
||||
// SafeGo безопасно запускает горутину с обработкой паники
|
||||
func SafeGo(fn func(), panicHandler func(interface{})) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if panicHandler != nil {
|
||||
panicHandler(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
// SafeGoWithLogger безопасно запускает горутину с логгером
|
||||
func SafeGoWithLogger(fn func(), logger LoggerInterface, name string) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if logger != nil {
|
||||
logger.Error(fmt.Sprintf("Goroutine %s panicked: %v\n%s", name, r, debug.Stack()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
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)
|
||||
}
|
||||
347
internal/commands/crud.go
Normal file
347
internal/commands/crud.go
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* 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() {
|
||||
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))
|
||||
}
|
||||
934
internal/config/config.go
Normal file
934
internal/config/config.go
Normal file
@@ -0,0 +1,934 @@
|
||||
/*
|
||||
* 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/config/config.go
|
||||
// Назначение: Загрузка и парсинг TOML-конфигурации, валидация параметров,
|
||||
// предоставление доступа к настройкам кластера, хранилища и REPL.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Cluster ClusterConfig `toml:"cluster"`
|
||||
Storage StorageConfig `toml:"storage"`
|
||||
Repl ReplConfig `toml:"repl"`
|
||||
Log LogConfig `toml:"log"`
|
||||
API APIConfig `toml:"api"`
|
||||
Replication ReplicationConfig `toml:"replication"`
|
||||
Plugins PluginsConfig `toml:"plugins"`
|
||||
Compression CompressionConfig `toml:"compression"`
|
||||
WebUI WebUIConfig `toml:"webui"`
|
||||
Performance PerformanceConfig `toml:"performance"`
|
||||
Security SecurityConfig `toml:"security"`
|
||||
Monitoring MonitoringConfig `toml:"monitoring"`
|
||||
Recovery RecoveryConfig `toml:"recovery"`
|
||||
WAL WALConfig `toml:"wal"`
|
||||
MVCC MVCCConfig `toml:"mvcc"`
|
||||
Transactions TransactionsConfig `toml:"transactions"`
|
||||
ACL ACLConfig `toml:"acl"`
|
||||
}
|
||||
|
||||
type ClusterConfig struct {
|
||||
Name string `toml:"name"`
|
||||
NodeIP string `toml:"node_ip"`
|
||||
NodePort int `toml:"node_port"`
|
||||
RaftPort int `toml:"raft_port"`
|
||||
RaftDataDir string `toml:"raft_data_dir"`
|
||||
Bootstrap bool `toml:"bootstrap"`
|
||||
Nodes []string `toml:"nodes"`
|
||||
|
||||
HeartbeatTimeoutMs int `toml:"heartbeat_timeout_ms"`
|
||||
ElectionTimeoutMs int `toml:"election_timeout_ms"`
|
||||
CommitTimeoutMs int `toml:"commit_timeout_ms"`
|
||||
|
||||
SnapshotIntervalMin int `toml:"snapshot_interval_min"`
|
||||
SnapshotThreshold int `toml:"snapshot_threshold"`
|
||||
|
||||
SplitBrainPrevention bool `toml:"split_brain_prevention"`
|
||||
RecoveryTimeoutSec int `toml:"recovery_timeout_sec"`
|
||||
|
||||
Region string `toml:"region"`
|
||||
PriorityZone int `toml:"priority_zone"`
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
PageSizeMB int `toml:"page_size_mb"`
|
||||
MaxCollections int `toml:"max_collections"`
|
||||
MaxDocumentsPerCollection int `toml:"max_documents_per_collection"`
|
||||
}
|
||||
|
||||
type ReplConfig struct {
|
||||
PromptColor string `toml:"prompt_color"`
|
||||
HistorySize int `toml:"history_size"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
LogFile string `toml:"log_file"`
|
||||
LogLevel string `toml:"log_level"`
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
Port int `toml:"port"`
|
||||
}
|
||||
|
||||
type ReplicationConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MasterMaster bool `toml:"master_master"`
|
||||
SyncReplication bool `toml:"sync_replication"`
|
||||
ReplicationTimeoutMs int `toml:"replication_timeout_ms"`
|
||||
}
|
||||
|
||||
type PluginsConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
ScriptDir string `toml:"script_dir"`
|
||||
AllowList []string `toml:"allow_list"`
|
||||
MaxCPUTimeMs int `toml:"max_cpu_time_ms"`
|
||||
MaxMemoryMB int `toml:"max_memory_mb"`
|
||||
MaxExecutionTimeSec int `toml:"max_execution_time_sec"`
|
||||
MaxInstructions int64 `toml:"max_instructions"`
|
||||
HotReloadIntervalSec int `toml:"hot_reload_interval_sec"`
|
||||
MaxEventLogSize int `toml:"max_event_log_size"`
|
||||
LoadTimeoutSec int `toml:"load_timeout_sec"`
|
||||
}
|
||||
|
||||
type CompressionConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Algorithm string `toml:"algorithm"`
|
||||
Level int `toml:"level"`
|
||||
MinSize int `toml:"min_size"`
|
||||
}
|
||||
|
||||
type WebUIConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Port int `toml:"port"`
|
||||
Theme string `toml:"theme"`
|
||||
}
|
||||
|
||||
type PerformanceConfig struct {
|
||||
EnablePipeline bool `toml:"enable_pipeline"`
|
||||
BatchSize int `toml:"batch_size"`
|
||||
ReadFromFollower bool `toml:"read_from_follower"`
|
||||
MaxConnections int `toml:"max_connections"`
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
EnableTLS bool `toml:"enable_tls"`
|
||||
CertFile string `toml:"cert_file"`
|
||||
KeyFile string `toml:"key_file"`
|
||||
CAFile string `toml:"ca_file"`
|
||||
MinVersion string `toml:"min_version"`
|
||||
}
|
||||
|
||||
type MonitoringConfig struct {
|
||||
EnableMetrics bool `toml:"enable_metrics"`
|
||||
MetricsPort int `toml:"metrics_port"`
|
||||
EnableTracing bool `toml:"enable_tracing"`
|
||||
TraceSampleRate float64 `toml:"trace_sample_rate"`
|
||||
}
|
||||
|
||||
type RecoveryConfig struct {
|
||||
AutoRejoin bool `toml:"auto_rejoin"`
|
||||
MaxRetrySec int `toml:"max_retry_sec"`
|
||||
DataReplicationTimeoutSec int `toml:"data_replication_timeout_sec"`
|
||||
StaleReadTimeoutSec int `toml:"stale_read_timeout_sec"`
|
||||
}
|
||||
|
||||
// ========== НОВЫЕ КОНФИГУРАЦИОННЫЕ СТРУКТУРЫ ==========
|
||||
|
||||
// WALConfig настройки Write-Ahead Log
|
||||
type WALConfig struct {
|
||||
SegmentSizeMB int `toml:"segment_size_mb"`
|
||||
SyncIntervalSec int `toml:"sync_interval_sec"`
|
||||
BatchSize int `toml:"batch_size"`
|
||||
RecoveryWorkers int `toml:"recovery_workers"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
}
|
||||
|
||||
// MVCCConfig настройки Multi-Version Concurrency Control
|
||||
type MVCCConfig struct {
|
||||
MaxVersionsPerDoc int `toml:"max_versions_per_doc"`
|
||||
VisibilityMapSize int `toml:"visibility_map_size"`
|
||||
PruneIntervalMin int `toml:"prune_interval_min"`
|
||||
RetentionDays int `toml:"retention_days"`
|
||||
ReadCacheSize int `toml:"read_cache_size"`
|
||||
ReadCacheTTLSec int `toml:"read_cache_ttl_sec"`
|
||||
}
|
||||
|
||||
// TransactionsConfig настройки транзакций
|
||||
type TransactionsConfig struct {
|
||||
DefaultTimeoutSec int `toml:"default_timeout_sec"`
|
||||
DeadlockCheckIntervalSec int `toml:"deadlock_check_interval_sec"`
|
||||
MaxSavepointsPerTx int `toml:"max_savepoints_per_tx"`
|
||||
TwoPhaseCommitTimeoutSec int `toml:"two_phase_commit_timeout_sec"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
CheckpointIntervalSec int `toml:"checkpoint_interval_sec"`
|
||||
}
|
||||
|
||||
// ACLConfig настройки Access Control List
|
||||
type ACLConfig struct {
|
||||
MaxDeniedLogSize int `toml:"max_denied_log_size"`
|
||||
TemporaryGrantTTLHours int `toml:"temporary_grant_ttl_hours"`
|
||||
EnableRoleHierarchy bool `toml:"enable_role_hierarchy"`
|
||||
CacheTTLSec int `toml:"cache_ttl_sec"`
|
||||
}
|
||||
|
||||
// ========== Методы для новых конфигураций ==========
|
||||
|
||||
// GetSegmentSize возвращает размер сегмента WAL в байтах
|
||||
func (w *WALConfig) GetSegmentSize() int64 {
|
||||
if w.SegmentSizeMB <= 0 {
|
||||
return 64 * 1024 * 1024
|
||||
}
|
||||
return int64(w.SegmentSizeMB) * 1024 * 1024
|
||||
}
|
||||
|
||||
// GetSyncInterval возвращает интервал синхронизации WAL
|
||||
func (w *WALConfig) GetSyncInterval() time.Duration {
|
||||
if w.SyncIntervalSec <= 0 {
|
||||
return 5 * time.Second
|
||||
}
|
||||
return time.Duration(w.SyncIntervalSec) * time.Second
|
||||
}
|
||||
|
||||
// GetBatchSize возвращает размер пакета WAL
|
||||
func (w *WALConfig) GetBatchSize() int {
|
||||
if w.BatchSize <= 0 {
|
||||
return 100
|
||||
}
|
||||
return w.BatchSize
|
||||
}
|
||||
|
||||
// GetRecoveryWorkers возвращает количество воркеров для parallel recovery
|
||||
func (w *WALConfig) GetRecoveryWorkers() int {
|
||||
if w.RecoveryWorkers <= 0 {
|
||||
return 4
|
||||
}
|
||||
return w.RecoveryWorkers
|
||||
}
|
||||
|
||||
// IsWALEnabled возвращает статус WAL
|
||||
func (w *WALConfig) IsWALEnabled() bool {
|
||||
return w.Enabled
|
||||
}
|
||||
|
||||
// GetMaxVersionsPerDoc возвращает максимальное количество версий на документ
|
||||
func (m *MVCCConfig) GetMaxVersionsPerDoc() int {
|
||||
if m.MaxVersionsPerDoc <= 0 {
|
||||
return 10
|
||||
}
|
||||
return m.MaxVersionsPerDoc
|
||||
}
|
||||
|
||||
// GetVisibilityMapSize возвращает размер карты видимости
|
||||
func (m *MVCCConfig) GetVisibilityMapSize() int {
|
||||
if m.VisibilityMapSize <= 0 {
|
||||
return 1024 * 1024
|
||||
}
|
||||
return m.VisibilityMapSize
|
||||
}
|
||||
|
||||
// GetPruneInterval возвращает интервал очистки версий
|
||||
func (m *MVCCConfig) GetPruneInterval() time.Duration {
|
||||
if m.PruneIntervalMin <= 0 {
|
||||
return 5 * time.Minute
|
||||
}
|
||||
return time.Duration(m.PruneIntervalMin) * time.Minute
|
||||
}
|
||||
|
||||
// GetRetentionDays возвращает дни хранения версий
|
||||
func (m *MVCCConfig) GetRetentionDays() int {
|
||||
if m.RetentionDays <= 0 {
|
||||
return 7
|
||||
}
|
||||
return m.RetentionDays
|
||||
}
|
||||
|
||||
// GetReadCacheSize возвращает размер кэша чтения
|
||||
func (m *MVCCConfig) GetReadCacheSize() int {
|
||||
if m.ReadCacheSize <= 0 {
|
||||
return 10000
|
||||
}
|
||||
return m.ReadCacheSize
|
||||
}
|
||||
|
||||
// GetReadCacheTTL возвращает TTL кэша чтения
|
||||
func (m *MVCCConfig) GetReadCacheTTL() time.Duration {
|
||||
if m.ReadCacheTTLSec <= 0 {
|
||||
return 300 * time.Second
|
||||
}
|
||||
return time.Duration(m.ReadCacheTTLSec) * time.Second
|
||||
}
|
||||
|
||||
// GetDefaultTimeout возвращает таймаут транзакции по умолчанию
|
||||
func (t *TransactionsConfig) GetDefaultTimeout() time.Duration {
|
||||
if t.DefaultTimeoutSec <= 0 {
|
||||
return 30 * time.Second
|
||||
}
|
||||
return time.Duration(t.DefaultTimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
// GetDeadlockCheckInterval возвращает интервал проверки дедлоков
|
||||
func (t *TransactionsConfig) GetDeadlockCheckInterval() time.Duration {
|
||||
if t.DeadlockCheckIntervalSec <= 0 {
|
||||
return 1 * time.Second
|
||||
}
|
||||
return time.Duration(t.DeadlockCheckIntervalSec) * time.Second
|
||||
}
|
||||
|
||||
// GetMaxSavepointsPerTx возвращает максимальное количество точек сохранения
|
||||
func (t *TransactionsConfig) GetMaxSavepointsPerTx() int {
|
||||
if t.MaxSavepointsPerTx <= 0 {
|
||||
return 100
|
||||
}
|
||||
return t.MaxSavepointsPerTx
|
||||
}
|
||||
|
||||
// GetTwoPhaseCommitTimeout возвращает таймаут двухфазного коммита
|
||||
func (t *TransactionsConfig) GetTwoPhaseCommitTimeout() time.Duration {
|
||||
if t.TwoPhaseCommitTimeoutSec <= 0 {
|
||||
return 10 * time.Second
|
||||
}
|
||||
return time.Duration(t.TwoPhaseCommitTimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
// IsTransactionsEnabled возвращает статус поддержки транзакций
|
||||
func (t *TransactionsConfig) IsTransactionsEnabled() bool {
|
||||
return t.Enabled
|
||||
}
|
||||
|
||||
// GetCheckpointInterval возвращает интервал чекпоинтов
|
||||
func (t *TransactionsConfig) GetCheckpointInterval() int64 {
|
||||
if t.CheckpointIntervalSec <= 0 {
|
||||
return 300
|
||||
}
|
||||
return int64(t.CheckpointIntervalSec)
|
||||
}
|
||||
|
||||
// GetMaxDeniedLogSize возвращает максимальный размер лога отказов
|
||||
func (a *ACLConfig) GetMaxDeniedLogSize() int {
|
||||
if a.MaxDeniedLogSize <= 0 {
|
||||
return 10000
|
||||
}
|
||||
return a.MaxDeniedLogSize
|
||||
}
|
||||
|
||||
// GetTemporaryGrantTTL возвращает TTL временных разрешений
|
||||
func (a *ACLConfig) GetTemporaryGrantTTL() time.Duration {
|
||||
if a.TemporaryGrantTTLHours <= 0 {
|
||||
return 24 * time.Hour
|
||||
}
|
||||
return time.Duration(a.TemporaryGrantTTLHours) * time.Hour
|
||||
}
|
||||
|
||||
// IsRoleHierarchyEnabled возвращает статус иерархии ролей
|
||||
func (a *ACLConfig) IsRoleHierarchyEnabled() bool {
|
||||
return a.EnableRoleHierarchy
|
||||
}
|
||||
|
||||
// GetCacheTTL возвращает TTL кэша ACL
|
||||
func (a *ACLConfig) GetCacheTTL() time.Duration {
|
||||
if a.CacheTTLSec <= 0 {
|
||||
return 60 * time.Second
|
||||
}
|
||||
return time.Duration(a.CacheTTLSec) * time.Second
|
||||
}
|
||||
|
||||
// ========== Методы PluginsConfig (расширенные) ==========
|
||||
|
||||
// GetMaxCPUTime возвращает лимит CPU для плагина
|
||||
func (p *PluginsConfig) GetMaxCPUTime() time.Duration {
|
||||
if p.MaxCPUTimeMs <= 0 {
|
||||
return 100 * time.Millisecond
|
||||
}
|
||||
return time.Duration(p.MaxCPUTimeMs) * time.Millisecond
|
||||
}
|
||||
|
||||
// GetMaxMemory возвращает лимит памяти для плагина в байтах
|
||||
func (p *PluginsConfig) GetMaxMemory() int64 {
|
||||
if p.MaxMemoryMB <= 0 {
|
||||
return 50 * 1024 * 1024
|
||||
}
|
||||
return int64(p.MaxMemoryMB) * 1024 * 1024
|
||||
}
|
||||
|
||||
// GetMaxExecutionTime возвращает лимит времени выполнения плагина
|
||||
func (p *PluginsConfig) GetMaxExecutionTime() time.Duration {
|
||||
if p.MaxExecutionTimeSec <= 0 {
|
||||
return 5 * time.Second
|
||||
}
|
||||
return time.Duration(p.MaxExecutionTimeSec) * time.Second
|
||||
}
|
||||
|
||||
// GetMaxInstructions возвращает лимит инструкций Lua
|
||||
func (p *PluginsConfig) GetMaxInstructions() int64 {
|
||||
if p.MaxInstructions <= 0 {
|
||||
return 1000000
|
||||
}
|
||||
return p.MaxInstructions
|
||||
}
|
||||
|
||||
// GetHotReloadInterval возвращает интервал проверки изменений плагинов
|
||||
func (p *PluginsConfig) GetHotReloadInterval() time.Duration {
|
||||
if p.HotReloadIntervalSec <= 0 {
|
||||
return 30 * time.Second
|
||||
}
|
||||
return time.Duration(p.HotReloadIntervalSec) * time.Second
|
||||
}
|
||||
|
||||
// GetMaxEventLogSize возвращает максимальный размер лога событий плагина
|
||||
func (p *PluginsConfig) GetMaxEventLogSize() int {
|
||||
if p.MaxEventLogSize <= 0 {
|
||||
return 1000
|
||||
}
|
||||
return p.MaxEventLogSize
|
||||
}
|
||||
|
||||
// GetLoadTimeout возвращает таймаут загрузки плагина
|
||||
func (p *PluginsConfig) GetLoadTimeout() time.Duration {
|
||||
if p.LoadTimeoutSec <= 0 {
|
||||
return 10 * time.Second
|
||||
}
|
||||
return time.Duration(p.LoadTimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
// ========== Остальные методы без изменений ==========
|
||||
|
||||
func (c *ClusterConfig) GetHeartbeatTimeout() time.Duration {
|
||||
if c.HeartbeatTimeoutMs <= 0 {
|
||||
return 1000 * time.Millisecond
|
||||
}
|
||||
return time.Duration(c.HeartbeatTimeoutMs) * time.Millisecond
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetElectionTimeout() time.Duration {
|
||||
if c.ElectionTimeoutMs <= 0 {
|
||||
return 1000 * time.Millisecond
|
||||
}
|
||||
return time.Duration(c.ElectionTimeoutMs) * time.Millisecond
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetCommitTimeout() time.Duration {
|
||||
if c.CommitTimeoutMs <= 0 {
|
||||
return 500 * time.Millisecond
|
||||
}
|
||||
return time.Duration(c.CommitTimeoutMs) * time.Millisecond
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetSnapshotInterval() time.Duration {
|
||||
if c.SnapshotIntervalMin <= 0 {
|
||||
return 30 * time.Minute
|
||||
}
|
||||
return time.Duration(c.SnapshotIntervalMin) * time.Minute
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetSnapshotThreshold() uint64 {
|
||||
if c.SnapshotThreshold <= 0 {
|
||||
return 1000
|
||||
}
|
||||
return uint64(c.SnapshotThreshold)
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) IsSplitBrainPreventionEnabled() bool {
|
||||
return c.SplitBrainPrevention
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetRecoveryTimeout() time.Duration {
|
||||
if c.RecoveryTimeoutSec <= 0 {
|
||||
return 30 * time.Second
|
||||
}
|
||||
return time.Duration(c.RecoveryTimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetRegion() string {
|
||||
if c.Region == "" {
|
||||
return "default"
|
||||
}
|
||||
return c.Region
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetPriorityZone() int {
|
||||
if c.PriorityZone < 0 {
|
||||
return 0
|
||||
}
|
||||
if c.PriorityZone > 9 {
|
||||
return 9
|
||||
}
|
||||
return c.PriorityZone
|
||||
}
|
||||
|
||||
func (r *ReplicationConfig) GetReplicationTimeout() time.Duration {
|
||||
if r.ReplicationTimeoutMs <= 0 {
|
||||
return 5 * time.Second
|
||||
}
|
||||
return time.Duration(r.ReplicationTimeoutMs) * time.Millisecond
|
||||
}
|
||||
|
||||
func (r *ReplicationConfig) IsReplicationEnabled() bool {
|
||||
return r.Enabled
|
||||
}
|
||||
|
||||
func (r *ReplicationConfig) IsMasterMasterEnabled() bool {
|
||||
return r.MasterMaster
|
||||
}
|
||||
|
||||
func (r *ReplicationConfig) IsSyncReplicationEnabled() bool {
|
||||
return r.SyncReplication
|
||||
}
|
||||
|
||||
func (r *RecoveryConfig) GetMaxRetryDuration() time.Duration {
|
||||
if r.MaxRetrySec <= 0 {
|
||||
return 300 * time.Second
|
||||
}
|
||||
return time.Duration(r.MaxRetrySec) * time.Second
|
||||
}
|
||||
|
||||
func (r *RecoveryConfig) GetDataReplicationTimeout() time.Duration {
|
||||
if r.DataReplicationTimeoutSec <= 0 {
|
||||
return 60 * time.Second
|
||||
}
|
||||
return time.Duration(r.DataReplicationTimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
func (r *RecoveryConfig) GetStaleReadTimeout() time.Duration {
|
||||
if r.StaleReadTimeoutSec <= 0 {
|
||||
return 30 * time.Second
|
||||
}
|
||||
return time.Duration(r.StaleReadTimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
func (r *RecoveryConfig) IsAutoRejoinEnabled() bool {
|
||||
return r.AutoRejoin
|
||||
}
|
||||
|
||||
func (p *PerformanceConfig) GetBatchSize() int {
|
||||
if p.BatchSize <= 0 {
|
||||
return 100
|
||||
}
|
||||
return p.BatchSize
|
||||
}
|
||||
|
||||
func (p *PerformanceConfig) GetMaxConnections() int {
|
||||
if p.MaxConnections <= 0 {
|
||||
return 1000
|
||||
}
|
||||
return p.MaxConnections
|
||||
}
|
||||
|
||||
func (p *PerformanceConfig) IsReadFromFollowerEnabled() bool {
|
||||
return p.ReadFromFollower
|
||||
}
|
||||
|
||||
func (p *PerformanceConfig) IsPipelineEnabled() bool {
|
||||
return p.EnablePipeline
|
||||
}
|
||||
|
||||
func (m *MonitoringConfig) GetMetricsPort() int {
|
||||
if m.MetricsPort <= 0 {
|
||||
return 9090
|
||||
}
|
||||
return m.MetricsPort
|
||||
}
|
||||
|
||||
func (m *MonitoringConfig) GetTraceSampleRate() float64 {
|
||||
if m.TraceSampleRate <= 0 || m.TraceSampleRate > 1 {
|
||||
return 0.01
|
||||
}
|
||||
return m.TraceSampleRate
|
||||
}
|
||||
|
||||
func (m *MonitoringConfig) IsMetricsEnabled() bool {
|
||||
return m.EnableMetrics
|
||||
}
|
||||
|
||||
func (m *MonitoringConfig) IsTracingEnabled() bool {
|
||||
return m.EnableTracing
|
||||
}
|
||||
|
||||
func (s *SecurityConfig) GetTLSMinVersion() uint16 {
|
||||
switch s.MinVersion {
|
||||
case "1.0":
|
||||
return 0x0301
|
||||
case "1.1":
|
||||
return 0x0302
|
||||
case "1.2":
|
||||
return 0x0303
|
||||
case "1.3":
|
||||
return 0x0304
|
||||
default:
|
||||
return 0x0303
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SecurityConfig) IsTLSEnabled() bool {
|
||||
return s.EnableTLS && s.CertFile != "" && s.KeyFile != ""
|
||||
}
|
||||
|
||||
func (s *SecurityConfig) GetCertFile() string {
|
||||
return s.CertFile
|
||||
}
|
||||
|
||||
func (s *SecurityConfig) GetKeyFile() string {
|
||||
return s.KeyFile
|
||||
}
|
||||
|
||||
func (s *SecurityConfig) GetCAFile() string {
|
||||
return s.CAFile
|
||||
}
|
||||
|
||||
func (c *CompressionConfig) IsCompressionEnabled() bool {
|
||||
return c.Enabled
|
||||
}
|
||||
|
||||
func (c *CompressionConfig) GetAlgorithm() string {
|
||||
if c.Algorithm == "" {
|
||||
return "snappy"
|
||||
}
|
||||
return c.Algorithm
|
||||
}
|
||||
|
||||
func (c *CompressionConfig) GetLevel() int {
|
||||
if c.Level < 1 {
|
||||
return 3
|
||||
}
|
||||
if c.Level > 9 {
|
||||
return 9
|
||||
}
|
||||
return c.Level
|
||||
}
|
||||
|
||||
func (c *CompressionConfig) GetMinSize() int {
|
||||
if c.MinSize <= 0 {
|
||||
return 1024
|
||||
}
|
||||
return c.MinSize
|
||||
}
|
||||
|
||||
func (w *WebUIConfig) IsWebUIEnabled() bool {
|
||||
return w.Enabled
|
||||
}
|
||||
|
||||
func (w *WebUIConfig) GetWebUIPort() int {
|
||||
if w.Port <= 0 {
|
||||
return 8080
|
||||
}
|
||||
return w.Port
|
||||
}
|
||||
|
||||
func (w *WebUIConfig) GetTheme() string {
|
||||
if w.Theme == "" {
|
||||
return "dark"
|
||||
}
|
||||
return w.Theme
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.Cluster.NodePort <= 0 || c.Cluster.NodePort > 65535 {
|
||||
return fmt.Errorf("invalid node_port: %d", c.Cluster.NodePort)
|
||||
}
|
||||
if c.Cluster.RaftPort <= 0 || c.Cluster.RaftPort > 65535 {
|
||||
return fmt.Errorf("invalid raft_port: %d", c.Cluster.RaftPort)
|
||||
}
|
||||
if c.Cluster.PriorityZone < 0 || c.Cluster.PriorityZone > 9 {
|
||||
return fmt.Errorf("priority_zone must be between 0 and 9, got %d", c.Cluster.PriorityZone)
|
||||
}
|
||||
if c.Performance.BatchSize < 1 {
|
||||
return fmt.Errorf("batch_size must be at least 1, got %d", c.Performance.BatchSize)
|
||||
}
|
||||
if c.Performance.MaxConnections < 1 {
|
||||
return fmt.Errorf("max_connections must be at least 1, got %d", c.Performance.MaxConnections)
|
||||
}
|
||||
if c.Monitoring.TraceSampleRate < 0 || c.Monitoring.TraceSampleRate > 1 {
|
||||
return fmt.Errorf("trace_sample_rate must be between 0 and 1, got %f", c.Monitoring.TraceSampleRate)
|
||||
}
|
||||
if c.API.Port == c.Cluster.NodePort {
|
||||
return fmt.Errorf("API port %d conflicts with cluster node port", c.API.Port)
|
||||
}
|
||||
if c.WebUI.Port == c.Cluster.NodePort {
|
||||
return fmt.Errorf("WebUI port %d conflicts with cluster node port", c.WebUI.Port)
|
||||
}
|
||||
if c.Monitoring.MetricsPort == c.Cluster.NodePort {
|
||||
return fmt.Errorf("metrics port %d conflicts with cluster node port", c.Monitoring.MetricsPort)
|
||||
}
|
||||
if c.Security.IsTLSEnabled() {
|
||||
if c.Security.CertFile == "" {
|
||||
return fmt.Errorf("cert_file is required when TLS is enabled")
|
||||
}
|
||||
if c.Security.KeyFile == "" {
|
||||
return fmt.Errorf("key_file is required when TLS is enabled")
|
||||
}
|
||||
}
|
||||
if c.Compression.Enabled {
|
||||
switch c.Compression.Algorithm {
|
||||
case "snappy", "lz4", "zstd":
|
||||
default:
|
||||
return fmt.Errorf("unsupported compression algorithm: %s, supported: snappy, lz4, zstd", c.Compression.Algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация новых настроек
|
||||
if c.WAL.RecoveryWorkers < 1 {
|
||||
return fmt.Errorf("wal.recovery_workers must be at least 1, got %d", c.WAL.RecoveryWorkers)
|
||||
}
|
||||
if c.MVCC.MaxVersionsPerDoc < 0 {
|
||||
return fmt.Errorf("mvcc.max_versions_per_doc cannot be negative, got %d", c.MVCC.MaxVersionsPerDoc)
|
||||
}
|
||||
if c.Plugins.MaxCPUTimeMs < 0 {
|
||||
return fmt.Errorf("plugins.max_cpu_time_ms cannot be negative, got %d", c.Plugins.MaxCPUTimeMs)
|
||||
}
|
||||
if c.Plugins.MaxMemoryMB < 0 {
|
||||
return fmt.Errorf("plugins.max_memory_mb cannot be negative, got %d", c.Plugins.MaxMemoryMB)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
var cfg Config
|
||||
if _, err := toml.DecodeFile(path, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode config file: %v", err)
|
||||
}
|
||||
|
||||
// Значения по умолчанию для существующих настроек
|
||||
if cfg.Cluster.RaftPort == 0 {
|
||||
cfg.Cluster.RaftPort = 9878
|
||||
}
|
||||
if cfg.Cluster.RaftDataDir == "" {
|
||||
cfg.Cluster.RaftDataDir = "raft_data"
|
||||
}
|
||||
if cfg.Replication.ReplicationTimeoutMs == 0 {
|
||||
cfg.Replication.ReplicationTimeoutMs = 5000
|
||||
}
|
||||
if cfg.Plugins.ScriptDir == "" {
|
||||
cfg.Plugins.ScriptDir = "plugins"
|
||||
}
|
||||
if cfg.API.Port == 0 {
|
||||
cfg.API.Port = 8080
|
||||
}
|
||||
if cfg.Compression.Algorithm == "" {
|
||||
cfg.Compression.Algorithm = "snappy"
|
||||
}
|
||||
if cfg.Compression.MinSize == 0 {
|
||||
cfg.Compression.MinSize = 1024
|
||||
}
|
||||
if cfg.Compression.Level == 0 {
|
||||
cfg.Compression.Level = 3
|
||||
}
|
||||
if cfg.WebUI.Port == 0 {
|
||||
cfg.WebUI.Port = 9080
|
||||
}
|
||||
if cfg.WebUI.Theme == "" {
|
||||
cfg.WebUI.Theme = "dark"
|
||||
}
|
||||
if cfg.Cluster.HeartbeatTimeoutMs == 0 {
|
||||
cfg.Cluster.HeartbeatTimeoutMs = 1000
|
||||
}
|
||||
if cfg.Cluster.ElectionTimeoutMs == 0 {
|
||||
cfg.Cluster.ElectionTimeoutMs = 1000
|
||||
}
|
||||
if cfg.Cluster.CommitTimeoutMs == 0 {
|
||||
cfg.Cluster.CommitTimeoutMs = 500
|
||||
}
|
||||
if cfg.Cluster.SnapshotIntervalMin == 0 {
|
||||
cfg.Cluster.SnapshotIntervalMin = 30
|
||||
}
|
||||
if cfg.Cluster.SnapshotThreshold == 0 {
|
||||
cfg.Cluster.SnapshotThreshold = 1000
|
||||
}
|
||||
if cfg.Cluster.RecoveryTimeoutSec == 0 {
|
||||
cfg.Cluster.RecoveryTimeoutSec = 30
|
||||
}
|
||||
if cfg.Cluster.Region == "" {
|
||||
cfg.Cluster.Region = "default"
|
||||
}
|
||||
if cfg.Performance.BatchSize == 0 {
|
||||
cfg.Performance.BatchSize = 100
|
||||
}
|
||||
if cfg.Performance.MaxConnections == 0 {
|
||||
cfg.Performance.MaxConnections = 1000
|
||||
}
|
||||
if cfg.Monitoring.MetricsPort == 0 {
|
||||
cfg.Monitoring.MetricsPort = 9090
|
||||
}
|
||||
if cfg.Monitoring.TraceSampleRate == 0 {
|
||||
cfg.Monitoring.TraceSampleRate = 0.01
|
||||
}
|
||||
if cfg.Recovery.MaxRetrySec == 0 {
|
||||
cfg.Recovery.MaxRetrySec = 300
|
||||
}
|
||||
if cfg.Recovery.DataReplicationTimeoutSec == 0 {
|
||||
cfg.Recovery.DataReplicationTimeoutSec = 60
|
||||
}
|
||||
if cfg.Recovery.StaleReadTimeoutSec == 0 {
|
||||
cfg.Recovery.StaleReadTimeoutSec = 30
|
||||
}
|
||||
if cfg.Security.MinVersion == "" {
|
||||
cfg.Security.MinVersion = "1.2"
|
||||
}
|
||||
|
||||
// Значения по умолчанию для WAL
|
||||
if cfg.WAL.SegmentSizeMB == 0 {
|
||||
cfg.WAL.SegmentSizeMB = 64
|
||||
}
|
||||
if cfg.WAL.SyncIntervalSec == 0 {
|
||||
cfg.WAL.SyncIntervalSec = 5
|
||||
}
|
||||
if cfg.WAL.BatchSize == 0 {
|
||||
cfg.WAL.BatchSize = 100
|
||||
}
|
||||
if cfg.WAL.RecoveryWorkers == 0 {
|
||||
cfg.WAL.RecoveryWorkers = 4
|
||||
}
|
||||
|
||||
// Значения по умолчанию для MVCC
|
||||
if cfg.MVCC.MaxVersionsPerDoc == 0 {
|
||||
cfg.MVCC.MaxVersionsPerDoc = 10
|
||||
}
|
||||
if cfg.MVCC.VisibilityMapSize == 0 {
|
||||
cfg.MVCC.VisibilityMapSize = 1024 * 1024
|
||||
}
|
||||
if cfg.MVCC.PruneIntervalMin == 0 {
|
||||
cfg.MVCC.PruneIntervalMin = 5
|
||||
}
|
||||
if cfg.MVCC.RetentionDays == 0 {
|
||||
cfg.MVCC.RetentionDays = 7
|
||||
}
|
||||
if cfg.MVCC.ReadCacheSize == 0 {
|
||||
cfg.MVCC.ReadCacheSize = 10000
|
||||
}
|
||||
if cfg.MVCC.ReadCacheTTLSec == 0 {
|
||||
cfg.MVCC.ReadCacheTTLSec = 300
|
||||
}
|
||||
|
||||
// Значения по умолчанию для транзакций
|
||||
if cfg.Transactions.DefaultTimeoutSec == 0 {
|
||||
cfg.Transactions.DefaultTimeoutSec = 30
|
||||
}
|
||||
if cfg.Transactions.DeadlockCheckIntervalSec == 0 {
|
||||
cfg.Transactions.DeadlockCheckIntervalSec = 1
|
||||
}
|
||||
if cfg.Transactions.MaxSavepointsPerTx == 0 {
|
||||
cfg.Transactions.MaxSavepointsPerTx = 100
|
||||
}
|
||||
if cfg.Transactions.TwoPhaseCommitTimeoutSec == 0 {
|
||||
cfg.Transactions.TwoPhaseCommitTimeoutSec = 10
|
||||
}
|
||||
if cfg.Transactions.CheckpointIntervalSec == 0 {
|
||||
cfg.Transactions.CheckpointIntervalSec = 300
|
||||
}
|
||||
|
||||
// Значения по умолчанию для ACL
|
||||
if cfg.ACL.MaxDeniedLogSize == 0 {
|
||||
cfg.ACL.MaxDeniedLogSize = 10000
|
||||
}
|
||||
if cfg.ACL.TemporaryGrantTTLHours == 0 {
|
||||
cfg.ACL.TemporaryGrantTTLHours = 24
|
||||
}
|
||||
if cfg.ACL.CacheTTLSec == 0 {
|
||||
cfg.ACL.CacheTTLSec = 60
|
||||
}
|
||||
|
||||
// Значения по умолчанию для расширенных настроек плагинов
|
||||
if cfg.Plugins.MaxCPUTimeMs == 0 {
|
||||
cfg.Plugins.MaxCPUTimeMs = 100
|
||||
}
|
||||
if cfg.Plugins.MaxMemoryMB == 0 {
|
||||
cfg.Plugins.MaxMemoryMB = 50
|
||||
}
|
||||
if cfg.Plugins.MaxExecutionTimeSec == 0 {
|
||||
cfg.Plugins.MaxExecutionTimeSec = 5
|
||||
}
|
||||
if cfg.Plugins.MaxInstructions == 0 {
|
||||
cfg.Plugins.MaxInstructions = 1000000
|
||||
}
|
||||
if cfg.Plugins.HotReloadIntervalSec == 0 {
|
||||
cfg.Plugins.HotReloadIntervalSec = 30
|
||||
}
|
||||
if cfg.Plugins.MaxEventLogSize == 0 {
|
||||
cfg.Plugins.MaxEventLogSize = 1000
|
||||
}
|
||||
if cfg.Plugins.LoadTimeoutSec == 0 {
|
||||
cfg.Plugins.LoadTimeoutSec = 10
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("config validation failed: %v", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (c *Config) GetConfigSummary() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"cluster": map[string]interface{}{
|
||||
"name": c.Cluster.Name,
|
||||
"node_ip": c.Cluster.NodeIP,
|
||||
"node_port": c.Cluster.NodePort,
|
||||
"raft_port": c.Cluster.RaftPort,
|
||||
"bootstrap": c.Cluster.Bootstrap,
|
||||
"nodes_count": len(c.Cluster.Nodes),
|
||||
"region": c.Cluster.GetRegion(),
|
||||
"priority_zone": c.Cluster.GetPriorityZone(),
|
||||
"split_brain_prevention": c.Cluster.IsSplitBrainPreventionEnabled(),
|
||||
},
|
||||
"replication": map[string]interface{}{
|
||||
"enabled": c.Replication.IsReplicationEnabled(),
|
||||
"master_master": c.Replication.IsMasterMasterEnabled(),
|
||||
"sync": c.Replication.IsSyncReplicationEnabled(),
|
||||
"timeout_ms": c.Replication.ReplicationTimeoutMs,
|
||||
},
|
||||
"performance": map[string]interface{}{
|
||||
"pipeline": c.Performance.IsPipelineEnabled(),
|
||||
"batch_size": c.Performance.GetBatchSize(),
|
||||
"read_from_follower": c.Performance.IsReadFromFollowerEnabled(),
|
||||
"max_connections": c.Performance.GetMaxConnections(),
|
||||
},
|
||||
"security": map[string]interface{}{
|
||||
"tls_enabled": c.Security.IsTLSEnabled(),
|
||||
"tls_version": c.Security.MinVersion,
|
||||
},
|
||||
"monitoring": map[string]interface{}{
|
||||
"metrics_enabled": c.Monitoring.IsMetricsEnabled(),
|
||||
"metrics_port": c.Monitoring.GetMetricsPort(),
|
||||
"tracing_enabled": c.Monitoring.IsTracingEnabled(),
|
||||
"trace_sample_rate": c.Monitoring.GetTraceSampleRate(),
|
||||
},
|
||||
"compression": map[string]interface{}{
|
||||
"enabled": c.Compression.IsCompressionEnabled(),
|
||||
"algorithm": c.Compression.GetAlgorithm(),
|
||||
"level": c.Compression.GetLevel(),
|
||||
"min_size": c.Compression.GetMinSize(),
|
||||
},
|
||||
"wal": map[string]interface{}{
|
||||
"segment_size_mb": c.WAL.SegmentSizeMB,
|
||||
"sync_interval_sec": c.WAL.SyncIntervalSec,
|
||||
"batch_size": c.WAL.BatchSize,
|
||||
"recovery_workers": c.WAL.RecoveryWorkers,
|
||||
"enabled": c.WAL.Enabled,
|
||||
},
|
||||
"mvcc": map[string]interface{}{
|
||||
"max_versions_per_doc": c.MVCC.MaxVersionsPerDoc,
|
||||
"prune_interval_min": c.MVCC.PruneIntervalMin,
|
||||
"retention_days": c.MVCC.RetentionDays,
|
||||
},
|
||||
"transactions": map[string]interface{}{
|
||||
"default_timeout_sec": c.Transactions.DefaultTimeoutSec,
|
||||
"enabled": c.Transactions.Enabled,
|
||||
},
|
||||
"acl": map[string]interface{}{
|
||||
"enable_role_hierarchy": c.ACL.EnableRoleHierarchy,
|
||||
},
|
||||
}
|
||||
}
|
||||
518
internal/log/logger.go
Normal file
518
internal/log/logger.go
Normal file
@@ -0,0 +1,518 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
2400
internal/plugin/plugin.go
Normal file
2400
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
|
||||
}
|
||||
2549
internal/repl/repl.go
Normal file
2549
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,
|
||||
})
|
||||
}
|
||||
1354
internal/storage/collection.go
Normal file
1354
internal/storage/collection.go
Normal file
File diff suppressed because it is too large
Load Diff
584
internal/storage/document.go
Normal file
584
internal/storage/document.go
Normal file
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
* 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.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"futriis/internal/compression"
|
||||
"futriis/internal/serializer"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Document представляет документ в коллекции (аналог строки в реляционной СУБД)
|
||||
type Document struct {
|
||||
ID string `msgpack:"_id"` // Уникальный идентификатор документа
|
||||
Fields map[string]interface{} `msgpack:"fields"` // Поля документа (аналог колонок)
|
||||
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"` // Оригинальный размер до сжатия
|
||||
mu sync.RWMutex `msgpack:"-"` // Блокировка для wait-free операций
|
||||
}
|
||||
|
||||
// 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()
|
||||
return &Document{
|
||||
ID: uuid.New().String(),
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DeletedAt: 0,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDocumentWithID создаёт документ с указанным ID
|
||||
func NewDocumentWithID(id string) *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
return &Document{
|
||||
ID: id,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DeletedAt: 0,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SetField устанавливает значение поля документа (wait-free)
|
||||
func (d *Document) SetField(name string, value interface{}) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.Fields[name] = value
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // При изменении документа снимаем флаг сжатия
|
||||
|
||||
// Аудит изменения поля
|
||||
AuditFieldOperation("UPDATE", "", "", d.ID, name, value)
|
||||
}
|
||||
|
||||
// GetField возвращает значение поля документа
|
||||
func (d *Document) GetField(name string) (interface{}, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if val, ok := d.Fields[name]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field not found: %s", name)
|
||||
}
|
||||
|
||||
// DeleteField удаляет поле из документа
|
||||
func (d *Document) DeleteField(name string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
delete(d.Fields, name)
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false
|
||||
|
||||
// Аудит удаления поля
|
||||
AuditFieldOperation("DELETE", "", "", d.ID, name, nil)
|
||||
}
|
||||
|
||||
// HasField проверяет наличие поля в документе
|
||||
func (d *Document) HasField(name string) bool {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
_, ok := d.Fields[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetFields возвращает копию всех полей документа
|
||||
func (d *Document) GetFields() map[string]interface{} {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
copy := make(map[string]interface{})
|
||||
for k, v := range d.Fields {
|
||||
copy[k] = v
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// 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) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
data, err := serializer.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SerializeCompressed сериализует и сжимает документ
|
||||
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
// Сериализуем документ
|
||||
data, err := serializer.Marshal(d)
|
||||
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 {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// Пытаемся определить, сжаты ли данные
|
||||
// Для этого пробуем распаковать, если не получается - данные несжатые
|
||||
decompressed, err := compression.DecompressAuto(data)
|
||||
if err == nil && len(decompressed) < len(data) {
|
||||
// Данные были сжаты, используем распакованную версию
|
||||
if err := serializer.Unmarshal(decompressed, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(len(decompressed))
|
||||
} else {
|
||||
// Данные не сжаты или не удалось распаковать
|
||||
if err := serializer.Unmarshal(data, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
}
|
||||
|
||||
// Обновляем временные метки при десериализации
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone создаёт глубокую копию документа
|
||||
func (d *Document) Clone() *Document {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
clone := &Document{
|
||||
ID: d.ID,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
DeletedAt: d.DeletedAt,
|
||||
Version: d.Version,
|
||||
Compressed: d.Compressed,
|
||||
OriginalSize: d.OriginalSize,
|
||||
}
|
||||
|
||||
// Глубокое копирование полей
|
||||
for k, v := range d.Fields {
|
||||
clone.Fields[k] = deepCopyValue(v)
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// Update применяет обновление к документу (атомарно)
|
||||
func (d *Document) Update(updates map[string]interface{}) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
for k, v := range updates {
|
||||
d.Fields[k] = v
|
||||
}
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // После обновления документ больше не сжат
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SoftDelete мягко удаляет документ (устанавливает метку времени удаления)
|
||||
func (d *Document) SoftDelete() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.DeletedAt = time.Now().UnixMilli()
|
||||
d.UpdatedAt = d.DeletedAt
|
||||
d.Version++
|
||||
}
|
||||
|
||||
// IsDeleted проверяет, удалён ли документ (мягкое удаление)
|
||||
func (d *Document) IsDeleted() bool {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return d.DeletedAt > 0
|
||||
}
|
||||
|
||||
// Restore восстанавливает мягко удалённый документ
|
||||
func (d *Document) Restore() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.DeletedAt = 0
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
}
|
||||
|
||||
// GetDeletedAtStr возвращает человекочитаемую строку времени удаления
|
||||
func (d *Document) GetDeletedAtStr() string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
if d.DeletedAt == 0 {
|
||||
return ""
|
||||
}
|
||||
return time.UnixMilli(d.DeletedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetCreatedAtStr возвращает человекочитаемую строку времени создания
|
||||
func (d *Document) GetCreatedAtStr() string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return time.UnixMilli(d.CreatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// GetUpdatedAtStr возвращает человекочитаемую строку времени обновления
|
||||
func (d *Document) GetUpdatedAtStr() string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return time.UnixMilli(d.UpdatedAt).Format("2006-01-02 15:04:05.000")
|
||||
}
|
||||
|
||||
// Compress сжимает документ в памяти
|
||||
func (d *Document) Compress(config *compression.Config) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Сохраняем текущее состояние
|
||||
originalSize := len(d.Fields)
|
||||
if originalSize < config.MinSize {
|
||||
return nil // Не сжимаем маленькие документы
|
||||
}
|
||||
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(originalSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decompress распаковывает документ в памяти
|
||||
func (d *Document) Decompress() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if !d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompressionRatio возвращает коэффициент сжатия
|
||||
func (d *Document) GetCompressionRatio() float64 {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if !d.Compressed || d.OriginalSize == 0 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
currentSize := len(d.Fields)
|
||||
return float64(currentSize) / float64(d.OriginalSize)
|
||||
}
|
||||
|
||||
// GetMetadata возвращает метаданные документа (временные метки)
|
||||
func (d *Document) GetMetadata() map[string]int64 {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Иначе нужно создать промежуточные структуры
|
||||
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
|
||||
}
|
||||
797
internal/storage/engine.go
Normal file
797
internal/storage/engine.go
Normal file
@@ -0,0 +1,797 @@
|
||||
/*
|
||||
* 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// 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()
|
||||
|
||||
// Добавляем ACL при экспорте
|
||||
collData["acl_timestamps"] = coll.GetACLTimestamps()
|
||||
|
||||
dbData[key.(string)] = collData
|
||||
return true
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
466
internal/storage/persistence.go
Normal file
466
internal/storage/persistence.go
Normal file
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
* 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"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// DefaultPersistenceConfig возвращает конфигурацию по умолчанию
|
||||
func DefaultPersistenceConfig() *PersistenceConfig {
|
||||
return &PersistenceConfig{
|
||||
DataDir: "futriis_data",
|
||||
CheckpointInterval: 5 * time.Minute,
|
||||
MaxCheckpoints: 10,
|
||||
CompressEnabled: true,
|
||||
SyncWrites: 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"`
|
||||
}
|
||||
|
||||
// PersistenceManager управляет персистентным хранением
|
||||
type PersistenceManager struct {
|
||||
config *PersistenceConfig
|
||||
storage *Storage
|
||||
logger LoggerInterface
|
||||
mu sync.RWMutex
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
lastCheckpoint int64
|
||||
checkpointID atomic.Uint64
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
// 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 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 {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database not found: %s", dbName)
|
||||
}
|
||||
|
||||
// Собираем данные всех коллекций
|
||||
snapshot := &DatabaseSnapshot{
|
||||
Name: dbName,
|
||||
Collections: make(map[string]interface{}),
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Version: pm.checkpointID.Add(1),
|
||||
}
|
||||
|
||||
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,
|
||||
"version": doc.Version,
|
||||
})
|
||||
}
|
||||
snapshot.Collections[collName] = collData
|
||||
}
|
||||
|
||||
// Сериализуем и сохраняем
|
||||
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 := os.WriteFile(filename, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Синхронизируем с диском если нужно
|
||||
if pm.config.SyncWrites {
|
||||
if f, err := os.OpenFile(filename, os.O_RDWR, 0644); err == nil {
|
||||
RealFsync(f)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Saved database %s checkpoint %d (%d bytes)", dbName, snapshot.Version, len(data)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAll сохраняет все базы данных
|
||||
func (pm *PersistenceManager) SaveAll() error {
|
||||
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 загружает базу данных с диска
|
||||
func (pm *PersistenceManager) LoadDatabase(dbName string) error {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
|
||||
// Находим последний чекпоинт
|
||||
snapshot, err := pm.findLatestSnapshot(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if snapshot == nil {
|
||||
return fmt.Errorf("no snapshot found for database: %s", dbName)
|
||||
}
|
||||
|
||||
// Восстанавливаем базу данных
|
||||
if !pm.storage.ExistsDatabase(dbName) {
|
||||
if err := pm.storage.CreateDatabase(dbName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Восстанавливаем коллекции
|
||||
for collName, collDataRaw := range snapshot.Collections {
|
||||
collData, ok := collDataRaw.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := db.GetCollection(collName); err != nil {
|
||||
if err := db.CreateCollection(collName); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if updatedAt, ok := docMap["updated_at"].(float64); ok {
|
||||
doc.UpdatedAt = int64(updatedAt)
|
||||
}
|
||||
|
||||
if version, ok := docMap["version"].(float64); ok {
|
||||
doc.Version = uint64(version)
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to restore document %s: %v", docID, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Loaded database %s from snapshot (version %d)", dbName, snapshot.Version))
|
||||
}
|
||||
|
||||
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)
|
||||
// Извлекаем имя базы данных из имени файла
|
||||
// Формат: snapshot_{dbname}_{version}.json.gz
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
2241
internal/storage/transaction.go
Normal file
2241
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