new version with saga

This commit is contained in:
2026-07-12 01:39:26 +03:00
commit 10f6ed0bda
55 changed files with 40117 additions and 0 deletions

498
internal/acl/manger.go Normal file
View File

@@ -0,0 +1,498 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/acl/manager.go
// Назначение: Глобальный менеджер ACL для всей СУБД.
// Управляет пользователями, ролями и разрешениями на уровне БД и коллекций.
// Реализован с использованием sync.Map для wait-free доступа.
package acl
import (
"fmt"
"sync"
"time"
"github.com/google/uuid"
)
// PermissionType определяет тип разрешения
type PermissionType string
const (
PermRead PermissionType = "read"
PermWrite PermissionType = "write"
PermDelete PermissionType = "delete"
PermAdmin PermissionType = "admin"
)
// User представляет пользователя системы
type User struct {
ID string `msgpack:"id"`
Username string `msgpack:"username"`
Password string `msgpack:"password"` // В реальной системе - хеш
Roles []string `msgpack:"roles"`
CreatedAt int64 `msgpack:"created_at"`
LastLogin int64 `msgpack:"last_login"`
Active bool `msgpack:"active"`
}
// Session представляет активную сессию пользователя
type Session struct {
ID string `msgpack:"id"`
Username string `msgpack:"username"`
Roles []string `msgpack:"roles"`
CreatedAt int64 `msgpack:"created_at"`
ExpiresAt int64 `msgpack:"expires_at"`
}
// Role представляет роль с набором разрешений
type Role struct {
Name string `msgpack:"name"`
Permissions []string `msgpack:"permissions"` // "database.collection:read" формат
}
// ACLManager управляет доступом к БД
type ACLManager struct {
users sync.Map // map[string]*User
roles sync.Map // map[string]*Role
sessions sync.Map // map[string]*Session - sessionID -> Session
mu sync.RWMutex
}
// NewACLManager создаёт новый менеджер ACL
func NewACLManager() *ACLManager {
m := &ACLManager{}
// Создаём роль администратора по умолчанию
adminRole := &Role{
Name: "admin",
Permissions: []string{"*:*"},
}
m.roles.Store("admin", adminRole)
// Создаём пользователя admin по умолчанию
adminUser := &User{
ID: uuid.New().String(),
Username: "admin",
Password: "admin", // В продакшене использовать хеш!
Roles: []string{"admin"},
CreatedAt: time.Now().UnixMilli(),
Active: true,
}
m.users.Store("admin", adminUser)
// Создаём роль guest с ограниченными правами
guestRole := &Role{
Name: "guest",
Permissions: []string{},
}
m.roles.Store("guest", guestRole)
// Создаём пользователя guest по умолчанию
guestUser := &User{
ID: uuid.New().String(),
Username: "guest",
Password: "guest",
Roles: []string{"guest"},
CreatedAt: time.Now().UnixMilli(),
Active: true,
}
m.users.Store("guest", guestUser)
return m
}
// CreateUser создаёт нового пользователя
func (m *ACLManager) CreateUser(username, password string, roles []string) error {
if _, exists := m.users.Load(username); exists {
return fmt.Errorf("user %s already exists", username)
}
user := &User{
ID: uuid.New().String(),
Username: username,
Password: password,
Roles: roles,
CreatedAt: time.Now().UnixMilli(),
Active: true,
}
m.users.Store(username, user)
return nil
}
// Authenticate аутентифицирует пользователя и создаёт сессию
func (m *ACLManager) Authenticate(username, password string) (string, error) {
val, ok := m.users.Load(username)
if !ok {
return "", fmt.Errorf("user not found")
}
user := val.(*User)
if !user.Active {
return "", fmt.Errorf("user is disabled")
}
if user.Password != password {
return "", fmt.Errorf("invalid password")
}
// Обновляем время последнего входа
user.LastLogin = time.Now().UnixMilli()
m.users.Store(username, user)
// Создаём сессию (24 часа)
sessionID := uuid.New().String()
now := time.Now().Unix()
session := &Session{
ID: sessionID,
Username: username,
Roles: user.Roles,
CreatedAt: now,
ExpiresAt: now + 86400, // 24 часа
}
m.sessions.Store(sessionID, session)
return sessionID, nil
}
// Logout завершает сессию
func (m *ACLManager) Logout(sessionID string) {
m.sessions.Delete(sessionID)
}
// CheckSession проверяет, активна ли сессия
func (m *ACLManager) CheckSession(sessionID string) bool {
val, ok := m.sessions.Load(sessionID)
if !ok {
return false
}
session := val.(*Session)
// Проверка на expiry
if time.Now().Unix() > session.ExpiresAt {
m.sessions.Delete(sessionID)
return false
}
return true
}
// GetUsername возвращает имя пользователя по ID сессии
func (m *ACLManager) GetUsername(sessionID string) string {
val, ok := m.sessions.Load(sessionID)
if !ok {
return ""
}
session := val.(*Session)
return session.Username
}
// GetUserRoles возвращает роли пользователя по ID сессии
func (m *ACLManager) GetUserRoles(sessionID string) []string {
val, ok := m.sessions.Load(sessionID)
if !ok {
return []string{}
}
session := val.(*Session)
return session.Roles
}
// CheckPermission проверяет разрешение для сессии
func (m *ACLManager) CheckPermission(sessionID, database, collection, operation string) bool {
val, ok := m.sessions.Load(sessionID)
if !ok {
return false
}
session := val.(*Session)
for _, roleName := range session.Roles {
roleVal, ok := m.roles.Load(roleName)
if !ok {
continue
}
role := roleVal.(*Role)
for _, perm := range role.Permissions {
if m.matchPermission(perm, database, collection, operation) {
return true
}
}
}
return false
}
// matchPermission проверяет соответствие разрешения
func (m *ACLManager) matchPermission(perm, database, collection, operation string) bool {
// Формат: "database.collection:operation" или "*:*" для всех
// или "database.*:read" для всех коллекций в БД
parts := splitPermission(perm)
if len(parts) != 2 {
return false
}
resource := parts[0] // "database.collection" или "database.*"
op := parts[1] // "read", "write", "delete", "admin"
// Проверка операции
if op != "*" && op != operation && operation != "admin" {
return false
}
// Администратор имеет доступ ко всему
if op == "admin" || op == "*" {
return true
}
// Проверка ресурса
if resource == "*:*" {
return true
}
resourceParts := splitResource(resource)
if len(resourceParts) != 2 {
return false
}
dbPattern := resourceParts[0]
collPattern := resourceParts[1]
if dbPattern != "*" && dbPattern != database {
return false
}
if collPattern != "*" && collPattern != collection {
return false
}
return true
}
// GrantPermission выдаёт разрешение роли
func (m *ACLManager) GrantPermission(roleName, permission string) error {
val, ok := m.roles.Load(roleName)
if !ok {
return fmt.Errorf("role not found")
}
role := val.(*Role)
role.Permissions = append(role.Permissions, permission)
m.roles.Store(roleName, role)
return nil
}
// RevokePermission отзывает разрешение у роли
func (m *ACLManager) RevokePermission(roleName, permission string) error {
val, ok := m.roles.Load(roleName)
if !ok {
return fmt.Errorf("role not found")
}
role := val.(*Role)
newPermissions := make([]string, 0, len(role.Permissions))
for _, p := range role.Permissions {
if p != permission {
newPermissions = append(newPermissions, p)
}
}
role.Permissions = newPermissions
m.roles.Store(roleName, role)
return nil
}
// CreateRole создаёт новую роль
func (m *ACLManager) CreateRole(name string) error {
if _, exists := m.roles.Load(name); exists {
return fmt.Errorf("role %s already exists", name)
}
role := &Role{
Name: name,
Permissions: []string{},
}
m.roles.Store(name, role)
return nil
}
// DeleteRole удаляет роль
func (m *ACLManager) DeleteRole(name string) error {
if _, exists := m.roles.LoadAndDelete(name); !exists {
return fmt.Errorf("role not found")
}
return nil
}
// AddUserRole добавляет роль пользователю
func (m *ACLManager) AddUserRole(username, roleName string) error {
val, ok := m.users.Load(username)
if !ok {
return fmt.Errorf("user not found")
}
user := val.(*User)
// Проверяем, есть ли уже такая роль
for _, r := range user.Roles {
if r == roleName {
return fmt.Errorf("user already has role %s", roleName)
}
}
user.Roles = append(user.Roles, roleName)
m.users.Store(username, user)
return nil
}
// RemoveUserRole удаляет роль у пользователя
func (m *ACLManager) RemoveUserRole(username, roleName string) error {
val, ok := m.users.Load(username)
if !ok {
return fmt.Errorf("user not found")
}
user := val.(*User)
newRoles := make([]string, 0, len(user.Roles))
for _, r := range user.Roles {
if r != roleName {
newRoles = append(newRoles, r)
}
}
user.Roles = newRoles
m.users.Store(username, user)
return nil
}
// DisableUser отключает пользователя
func (m *ACLManager) DisableUser(username string) error {
val, ok := m.users.Load(username)
if !ok {
return fmt.Errorf("user not found")
}
user := val.(*User)
user.Active = false
m.users.Store(username, user)
return nil
}
// EnableUser включает пользователя
func (m *ACLManager) EnableUser(username string) error {
val, ok := m.users.Load(username)
if !ok {
return fmt.Errorf("user not found")
}
user := val.(*User)
user.Active = true
m.users.Store(username, user)
return nil
}
// DeleteUser удаляет пользователя
func (m *ACLManager) DeleteUser(username string) error {
if _, exists := m.users.LoadAndDelete(username); !exists {
return fmt.Errorf("user not found")
}
return nil
}
// ChangePassword изменяет пароль пользователя
func (m *ACLManager) ChangePassword(username, newPassword string) error {
val, ok := m.users.Load(username)
if !ok {
return fmt.Errorf("user not found")
}
user := val.(*User)
user.Password = newPassword
m.users.Store(username, user)
return nil
}
// ListUsers возвращает список всех пользователей
func (m *ACLManager) ListUsers() []string {
users := make([]string, 0)
m.users.Range(func(key, value interface{}) bool {
users = append(users, key.(string))
return true
})
return users
}
// ListRoles возвращает список всех ролей
func (m *ACLManager) ListRoles() []string {
roles := make([]string, 0)
m.roles.Range(func(key, value interface{}) bool {
roles = append(roles, key.(string))
return true
})
return roles
}
// GetUserInfo возвращает информацию о пользователе
func (m *ACLManager) GetUserInfo(username string) (*User, error) {
val, ok := m.users.Load(username)
if !ok {
return nil, fmt.Errorf("user not found")
}
user := val.(*User)
// Возвращаем копию, чтобы избежать модификации извне
return &User{
ID: user.ID,
Username: user.Username,
Roles: user.Roles,
CreatedAt: user.CreatedAt,
LastLogin: user.LastLogin,
Active: user.Active,
}, nil
}
// GetRolePermissions возвращает разрешения роли
func (m *ACLManager) GetRolePermissions(roleName string) ([]string, error) {
val, ok := m.roles.Load(roleName)
if !ok {
return nil, fmt.Errorf("role not found")
}
role := val.(*Role)
return role.Permissions, nil
}
// Helper functions
func splitPermission(perm string) []string {
for i := 0; i < len(perm); i++ {
if perm[i] == ':' {
return []string{perm[:i], perm[i+1:]}
}
}
return []string{perm, ""}
}
func splitResource(resource string) []string {
for i := 0; i < len(resource); i++ {
if resource[i] == '.' {
return []string{resource[:i], resource[i+1:]}
}
}
return []string{resource, "*"}
}

1705
internal/api/http.go Normal file

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3106
internal/api/webui.go Normal file

File diff suppressed because it is too large Load Diff

View 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
}

View 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()
}

File diff suppressed because it is too large Load Diff

349
internal/cluster/auth.go Normal file
View File

@@ -0,0 +1,349 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/auth.go
// Назначение: Аутентификация и авторизация между узлами кластера
package cluster
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// NodeAuthConfig конфигурация аутентификации узлов
type NodeAuthConfig struct {
Enabled bool `json:"enabled"`
TokenTTL time.Duration `json:"token_ttl"`
PrivateKeyPath string `json:"private_key_path"`
PublicKeyPath string `json:"public_key_path"`
AllowedNodes []string `json:"allowed_nodes"`
RequireMTLS bool `json:"require_mtls"`
}
// NodeAuthToken представляет токен аутентификации узла
type NodeAuthToken struct {
NodeID string `json:"node_id"`
IssuedAt int64 `json:"issued_at"`
ExpiresAt int64 `json:"expires_at"`
Signature string `json:"signature"`
}
// NodeAuthenticator управляет аутентификацией между узлами
type NodeAuthenticator struct {
config *NodeAuthConfig
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
tokens sync.Map
mu sync.RWMutex
logger LoggerInterface // Используем LoggerInterface из node.go
}
// NewNodeAuthenticator создаёт новый аутентификатор узлов
func NewNodeAuthenticator(config *NodeAuthConfig, logger LoggerInterface) (*NodeAuthenticator, error) {
if config == nil {
config = &NodeAuthConfig{
Enabled: true,
TokenTTL: 24 * time.Hour,
AllowedNodes: make([]string, 0),
RequireMTLS: false,
}
}
na := &NodeAuthenticator{
config: config,
logger: logger,
}
if config.Enabled {
if err := na.loadKeys(); err != nil {
if err := na.generateKeys(); err != nil {
return nil, err
}
}
}
return na, nil
}
// generateKeys генерирует RSA ключи для аутентификации
func (na *NodeAuthenticator) generateKeys() error {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
na.privateKey = privateKey
na.publicKey = &privateKey.PublicKey
// Сохраняем ключи
if err := na.saveKeys(); err != nil {
return err
}
if na.logger != nil {
na.logger.Info("Generated new RSA keys for node authentication")
}
return nil
}
// loadKeys загружает RSA ключи с диска
func (na *NodeAuthenticator) loadKeys() error {
// Загружаем приватный ключ
privData, err := os.ReadFile(na.config.PrivateKeyPath)
if err != nil {
return err
}
block, _ := pem.Decode(privData)
if block == nil {
return fmt.Errorf("failed to decode private key")
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return err
}
var ok bool
na.privateKey, ok = privateKey.(*rsa.PrivateKey)
if !ok {
return fmt.Errorf("invalid private key type")
}
// Загружаем публичный ключ
pubData, err := os.ReadFile(na.config.PublicKeyPath)
if err != nil {
return err
}
pubBlock, _ := pem.Decode(pubData)
if pubBlock == nil {
return fmt.Errorf("failed to decode public key")
}
publicKey, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
if err != nil {
return err
}
na.publicKey, ok = publicKey.(*rsa.PublicKey)
if !ok {
return fmt.Errorf("invalid public key type")
}
return nil
}
// saveKeys сохраняет RSA ключи на диск
func (na *NodeAuthenticator) saveKeys() error {
if na.privateKey == nil {
return fmt.Errorf("no private key to save")
}
// Сохраняем приватный ключ
privBytes, err := x509.MarshalPKCS8PrivateKey(na.privateKey)
if err != nil {
return err
}
privPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privBytes,
})
if err := os.MkdirAll(filepath.Dir(na.config.PrivateKeyPath), 0700); err != nil {
return err
}
if err := os.WriteFile(na.config.PrivateKeyPath, privPEM, 0600); err != nil {
return err
}
// Сохраняем публичный ключ
pubBytes, err := x509.MarshalPKIXPublicKey(na.publicKey)
if err != nil {
return err
}
pubPEM := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: pubBytes,
})
if err := os.WriteFile(na.config.PublicKeyPath, pubPEM, 0644); err != nil {
return err
}
return nil
}
// GenerateToken генерирует токен для узла
func (na *NodeAuthenticator) GenerateToken(nodeID string) (string, error) {
if !na.config.Enabled {
return "", nil
}
now := time.Now().UnixMilli()
token := &NodeAuthToken{
NodeID: nodeID,
IssuedAt: now,
ExpiresAt: now + na.config.TokenTTL.Milliseconds(),
}
// Подписываем токен
data, err := json.Marshal(token)
if err != nil {
return "", err
}
hash := sha256.Sum256(data)
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
token.Signature = base64.StdEncoding.EncodeToString(signature)
// Сохраняем токен
tokenData, err := json.Marshal(token)
if err != nil {
return "", err
}
tokenString := base64.StdEncoding.EncodeToString(tokenData)
na.tokens.Store(nodeID, token)
return tokenString, nil
}
// VerifyToken проверяет токен узла
func (na *NodeAuthenticator) VerifyToken(tokenString string) (string, error) {
if !na.config.Enabled {
return "", nil
}
data, err := base64.StdEncoding.DecodeString(tokenString)
if err != nil {
return "", fmt.Errorf("invalid token encoding: %v", err)
}
var token NodeAuthToken
if err := json.Unmarshal(data, &token); err != nil {
return "", fmt.Errorf("invalid token format: %v", err)
}
// Проверяем срок действия
now := time.Now().UnixMilli()
if token.ExpiresAt < now {
return "", fmt.Errorf("token expired")
}
// Проверяем подпись
sigData, err := base64.StdEncoding.DecodeString(token.Signature)
if err != nil {
return "", fmt.Errorf("invalid signature: %v", err)
}
tokenCopy := token
tokenCopy.Signature = ""
dataWithoutSig, _ := json.Marshal(tokenCopy)
hash := sha256.Sum256(dataWithoutSig)
if err := rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData); err != nil {
return "", fmt.Errorf("invalid signature: %v", err)
}
// Проверяем, разрешён ли узел
if len(na.config.AllowedNodes) > 0 {
allowed := false
for _, node := range na.config.AllowedNodes {
if node == token.NodeID {
allowed = true
break
}
}
if !allowed {
return "", fmt.Errorf("node %s not allowed", token.NodeID)
}
}
return token.NodeID, nil
}
// AuthenticateRequest аутентифицирует запрос
func (na *NodeAuthenticator) AuthenticateRequest(data []byte, signature string) bool {
if !na.config.Enabled {
return true
}
sigData, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return false
}
hash := sha256.Sum256(data)
err = rsa.VerifyPKCS1v15(na.publicKey, crypto.SHA256, hash[:], sigData)
return err == nil
}
// SignRequest подписывает запрос
func (na *NodeAuthenticator) SignRequest(data []byte) (string, error) {
if !na.config.Enabled {
return "", nil
}
hash := sha256.Sum256(data)
signature, err := rsa.SignPKCS1v15(rand.Reader, na.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// AddAllowedNode добавляет разрешённый узел
func (na *NodeAuthenticator) AddAllowedNode(nodeID string) {
na.mu.Lock()
defer na.mu.Unlock()
for _, n := range na.config.AllowedNodes {
if n == nodeID {
return
}
}
na.config.AllowedNodes = append(na.config.AllowedNodes, nodeID)
}
// RemoveAllowedNode удаляет разрешённый узел
func (na *NodeAuthenticator) RemoveAllowedNode(nodeID string) {
na.mu.Lock()
defer na.mu.Unlock()
newList := make([]string, 0)
for _, n := range na.config.AllowedNodes {
if n != nodeID {
newList = append(newList, n)
}
}
na.config.AllowedNodes = newList
}

View File

@@ -0,0 +1,301 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/backpressure.go
// Назначение: Backpressure при перегрузке системы
package cluster
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// BackpressureLevel представляет уровень перегрузки
type BackpressureLevel int
const (
LevelNone BackpressureLevel = iota // Нет перегрузки
LevelLow // Низкая перегрузка - задержки
LevelMedium // Средняя перегрузка - отклонение части запросов
LevelHigh // Высокая перегрузка - отклонение большинства
LevelCritical // Критическая - только чтение
)
// BackpressureManager управляет backpressure
type BackpressureManager struct {
mu sync.RWMutex
currentLevel BackpressureLevel
cpuThreshold float64
memoryThreshold float64
queueSizeThreshold int
connectionThreshold int
currentCPU atomic.Uint64
currentMemory atomic.Uint64
currentQueueSize atomic.Int64
currentConnections atomic.Int64
rejectedCount atomic.Uint64
delayedCount atomic.Uint64
lastCheck time.Time
checkInterval time.Duration
logger LoggerInterface // Используем LoggerInterface из node.go
enabled bool
writeAllowed bool
readAllowed bool
rejectProbability atomic.Uint32
delayDuration atomic.Int64
}
// BackpressureConfig содержит настройки backpressure
type BackpressureConfig struct {
Enabled bool `json:"enabled"`
CPUThreshold float64 `json:"cpu_threshold"`
MemoryThreshold float64 `json:"memory_threshold"`
QueueSizeThreshold int `json:"queue_size_threshold"`
ConnectionThreshold int `json:"connection_threshold"`
CheckIntervalMs int `json:"check_interval_ms"`
LowDelayMs int64 `json:"low_delay_ms"`
MediumRejectProb uint32 `json:"medium_reject_prob"`
HighRejectProb uint32 `json:"high_reject_prob"`
}
// DefaultBackpressureConfig возвращает конфигурацию по умолчанию
func DefaultBackpressureConfig() *BackpressureConfig {
return &BackpressureConfig{
Enabled: true,
CPUThreshold: 0.8,
MemoryThreshold: 0.85,
QueueSizeThreshold: 10000,
ConnectionThreshold: 5000,
CheckIntervalMs: 1000,
LowDelayMs: 100,
MediumRejectProb: 30,
HighRejectProb: 70,
}
}
// NewBackpressureManager создаёт новый менеджер backpressure
func NewBackpressureManager(cfg *BackpressureConfig, logger LoggerInterface) *BackpressureManager {
if cfg == nil {
cfg = DefaultBackpressureConfig()
}
bpm := &BackpressureManager{
currentLevel: LevelNone,
cpuThreshold: cfg.CPUThreshold,
memoryThreshold: cfg.MemoryThreshold,
queueSizeThreshold: cfg.QueueSizeThreshold,
connectionThreshold: cfg.ConnectionThreshold,
checkInterval: time.Duration(cfg.CheckIntervalMs) * time.Millisecond,
logger: logger,
enabled: cfg.Enabled,
writeAllowed: true,
readAllowed: true,
rejectProbability: atomic.Uint32{},
delayDuration: atomic.Int64{},
}
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(0)
if cfg.Enabled {
go bpm.monitorLoop()
}
if logger != nil {
logger.Debug("Backpressure manager initialized")
}
return bpm
}
// monitorLoop периодически проверяет метрики
func (bpm *BackpressureManager) monitorLoop() {
ticker := time.NewTicker(bpm.checkInterval)
defer ticker.Stop()
for range ticker.C {
bpm.updateLevel()
}
}
// updateLevel обновляет уровень перегрузки
func (bpm *BackpressureManager) updateLevel() {
cpu := float64(bpm.currentCPU.Load()) / 100.0
memory := float64(bpm.currentMemory.Load()) / 100.0
queueSize := bpm.currentQueueSize.Load()
connections := bpm.currentConnections.Load()
newLevel := LevelNone
if cpu >= bpm.cpuThreshold || memory >= bpm.memoryThreshold {
newLevel = LevelHigh
} else if queueSize > int64(bpm.queueSizeThreshold) {
if queueSize > int64(bpm.queueSizeThreshold*2) {
newLevel = LevelCritical
} else {
newLevel = LevelMedium
}
} else if connections > int64(bpm.connectionThreshold) {
newLevel = LevelLow
}
bpm.mu.Lock()
oldLevel := bpm.currentLevel
bpm.currentLevel = newLevel
bpm.mu.Unlock()
// Применяем политики в зависимости от уровня
bpm.applyPolicies(newLevel)
if oldLevel != newLevel && bpm.logger != nil {
bpm.logger.Info(fmt.Sprintf("Backpressure level changed from %v to %v (cpu=%.2f%%, mem=%.2f%%, queue=%d, conns=%d)",
bpm.levelToString(oldLevel), bpm.levelToString(newLevel), cpu*100, memory*100, queueSize, connections))
}
}
// applyPolicies применяет политики в зависимости от уровня
func (bpm *BackpressureManager) applyPolicies(level BackpressureLevel) {
bpm.mu.Lock()
defer bpm.mu.Unlock()
switch level {
case LevelNone:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(0)
case LevelLow:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(100) // 100ms задержка
case LevelMedium:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(30) // 30% отклонение
bpm.delayDuration.Store(200)
case LevelHigh:
bpm.writeAllowed = false // Запись запрещена
bpm.readAllowed = true
bpm.rejectProbability.Store(70) // 70% отклонение
bpm.delayDuration.Store(500)
case LevelCritical:
bpm.writeAllowed = false
bpm.readAllowed = true // Только чтение
bpm.rejectProbability.Store(90)
bpm.delayDuration.Store(1000)
}
}
// BeforeRequest вызывается перед обработкой запроса
func (bpm *BackpressureManager) BeforeRequest(isWrite bool) error {
if !bpm.enabled {
return nil
}
bpm.mu.RLock()
level := bpm.currentLevel
writeAllowed := bpm.writeAllowed
readAllowed := bpm.readAllowed
rejectProb := bpm.rejectProbability.Load()
delayDur := bpm.delayDuration.Load()
bpm.mu.RUnlock()
// Проверяем разрешение на операцию
if isWrite && !writeAllowed {
bpm.rejectedCount.Add(1)
return fmt.Errorf("write operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
}
if !isWrite && !readAllowed {
bpm.rejectedCount.Add(1)
return fmt.Errorf("read operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
}
// Вероятностное отклонение
if rejectProb > 0 {
// Простая вероятностная проверка
if uint32(time.Now().UnixNano()%100) < rejectProb {
bpm.rejectedCount.Add(1)
return fmt.Errorf("request rejected due to backpressure (probability: %d%%)", rejectProb)
}
}
// Добавляем задержку если нужно
if delayDur > 0 {
bpm.delayedCount.Add(1)
time.Sleep(time.Duration(delayDur) * time.Millisecond)
}
return nil
}
// AfterRequest вызывается после обработки запроса
func (bpm *BackpressureManager) AfterRequest(duration time.Duration, success bool) {
// Можно использовать для дополнительной статистики
}
// UpdateMetrics обновляет метрики для backpressure
func (bpm *BackpressureManager) UpdateMetrics(cpuPercent, memoryPercent uint64, queueSize, connections int64) {
bpm.currentCPU.Store(cpuPercent)
bpm.currentMemory.Store(memoryPercent)
bpm.currentQueueSize.Store(queueSize)
bpm.currentConnections.Store(connections)
}
// GetCurrentLevel возвращает текущий уровень перегрузки
func (bpm *BackpressureManager) GetCurrentLevel() BackpressureLevel {
bpm.mu.RLock()
defer bpm.mu.RUnlock()
return bpm.currentLevel
}
// GetStats возвращает статистику backpressure
func (bpm *BackpressureManager) GetStats() map[string]interface{} {
bpm.mu.RLock()
defer bpm.mu.RUnlock()
return map[string]interface{}{
"current_level": bpm.levelToString(bpm.currentLevel),
"write_allowed": bpm.writeAllowed,
"read_allowed": bpm.readAllowed,
"reject_probability": bpm.rejectProbability.Load(),
"delay_ms": bpm.delayDuration.Load(),
"rejected_count": bpm.rejectedCount.Load(),
"delayed_count": bpm.delayedCount.Load(),
"cpu_threshold": bpm.cpuThreshold,
"memory_threshold": bpm.memoryThreshold,
"queue_threshold": bpm.queueSizeThreshold,
"conn_threshold": bpm.connectionThreshold,
}
}
func (bpm *BackpressureManager) levelToString(level BackpressureLevel) string {
switch level {
case LevelNone:
return "none"
case LevelLow:
return "low"
case LevelMedium:
return "medium"
case LevelHigh:
return "high"
case LevelCritical:
return "critical"
default:
return "unknown"
}
}

1208
internal/cluster/node.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,370 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/panic_recovery.go
// Назначение: Автоматическое восстановление после паники по всему коду
package cluster
import (
"fmt"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
"time"
)
// PanicInfo содержит информацию о панике
type PanicInfo struct {
ID string
GoroutineID int64
PanicValue interface{}
StackTrace string
Timestamp int64
Recovered bool
RecoveryTime int64
}
// PanicRecoveryManager управляет восстановлением после паник
type PanicRecoveryManager struct {
mu sync.RWMutex
panics map[string]*PanicInfo
maxPanics int
recoveryFuncs map[string]func(interface{}) error
logger LoggerInterface
stopChan chan struct{}
wg sync.WaitGroup
totalPanics atomic.Uint64
recoveredFrom atomic.Uint64
failedRecoveries atomic.Uint64
}
// NewPanicRecoveryManager создаёт новый менеджер восстановления
func NewPanicRecoveryManager(logger LoggerInterface) *PanicRecoveryManager {
prm := &PanicRecoveryManager{
panics: make(map[string]*PanicInfo),
maxPanics: 1000,
recoveryFuncs: make(map[string]func(interface{}) error),
logger: logger,
stopChan: make(chan struct{}),
}
prm.registerDefaultRecoveryFuncs()
// Запускаем периодическую очистку старых паник
prm.wg.Add(1)
go prm.cleanupOldPanics()
if logger != nil {
logger.Info("Panic recovery manager initialized")
}
return prm
}
// getGoroutineID возвращает ID текущей горутины
func getGoroutineID() int64 {
var buf [64]byte
n := runtime.Stack(buf[:], false)
var id int64
fmt.Sscanf(string(buf[:n]), "goroutine %d", &id)
return id
}
// RegisterRecoveryFunc регистрирует функцию восстановления для компонента
func (prm *PanicRecoveryManager) RegisterRecoveryFunc(component string, fn func(interface{}) error) {
prm.mu.Lock()
defer prm.mu.Unlock()
prm.recoveryFuncs[component] = fn
}
// registerDefaultRecoveryFuncs регистрирует стандартные функции восстановления
func (prm *PanicRecoveryManager) registerDefaultRecoveryFuncs() {
prm.recoveryFuncs["TCPServer"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["ConnectionHandler"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["Replication"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["WAL"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["Raft"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["PipelineReplicator"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["BatchCommitManager"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["ReshardingManager"] = func(p interface{}) error {
return nil
}
prm.recoveryFuncs["RecoveryManager"] = func(p interface{}) error {
return nil
}
}
// Recover обрабатывает панику и пытается восстановиться
func (prm *PanicRecoveryManager) Recover(component string, context map[string]interface{}) {
if r := recover(); r != nil {
prm.totalPanics.Add(1)
panicInfo := &PanicInfo{
ID: fmt.Sprintf("panic_%d_%d", time.Now().UnixNano(), prm.totalPanics.Load()),
GoroutineID: getGoroutineID(),
PanicValue: r,
StackTrace: string(debug.Stack()),
Timestamp: time.Now().UnixMilli(),
Recovered: false,
}
prm.mu.Lock()
if len(prm.panics) >= prm.maxPanics {
for k := range prm.panics {
delete(prm.panics, k)
break
}
}
prm.panics[panicInfo.ID] = panicInfo
prm.mu.Unlock()
if prm.logger != nil {
prm.logger.Error(fmt.Sprintf("PANIC in component %s: %v\n%s", component, r, panicInfo.StackTrace))
}
// Пытаемся восстановиться
if recoveryFn, ok := prm.recoveryFuncs[component]; ok {
if err := recoveryFn(r); err != nil {
prm.failedRecoveries.Add(1)
if prm.logger != nil {
prm.logger.Error(fmt.Sprintf("Failed to recover from panic in %s: %v", component, err))
}
} else {
panicInfo.Recovered = true
panicInfo.RecoveryTime = time.Now().UnixMilli()
prm.recoveredFrom.Add(1)
if prm.logger != nil {
prm.logger.Info(fmt.Sprintf("Successfully recovered from panic in %s", component))
}
}
} else {
if prm.logger != nil {
prm.logger.Error(fmt.Sprintf("No recovery function registered for component %s", component))
}
}
}
}
// RecoverWithRetry пытается восстановиться с повторными попытками
func (prm *PanicRecoveryManager) RecoverWithRetry(component string, retries int, fn func() error) error {
var lastErr error
for i := 0; i < retries; i++ {
func() {
defer prm.Recover(component, map[string]interface{}{
"attempt": i + 1,
"retries": retries,
})
if fn != nil {
lastErr = fn()
}
}()
if lastErr == nil {
return nil
}
if i < retries-1 {
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
}
}
return lastErr
}
// SafeGo безопасно запускает горутину с восстановлением
func (prm *PanicRecoveryManager) SafeGo(component string, fn func()) {
go func() {
defer prm.Recover(component, nil)
fn()
}()
}
// SafeGoWithContext безопасно запускает горутину с контекстом
func (prm *PanicRecoveryManager) SafeGoWithContext(component string, ctx map[string]interface{}, fn func()) {
go func() {
defer prm.Recover(component, ctx)
fn()
}()
}
// cleanupOldPanics периодически очищает старые записи о паниках
func (prm *PanicRecoveryManager) cleanupOldPanics() {
defer prm.wg.Done()
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-prm.stopChan:
return
case <-ticker.C:
prm.mu.Lock()
now := time.Now().UnixMilli()
for id, info := range prm.panics {
if now-info.Timestamp > 24*3600*1000 {
delete(prm.panics, id)
}
}
prm.mu.Unlock()
}
}
}
// GetPanicInfo возвращает информацию о панике
func (prm *PanicRecoveryManager) GetPanicInfo(id string) *PanicInfo {
prm.mu.RLock()
defer prm.mu.RUnlock()
return prm.panics[id]
}
// GetAllPanics возвращает все паники
func (prm *PanicRecoveryManager) GetAllPanics() []*PanicInfo {
prm.mu.RLock()
defer prm.mu.RUnlock()
result := make([]*PanicInfo, 0, len(prm.panics))
for _, info := range prm.panics {
result = append(result, info)
}
return result
}
// GetStats возвращает статистику
func (prm *PanicRecoveryManager) GetStats() map[string]interface{} {
return map[string]interface{}{
"total_panics": prm.totalPanics.Load(),
"recovered_from": prm.recoveredFrom.Load(),
"failed_recoveries": prm.failedRecoveries.Load(),
"active_panics": len(prm.panics),
"max_panics": prm.maxPanics,
}
}
// Stop останавливает менеджер
func (prm *PanicRecoveryManager) Stop() {
close(prm.stopChan)
prm.wg.Wait()
}
// RecoverableRoutine обёртка для восстанавливаемых горутин
type RecoverableRoutine struct {
name string
fn func() error
mgr *PanicRecoveryManager
maxRetries int
stopChan chan struct{}
running atomic.Bool
mu sync.Mutex
}
// NewRecoverableRoutine создаёт новую восстанавливаемую горутину
func NewRecoverableRoutine(name string, fn func() error, mgr *PanicRecoveryManager, maxRetries int) *RecoverableRoutine {
return &RecoverableRoutine{
name: name,
fn: fn,
mgr: mgr,
maxRetries: maxRetries,
stopChan: make(chan struct{}),
}
}
// Start запускает горутину с автоматическим восстановлением
func (rr *RecoverableRoutine) Start() {
if !rr.running.CompareAndSwap(false, true) {
return
}
go rr.run()
}
// Stop останавливает горутину
func (rr *RecoverableRoutine) Stop() {
close(rr.stopChan)
rr.running.Store(false)
}
// run запускает основной цикл с восстановлением
func (rr *RecoverableRoutine) run() {
defer rr.running.Store(false)
retries := 0
for {
select {
case <-rr.stopChan:
return
default:
err := rr.runWithRecovery()
if err == nil {
retries = 0
} else {
retries++
if retries > rr.maxRetries {
if rr.mgr.logger != nil {
rr.mgr.logger.Error(fmt.Sprintf("Routine %s exceeded max retries (%d)", rr.name, rr.maxRetries))
}
return
}
backoff := time.Duration(100*retries) * time.Millisecond
if backoff > 5*time.Second {
backoff = 5 * time.Second
}
select {
case <-rr.stopChan:
return
case <-time.After(backoff):
}
}
}
}
}
// runWithRecovery запускает функцию с защитой от паники
func (rr *RecoverableRoutine) runWithRecovery() (err error) {
defer func() {
if r := recover(); r != nil {
rr.mgr.Recover(rr.name, map[string]interface{}{
"routine": rr.name,
})
err = fmt.Errorf("panic recovered: %v", r)
}
}()
if rr.fn != nil {
return rr.fn()
}
return nil
}
// IsRunning возвращает статус работы
func (rr *RecoverableRoutine) IsRunning() bool {
return rr.running.Load()
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,375 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/worker_pool.go
// Назначение: Lock-free пул воркеров для ограничения количества горутин.
// Использует атомарные операции и lock-free структуры данных.
package cluster
import (
"fmt"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"futriis/internal/log"
)
// WorkerPool представляет lock-free пул воркеров для выполнения задач
type WorkerPool struct {
maxWorkers int32 // Максимальное количество воркеров
activeWorkers atomic.Int32 // Текущее количество активных воркеров
tasks *LockFreeQueue // Lock-free очередь задач
stopChan chan struct{}
wg sync.WaitGroup
logger *log.Logger
// Статистика (атомарные счётчики)
submittedTasks atomic.Uint64
completedTasks atomic.Uint64
failedTasks atomic.Uint64
rejectedTasks atomic.Uint64
lastSubmitTime atomic.Int64
lastCompleteTime atomic.Int64
}
// Task представляет задачу для выполнения в пуле
type Task struct {
ID string
Execute func() error
CreatedAt int64
RetryCount int32
}
// LockFreeQueue представляет lock-free очередь на основе CAS операций
type LockFreeQueue struct {
head atomic.Value // *queueNode
tail atomic.Value // *queueNode
size atomic.Int64
}
type queueNode struct {
value *Task
next atomic.Value // *queueNode
}
// NewLockFreeQueue создаёт новую lock-free очередь
func NewLockFreeQueue() *LockFreeQueue {
q := &LockFreeQueue{}
dummy := &queueNode{}
q.head.Store(dummy)
q.tail.Store(dummy)
return q
}
// Enqueue добавляет задачу в очередь (lock-free)
func (q *LockFreeQueue) Enqueue(task *Task) bool {
newNode := &queueNode{value: task}
for {
tailVal := q.tail.Load()
if tailVal == nil {
continue
}
tail := tailVal.(*queueNode)
nextVal := tail.next.Load()
var next *queueNode
if nextVal != nil {
next = nextVal.(*queueNode)
}
if tailVal != q.tail.Load() {
continue
}
if next != nil {
q.tail.CompareAndSwap(tailVal, next)
continue
}
if tail.next.CompareAndSwap(nil, newNode) {
q.tail.CompareAndSwap(tailVal, newNode)
q.size.Add(1)
return true
}
}
}
// Dequeue извлекает задачу из очереди (lock-free)
func (q *LockFreeQueue) Dequeue() *Task {
for {
headVal := q.head.Load()
if headVal == nil {
return nil
}
head := headVal.(*queueNode)
tailVal := q.tail.Load()
if tailVal == nil {
return nil
}
tail := tailVal.(*queueNode)
nextVal := head.next.Load()
var next *queueNode
if nextVal != nil {
next = nextVal.(*queueNode)
}
if headVal != q.head.Load() {
continue
}
if head == tail {
if next == nil {
return nil
}
q.tail.CompareAndSwap(tailVal, next)
continue
}
if next == nil {
return nil
}
task := next.value
if q.head.CompareAndSwap(headVal, next) {
q.size.Add(-1)
return task
}
}
}
// Size возвращает текущий размер очереди (lock-free)
func (q *LockFreeQueue) Size() int64 {
return q.size.Load()
}
// NewWorkerPool создаёт новый lock-free пул воркеров
func NewWorkerPool(maxWorkers int, logger *log.Logger) *WorkerPool {
if maxWorkers <= 0 {
maxWorkers = 500
}
wp := &WorkerPool{
maxWorkers: int32(maxWorkers),
tasks: NewLockFreeQueue(),
stopChan: make(chan struct{}),
logger: logger,
}
// Запускаем диспетчер задач
go wp.dispatcher()
if logger != nil {
logger.Debug(fmt.Sprintf("Lock-free worker pool created: maxWorkers=%d", maxWorkers))
}
return wp
}
// dispatcher управляет воркерами
func (wp *WorkerPool) dispatcher() {
defer func() {
if r := recover(); r != nil {
if wp.logger != nil {
wp.logger.Error(fmt.Sprintf("Worker pool dispatcher panicked: %v\n%s", r, debug.Stack()))
}
// Перезапускаем диспетчер
go wp.dispatcher()
}
}()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-wp.stopChan:
return
case <-ticker.C:
// Динамически регулируем количество воркеров
wp.adjustWorkers()
}
}
}
// adjustWorkers динамически регулирует количество воркеров
func (wp *WorkerPool) adjustWorkers() {
queueSize := wp.tasks.Size()
activeWorkers := wp.activeWorkers.Load()
// Если есть задачи и есть место для новых воркеров
if queueSize > 0 && activeWorkers < wp.maxWorkers {
if activeWorkers == 0 || queueSize > int64(activeWorkers)*2 {
if wp.activeWorkers.CompareAndSwap(activeWorkers, activeWorkers+1) {
wp.wg.Add(1)
go wp.worker()
if wp.logger != nil {
wp.logger.Debug(fmt.Sprintf("Added worker, active: %d/%d", activeWorkers+1, wp.maxWorkers))
}
}
}
}
// Уменьшаем количество воркеров, если нет задач
if queueSize == 0 && activeWorkers > 0 {
if activeWorkers > 1 {
lastComplete := wp.lastCompleteTime.Load()
if time.Now().UnixMilli()-lastComplete > 5000 {
task := &Task{
ID: fmt.Sprintf("stop_worker_%d", time.Now().UnixNano()),
Execute: func() error { return nil },
CreatedAt: time.Now().UnixMilli(),
}
if wp.tasks.Enqueue(task) {
if wp.logger != nil {
wp.logger.Debug(fmt.Sprintf("Stop signal sent to worker, active: %d/%d", activeWorkers-1, wp.maxWorkers))
}
}
}
}
}
}
// worker выполняет задачи из очереди
func (wp *WorkerPool) worker() {
defer func() {
if r := recover(); r != nil {
if wp.logger != nil {
wp.logger.Error(fmt.Sprintf("Worker panicked: %v\n%s", r, debug.Stack()))
}
wp.activeWorkers.Add(-1)
wp.wg.Done()
// Создаём нового воркера вместо упавшего
if wp.activeWorkers.Load() < wp.maxWorkers {
wp.activeWorkers.Add(1)
wp.wg.Add(1)
go wp.worker()
}
}
}()
for {
select {
case <-wp.stopChan:
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
default:
task := wp.tasks.Dequeue()
if task == nil {
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
}
// Проверяем специальную задачу остановки
if len(task.ID) >= 12 && task.ID[:12] == "stop_worker_" {
wp.activeWorkers.Add(-1)
wp.wg.Done()
return
}
// Выполняем задачу
startTime := time.Now().UnixMilli()
err := task.Execute()
duration := time.Now().UnixMilli() - startTime
if err != nil {
wp.failedTasks.Add(1)
if wp.logger != nil && duration > 100 {
wp.logger.Warn(fmt.Sprintf("Task %s failed after %dms: %v", task.ID, duration, err))
}
} else {
wp.completedTasks.Add(1)
wp.lastCompleteTime.Store(time.Now().UnixMilli())
if wp.logger != nil && duration > 1000 {
wp.logger.Debug(fmt.Sprintf("Task %s completed in %dms", task.ID, duration))
}
}
}
}
}
// Submit отправляет задачу в пул
func (wp *WorkerPool) Submit(task *Task) error {
if task.Execute == nil {
return fmt.Errorf("task execute function is nil")
}
task.CreatedAt = time.Now().UnixMilli()
if !wp.tasks.Enqueue(task) {
wp.rejectedTasks.Add(1)
return fmt.Errorf("failed to enqueue task (queue full)")
}
wp.submittedTasks.Add(1)
wp.lastSubmitTime.Store(task.CreatedAt)
// Асинхронно добавляем воркера при необходимости
if wp.activeWorkers.Load() == 0 {
if wp.activeWorkers.CompareAndSwap(0, 1) {
wp.wg.Add(1)
go wp.worker()
}
}
return nil
}
// SubmitFunc отправляет функцию как задачу
func (wp *WorkerPool) SubmitFunc(id string, fn func() error) error {
return wp.Submit(&Task{
ID: id,
Execute: fn,
})
}
// GetStats возвращает статистику пула
func (wp *WorkerPool) GetStats() map[string]interface{} {
return map[string]interface{}{
"max_workers": wp.maxWorkers,
"active_workers": wp.activeWorkers.Load(),
"queue_size": wp.tasks.Size(),
"submitted_tasks": wp.submittedTasks.Load(),
"completed_tasks": wp.completedTasks.Load(),
"failed_tasks": wp.failedTasks.Load(),
"rejected_tasks": wp.rejectedTasks.Load(),
"last_submit_time": wp.lastSubmitTime.Load(),
"last_complete_time": wp.lastCompleteTime.Load(),
}
}
// Stop останавливает пул воркеров
func (wp *WorkerPool) Stop() {
close(wp.stopChan)
done := make(chan struct{})
go func() {
wp.wg.Wait()
close(done)
}()
select {
case <-done:
if wp.logger != nil {
wp.logger.Debug("Worker pool stopped gracefully")
}
case <-time.After(10 * time.Second):
if wp.logger != nil {
wp.logger.Warn("Worker pool stop timeout, forcing shutdown")
}
}
}

View 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)
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/commands/commands.go
// Назначение: Реализация MongoDB-подобных команд CRUD и команд управления кластером.
// Добавлены команды для работы с индексами, ACL, триггерами и ограничениями.
package commands
import (
"futriis/pkg/utils"
)
// ShowHelp отображает справку по всем доступным командам
func ShowHelp() {
helpText := `
=== FUTRIIS DATABASE COMMANDS ===
DATABASE MANAGEMENT:
use <db> - Switch to database
show dbs - List all databases
show collections - List collections in current database
COLLECTION OPERATIONS:
db.createCollection("<name>") - Create new collection
db.<collection>.insert({...}) - Insert document into collection
db.<collection>.find({_id: "..."}) - Find document by ID
db.<collection>.find() - Find all documents in collection
db.<collection>.findByIndex("<index>", "<value>") - Find by secondary index
db.<collection>.update({_id: "..."}, {...}) - Update document
db.<collection>.remove({_id: "..."}) - Delete document
INDEX MANAGEMENT:
db.<collection>.createIndex("<name>", ["field1", "field2"], true|false) - Create index (last param = unique)
db.<collection>.dropIndex("<name>") - Drop index
db.<collection>.listIndexes() - List all indexes
CONSTRAINTS:
db.<collection>.addRequired("<field>") - Add required field constraint
db.<collection>.addUnique("<field>") - Add unique constraint
db.<collection>.addMin("<field>", <value>) - Add minimum value constraint
db.<collection>.addMax("<field>", <value>) - Add maximum value constraint
db.<collection>.addEnum("<field>", [values]) - Add enum constraint
TRIGGERS (MongoDB-like syntax):
db.<collection>.createTrigger("<name>", "<event>", {
condition: { field: "<field>", operator: "<op>", value: <value> },
action: "<action>",
operations: [
{ type: "set", field: "<field>", value: "<value>" },
{ type: "inc", field: "<field>", value: <number> },
{ type: "currentDate", field: "<field>" }
]
})
Events: BEFORE_INSERT, AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE
Actions: abort (cancel operation), skip (skip operation), modify (modify document), log (write to log), notify (send notification)
Special values: $$NOW (current timestamp), $$USER (current user), $$ROLE (current role)
db.<collection>.dropTrigger("<name>") - Drop trigger
db.<collection>.listTriggers() - List all triggers on collection
db.<collection>.enableTrigger("<name>") - Enable trigger
db.<collection>.disableTrigger("<name>") - Disable trigger
db.getTriggerLog() - Show trigger execution log
TRIGGER EXAMPLES:
// Auto-set updated_at timestamp on every update
db.users.createTrigger("update_timestamp", "BEFORE_UPDATE", {
action: "modify",
operations: [{ type: "set", field: "updated_at", value: "$$NOW" }]
})
// Prevent deletion of active users
db.users.createTrigger("protect_active", "BEFORE_DELETE", {
condition: { field: "status", operator: "eq", value: "active" },
action: "abort"
})
// Log all inserts
db.orders.createTrigger("audit_log", "AFTER_INSERT", {
action: "log",
description: "Log all order creations"
})
// Increment counter on document insert
db.stats.createTrigger("inc_counter", "AFTER_INSERT", {
action: "modify",
operations: [{ type: "inc", field: "counter", value: 1 }]
})
ACL MANAGEMENT:
acl createUser "<username>" "<password>" [roles] - Create new user
acl createRole "<rolename>" - Create new role
acl grant "<rolename>" "<permission>" - Grant permission to role
acl addUserRole "<username>" "<rolename>" - Add role to user
acl login "<username>" "<password>" - Login (returns session token)
acl logout - Logout current session
acl listUsers - List all users
acl listRoles - List all roles
TRANSACTIONS (MongoDB-like syntax):
session = db.startSession() - Start a new session
session.startTransaction() - Begin a transaction
session.commitTransaction() - Commit current transaction
session.abortTransaction() - Abort/Rollback current transaction
EXPORT/IMPORT (MessagePack format):
export "database_name" "filename.msgpack" - Export entire database
import "database_name" "filename.msgpack" - Import database from .msgpack file
CLUSTER MANAGEMENT:
cluster status - Show cluster status
cluster nodes - List all cluster nodes
cluster add <ip> <port> - Add node to cluster
cluster remove <node_id> - Remove node from cluster
cluster sync <db> <coll> - Sync collection across cluster
cluster replication-factor [n] - Get or set replication factor
cluster leader - Show cluster leader
cluster health - Check cluster health
HTTP API:
The database also exposes HTTP RESTful API on port 8080 (configurable)
See documentation for endpoints: /api/db/, /api/index/, /api/acl/, /api/constraint/, /api/trigger/
UTILITIES:
help - Show this help message
exit / quit - Exit database
`
utils.Println(helpText)
}

348
internal/commands/crud.go Normal file
View File

@@ -0,0 +1,348 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/commands/crud.go
// Назначение: Парсинг и выполнение CRUD-команд для работы с документами,
// коллекциями и базами данных c добавлением аудита. Поддерживает MongoDB-подобный синтаксис.
package commands
import (
"fmt"
"strings"
"futriis/internal/storage"
"futriis/internal/cluster"
"futriis/pkg/utils"
)
// Execute выполняет команду CRUD
func Execute(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
// Простейший парсинг для демонстрации
if strings.HasPrefix(cmd, "use ") {
dbName := strings.TrimPrefix(cmd, "use ")
if err := store.CreateDatabase(dbName); err != nil && err.Error() != "database already exists" {
return err
}
storage.AuditDatabaseOperation("USE", dbName)
return nil
}
if cmd == "show dbs" {
return showDatabases(store)
}
if cmd == "show collections" {
return showCollections(store)
}
if strings.HasPrefix(cmd, "db.") {
return executeDatabaseCommand(store, coord, cmd)
}
return fmt.Errorf("%s", utils.ColorizeText("unknown command: "+cmd, "\033[31m"))
}
// ExecuteTransaction выполняет команды транзакций MongoDB-подобного синтаксиса
func ExecuteTransaction(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
if strings.Contains(cmd, "startSession()") {
if err := storage.InitTransactionManager("futriis.wal"); err != nil {
return err
}
utils.Println("Session started")
storage.LogAudit("START", "SESSION", "global", map[string]interface{}{"action": "start_session"})
return nil
}
if strings.Contains(cmd, "startTransaction()") {
_ = storage.BeginTransaction()
utils.Println("Transaction started")
storage.LogAudit("START", "TRANSACTION", "current", map[string]interface{}{"action": "begin_transaction"})
return nil
}
if strings.Contains(cmd, "commitTransaction()") {
if err := storage.CommitCurrentTransaction(); err != nil {
return err
}
utils.Println("Transaction committed successfully")
storage.LogAudit("COMMIT", "TRANSACTION", "current", map[string]interface{}{"action": "commit_transaction"})
return nil
}
if strings.Contains(cmd, "abortTransaction()") {
if err := storage.AbortCurrentTransaction(); err != nil {
return err
}
utils.Println("Transaction aborted")
storage.LogAudit("ABORT", "TRANSACTION", "current", map[string]interface{}{"action": "abort_transaction"})
return nil
}
return fmt.Errorf("%s", utils.ColorizeText("unknown transaction command: "+cmd, "\033[31m"))
}
// showDatabases отображает список всех баз данных
func showDatabases(store *storage.Storage) error {
databases := store.ListDatabases()
if len(databases) == 0 {
utils.Println("No databases found")
return nil
}
utils.Println("\nDatabases:")
for _, db := range databases {
utils.Println(" - " + db)
}
return nil
}
// showCollections отображает список коллекций в текущей базе данных
func showCollections(store *storage.Storage) error {
databases := store.ListDatabases()
if len(databases) == 0 {
utils.Println("No databases found")
return nil
}
db, err := store.GetDatabase(databases[0])
if err != nil {
return err
}
collections := db.ListCollections()
if len(collections) == 0 {
utils.Println("No collections found")
return nil
}
utils.Println("\nCollections in database '" + databases[0] + "':")
for _, coll := range collections {
utils.Println(" - " + coll)
}
return nil
}
// executeDatabaseCommand выполняет команду вида db.<collection>.<operation>()
func executeDatabaseCommand(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
parts := strings.SplitN(cmd, ".", 3)
if len(parts) < 3 {
return fmt.Errorf("%s", utils.ColorizeText("invalid database command format", "\033[31m"))
}
collectionPart := parts[1]
operationPart := parts[2]
var collectionName, operation string
if strings.Contains(collectionPart, ".") {
collParts := strings.SplitN(collectionPart, ".", 2)
collectionName = collParts[0]
operation = collParts[1]
} else {
collectionName = collectionPart
opParts := strings.SplitN(operationPart, "(", 2)
if len(opParts) < 1 {
return fmt.Errorf("%s", utils.ColorizeText("invalid operation format", "\033[31m"))
}
operation = opParts[0]
}
databases := store.ListDatabases()
if len(databases) == 0 {
if err := store.CreateDatabase("test"); err != nil {
return err
}
storage.AuditDatabaseOperation("CREATE", "test")
databases = store.ListDatabases()
}
db, err := store.GetDatabase(databases[0])
if err != nil {
return err
}
coll, err := db.GetCollection(collectionName)
if err != nil {
if err := db.CreateCollection(collectionName); err != nil {
return err
}
storage.AuditCollectionOperation("CREATE", databases[0], collectionName, nil)
coll, _ = db.GetCollection(collectionName)
}
switch operation {
case "insert", "insertOne":
return executeInsertWithTransaction(coll, operationPart, databases[0], collectionName)
case "find", "findOne":
return executeFindWithTransaction(coll, operationPart)
case "update", "updateOne":
return executeUpdateWithTransaction(coll, operationPart, databases[0], collectionName)
case "remove", "delete", "deleteOne":
return executeDeleteWithTransaction(coll, operationPart, databases[0], collectionName)
default:
return fmt.Errorf("%s", utils.ColorizeText("unknown operation: "+operation, "\033[31m"))
}
}
func executeInsertWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
start := strings.Index(operationPart, "(")
end := strings.LastIndex(operationPart, ")")
if start == -1 || end == -1 {
return fmt.Errorf("%s", utils.ColorizeText("invalid insert syntax", "\033[31m"))
}
dataStr := operationPart[start+1 : end]
dataStr = strings.TrimSpace(dataStr)
if dataStr == "" || dataStr == "{}" {
return fmt.Errorf("%s", utils.ColorizeText("empty document", "\033[31m"))
}
doc := storage.NewDocument()
dataStr = strings.Trim(dataStr, "{}")
if dataStr != "" {
fields := strings.Split(dataStr, ",")
for _, field := range fields {
field = strings.TrimSpace(field)
if field == "" {
continue
}
parts := strings.SplitN(field, ":", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
value = strings.Trim(value, "\"'")
doc.SetField(key, value)
}
}
}
if storage.HasActiveTransaction() {
// ИСПРАВЛЕНО: передаём только coll, opType, doc (сигнатура из transaction.go)
if err := storage.AddToTransaction(coll, "insert", doc); err != nil {
return err
}
utils.Println("Document staged for transaction")
return nil
}
if err := coll.Insert(doc); err != nil {
return err
}
// Аудит операции вставки документа
storage.AuditDocumentOperation("INSERT", dbName, collName, doc.ID, doc.GetFields())
utils.Println("Inserted document with _id: " + doc.ID)
return nil
}
func executeFindWithTransaction(coll *storage.Collection, operationPart string) error {
start := strings.Index(operationPart, "{_id:")
if start == -1 {
docs := coll.GetAllDocuments()
if len(docs) == 0 {
utils.Println("No documents found")
return nil
}
utils.Println("\nFound " + utils.ColorizeTextInt(len(docs)) + " documents:")
for _, doc := range docs {
utils.Println(" _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
}
return nil
}
end := strings.Index(operationPart[start:], "}")
if end == -1 {
return fmt.Errorf("%s", utils.ColorizeText("invalid find syntax", "\033[31m"))
}
idPart := operationPart[start+5 : start+end]
idPart = strings.TrimSpace(idPart)
idPart = strings.Trim(idPart, "\"'")
if storage.HasActiveTransaction() {
doc, err := storage.FindInTransaction(coll, idPart)
if err != nil {
return err
}
utils.Println("Found document (in transaction): _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
return nil
}
doc, err := coll.Find(idPart)
if err != nil {
return err
}
utils.Println("Found document: _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
return nil
}
func executeUpdateWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
if storage.HasActiveTransaction() {
utils.Println("Update operation staged for transaction")
storage.LogAudit("STAGE", "UPDATE", collName, map[string]interface{}{"database": dbName})
return nil
}
// Извлечение ID из строки обновления
start := strings.Index(operationPart, "{_id:")
if start == -1 {
return fmt.Errorf("%s", utils.ColorizeText("update requires _id filter", "\033[31m"))
}
end := strings.Index(operationPart[start:], "}")
if end == -1 {
return fmt.Errorf("%s", utils.ColorizeText("invalid update syntax", "\033[31m"))
}
idPart := operationPart[start+5 : start+end]
idPart = strings.TrimSpace(idPart)
idPart = strings.Trim(idPart, "\"'")
storage.AuditDocumentOperation("UPDATE", dbName, collName, idPart, nil)
utils.Println("Update operation - to be implemented")
return nil
}
func executeDeleteWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
if storage.HasActiveTransaction() {
utils.Println("Delete operation staged for transaction")
storage.LogAudit("STAGE", "DELETE", collName, map[string]interface{}{"database": dbName})
return nil
}
// Извлечение ID из строки удаления
start := strings.Index(operationPart, "{_id:")
if start == -1 {
return fmt.Errorf("%s", utils.ColorizeText("delete requires _id filter", "\033[31m"))
}
end := strings.Index(operationPart[start:], "}")
if end == -1 {
return fmt.Errorf("%s", utils.ColorizeText("invalid delete syntax", "\033[31m"))
}
idPart := operationPart[start+5 : start+end]
idPart = strings.TrimSpace(idPart)
idPart = strings.Trim(idPart, "\"'")
if err := coll.Delete(idPart); err != nil {
return err
}
storage.AuditDocumentOperation("DELETE", dbName, collName, idPart, nil)
utils.Println("Delete operation - to be implemented")
return nil
}

View 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)
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/compression/compression.go
// Назначение: Реализация сжатия данных с использованием различных алгоритмов.
// Поддерживаемый алгоритм: Brotli.
// Обеспечивает прозрачное сжатие/распаковку для документов.
// LZ4 был удалён в пользу Brotli для лучшего версионирования.
package compression
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/golang/snappy"
"github.com/klauspost/compress/zstd"
"github.com/andybalholm/brotli"
)
// Config представляет конфигурацию сжатия
type Config struct {
Enabled bool // Включено ли сжатие
Algorithm string // Алгоритм сжатия: snappy, brotli, zstd
Level int // Уровень сжатия (1-9)
MinSize int // Минимальный размер для сжатия (байт)
}
// MagicNumber используется для идентификации сжатых данных
var MagicNumber = []byte{0x46, 0x54, 0x52, 0x53} // "FTRS" - Futriis
// CompressionType определяет тип сжатия
type CompressionType byte
const (
CompressionNone CompressionType = 0x00
CompressionSnappy CompressionType = 0x01
CompressionBrotli CompressionType = 0x02
CompressionZstd CompressionType = 0x03
)
// Compress сжимает данные с использованием указанного алгоритма
func Compress(data []byte, config *Config) ([]byte, error) {
if !config.Enabled {
return data, nil
}
if len(data) < config.MinSize {
return data, nil
}
var compressed []byte
var err error
var compType CompressionType
switch config.Algorithm {
case "snappy":
compressed = snappy.Encode(nil, data)
compType = CompressionSnappy
case "brotli":
buf := bytes.NewBuffer(nil)
writer := brotli.NewWriter(buf)
// Устанавливаем уровень сжатия для Brotli
// Brotli использует качество от 0 до 11, где 11 - максимальное сжатие
quality := config.Level
if quality < 0 {
quality = 4 // стандартное качество по умолчанию
}
if quality > 11 {
quality = 11
}
// Brotli Writer не имеет прямой установки качества в этой библиотеке,
// используем стандартные настройки
if _, err := writer.Write(data); err != nil {
return nil, fmt.Errorf("brotli write failed: %v", err)
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf("brotli close failed: %v", err)
}
compressed = buf.Bytes()
compType = CompressionBrotli
case "zstd":
// Для Zstandard используем предустановленные уровни скорости
var encoder *zstd.Encoder
var encoderLevel zstd.EncoderLevel
// Выбираем уровень сжатия на основе config.Level
switch {
case config.Level <= 1:
encoderLevel = zstd.SpeedFastest
case config.Level <= 3:
encoderLevel = zstd.SpeedDefault
case config.Level <= 6:
encoderLevel = zstd.SpeedBetterCompression
default:
encoderLevel = zstd.SpeedBestCompression
}
// Создаём энкодер с выбранным уровнем
encoder, err = zstd.NewWriter(nil, zstd.WithEncoderLevel(encoderLevel))
if err != nil {
return nil, fmt.Errorf("failed to create zstd encoder: %v", err)
}
defer encoder.Close()
compressed = encoder.EncodeAll(data, nil)
compType = CompressionZstd
default:
return nil, fmt.Errorf("unsupported compression algorithm: %s", config.Algorithm)
}
// Проверяем, что сжатие действительно уменьшило размер
if len(compressed) >= len(data) {
return data, nil
}
// Добавляем заголовок: магическое число (4 байта) + тип сжатия (1 байт) + оригинальный размер (8 байт)
header := make([]byte, 4+1+8)
copy(header[0:4], MagicNumber)
header[4] = byte(compType)
binary.LittleEndian.PutUint64(header[5:], uint64(len(data)))
result := make([]byte, 0, len(header)+len(compressed))
result = append(result, header...)
result = append(result, compressed...)
return result, nil
}
// Decompress распаковывает данные
func Decompress(data []byte) ([]byte, error) {
// Проверяем наличие магического числа
if len(data) < 4+1+8 {
return nil, fmt.Errorf("data too short for compressed format")
}
// Проверяем магическое число
if !bytes.Equal(data[0:4], MagicNumber) {
return nil, fmt.Errorf("invalid magic number")
}
compType := CompressionType(data[4])
originalSize := binary.LittleEndian.Uint64(data[5:13])
compressedData := data[13:]
if originalSize == 0 {
return nil, fmt.Errorf("invalid original size")
}
var decompressed []byte
var err error
switch compType {
case CompressionSnappy:
decompressed, err = snappy.Decode(nil, compressedData)
if err != nil {
return nil, fmt.Errorf("snappy decode failed: %v", err)
}
case CompressionBrotli:
reader := brotli.NewReader(bytes.NewReader(compressedData))
buf := bytes.NewBuffer(nil)
_, err = buf.ReadFrom(reader)
if err != nil {
return nil, fmt.Errorf("brotli decode failed: %v", err)
}
decompressed = buf.Bytes()
case CompressionZstd:
decoder, err := zstd.NewReader(nil)
if err != nil {
return nil, fmt.Errorf("failed to create zstd decoder: %v", err)
}
defer decoder.Close()
decompressed, err = decoder.DecodeAll(compressedData, nil)
if err != nil {
return nil, fmt.Errorf("zstd decode failed: %v", err)
}
case CompressionNone:
return compressedData, nil
default:
return nil, fmt.Errorf("unsupported compression type: %d", compType)
}
return decompressed, nil
}
// DecompressAuto автоматически определяет, сжаты ли данные, и распаковывает при необходимости
func DecompressAuto(data []byte) ([]byte, error) {
// Проверяем, есть ли магическое число (признак сжатых данных)
if len(data) >= 4 && bytes.Equal(data[0:4], MagicNumber) {
return Decompress(data)
}
return data, nil
}
// IsCompressed проверяет, сжаты ли данные
func IsCompressed(data []byte) bool {
if len(data) < 4 {
return false
}
return bytes.Equal(data[0:4], MagicNumber)
}
// GetCompressionType возвращает тип сжатия данных
func GetCompressionType(data []byte) CompressionType {
if !IsCompressed(data) || len(data) < 5 {
return CompressionNone
}
return CompressionType(data[4])
}
// GetCompressionRatio возвращает коэффициент сжатия
func GetCompressionRatio(original, compressed []byte) float64 {
if len(original) == 0 {
return 1.0
}
return float64(len(compressed)) / float64(len(original))
}

1523
internal/config/config.go Normal file

File diff suppressed because it is too large Load Diff

523
internal/log/logger.go Normal file
View File

@@ -0,0 +1,523 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
// Файл: internal/log/logger.go
// Назначение: Асинхронный логгер с поддержкой уровней логирования (DEBUG, INFO, WARN, ERROR, FATAL),
// ротации файлов, structured logging в JSON или текстовом формате, и глобальными функциями
// для удобного логирования из любого места приложения без создания экземпляра логгера.
*/
package log
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
)
// LogLevel представляет уровень логирования
type LogLevel int32
const (
DebugLevel LogLevel = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
// String возвращает строковое представление уровня
func (l LogLevel) String() string {
switch l {
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
case FatalLevel:
return "FATAL"
default:
return "UNKNOWN"
}
}
// ParseLogLevel парсит уровень логирования из строки
func ParseLogLevel(levelStr string) LogLevel {
switch levelStr {
case "debug":
return DebugLevel
case "info":
return InfoLevel
case "warn", "warning":
return WarnLevel
case "error":
return ErrorLevel
case "fatal":
return FatalLevel
default:
return InfoLevel
}
}
// LogEntry представляет одну запись в логе
type LogEntry struct {
Timestamp int64 `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Source string `json:"source,omitempty"`
Line int `json:"line,omitempty"`
Function string `json:"function,omitempty"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
// Logger представляет асинхронный логгер с поддержкой уровней
type Logger struct {
file *os.File
level atomic.Int32
writeChan chan LogEntry
done chan struct{}
mu sync.Mutex
path string
maxSize int64
currentSize int64
rotateCount int
formatter LogFormatter
}
// LogFormatter определяет интерфейс для форматирования логов
type LogFormatter interface {
Format(entry LogEntry) string
}
// TextFormatter форматирует логи в текстовом виде
type TextFormatter struct {
includeTimestamp bool
includeLevel bool
includeSource bool
}
// NewTextFormatter создаёт новый текстовый форматтер
func NewTextFormatter(includeTimestamp, includeLevel, includeSource bool) *TextFormatter {
return &TextFormatter{
includeTimestamp: includeTimestamp,
includeLevel: includeLevel,
includeSource: includeSource,
}
}
// Format форматирует запись лога
func (f *TextFormatter) Format(entry LogEntry) string {
var parts []string
if f.includeTimestamp {
parts = append(parts, time.UnixMilli(entry.Timestamp).Format("2006-01-02 15:04:05.000"))
}
if f.includeLevel {
parts = append(parts, fmt.Sprintf("[%s]", entry.Level))
}
if f.includeSource && entry.Source != "" {
parts = append(parts, fmt.Sprintf("[%s:%d]", entry.Source, entry.Line))
}
parts = append(parts, entry.Message)
if len(entry.Fields) > 0 {
parts = append(parts, fmt.Sprintf("%v", entry.Fields))
}
return fmt.Sprintf("%s", parts)
}
// JSONFormatter форматирует логи в JSON
type JSONFormatter struct {
pretty bool
}
// NewJSONFormatter создаёт новый JSON форматтер
func NewJSONFormatter(pretty bool) *JSONFormatter {
return &JSONFormatter{pretty: pretty}
}
// Format форматирует запись лога в JSON
func (f *JSONFormatter) Format(entry LogEntry) string {
type jsonLog struct {
Timestamp int64 `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Source string `json:"source,omitempty"`
Line int `json:"line,omitempty"`
Function string `json:"function,omitempty"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
logData := jsonLog{
Timestamp: entry.Timestamp,
Level: entry.Level,
Message: entry.Message,
Source: entry.Source,
Line: entry.Line,
Function: entry.Function,
Fields: entry.Fields,
}
if f.pretty {
data, _ := json.MarshalIndent(logData, "", " ")
return string(data)
}
data, _ := json.Marshal(logData)
return string(data)
}
// NewLogger создаёт новый экземпляр логгера
func NewLogger(filename string, levelStr string) (*Logger, error) {
dir := filepath.Dir(filename)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %v", err)
}
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
stat, _ := file.Stat()
level := ParseLogLevel(levelStr)
l := &Logger{
file: file,
writeChan: make(chan LogEntry, 50000),
done: make(chan struct{}),
path: filename,
maxSize: 100 * 1024 * 1024, // 100MB
currentSize: stat.Size(),
rotateCount: 10,
formatter: NewTextFormatter(true, true, false),
}
l.level.Store(int32(level))
go l.writerLoop()
return l, nil
}
// SetFormatter устанавливает форматтер
func (l *Logger) SetFormatter(formatter LogFormatter) {
l.mu.Lock()
defer l.mu.Unlock()
l.formatter = formatter
}
// writerLoop обрабатывает запись логов в файл
func (l *Logger) writerLoop() {
for entry := range l.writeChan {
l.writeEntry(entry)
}
close(l.done)
}
// writeEntry записывает одну запись в файл
func (l *Logger) writeEntry(entry LogEntry) {
l.mu.Lock()
defer l.mu.Unlock()
// Проверка ротации
if l.currentSize >= l.maxSize {
l.rotate()
}
formatted := l.formatter.Format(entry)
data := []byte(formatted + "\n")
if _, err := l.file.Write(data); err != nil {
// Не можем залогировать ошибку, так как это приведёт к рекурсии
return
}
l.currentSize += int64(len(data))
}
// rotate выполняет ротацию лог-файла
func (l *Logger) rotate() {
l.file.Sync()
l.file.Close()
// Переименовываем существующий файл
for i := l.rotateCount - 1; i >= 0; i-- {
oldName := fmt.Sprintf("%s.%d", l.path, i)
newName := fmt.Sprintf("%s.%d", l.path, i+1)
if i == 0 {
oldName = l.path
}
if _, err := os.Stat(oldName); err == nil {
os.Rename(oldName, newName)
}
}
// Создаём новый файл
file, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return
}
l.file = file
l.currentSize = 0
}
// log внутренний метод для записи лога
func (l *Logger) log(level LogLevel, msg string, fields map[string]interface{}) {
if level < LogLevel(l.level.Load()) {
return
}
entry := LogEntry{
Timestamp: time.Now().UnixMilli(),
Level: level.String(),
Message: msg,
Fields: fields,
}
select {
case l.writeChan <- entry:
default:
// Неблокирующая запись, старый лог теряется - wait-free
}
}
// Debug записывает DEBUG сообщение
func (l *Logger) Debug(msg string) {
l.log(DebugLevel, msg, nil)
}
// Debugf записывает форматированное DEBUG сообщение
func (l *Logger) Debugf(format string, args ...interface{}) {
l.log(DebugLevel, fmt.Sprintf(format, args...), nil)
}
// DebugWithFields записывает DEBUG сообщение с полями
func (l *Logger) DebugWithFields(msg string, fields map[string]interface{}) {
l.log(DebugLevel, msg, fields)
}
// Info записывает INFO сообщение
func (l *Logger) Info(msg string) {
l.log(InfoLevel, msg, nil)
}
// Infof записывает форматированное INFO сообщение
func (l *Logger) Infof(format string, args ...interface{}) {
l.log(InfoLevel, fmt.Sprintf(format, args...), nil)
}
// InfoWithFields записывает INFO сообщение с полями
func (l *Logger) InfoWithFields(msg string, fields map[string]interface{}) {
l.log(InfoLevel, msg, fields)
}
// Warn записывает WARN сообщение
func (l *Logger) Warn(msg string) {
l.log(WarnLevel, msg, nil)
}
// Warnf записывает форматированное WARN сообщение
func (l *Logger) Warnf(format string, args ...interface{}) {
l.log(WarnLevel, fmt.Sprintf(format, args...), nil)
}
// WarnWithFields записывает WARN сообщение с полями
func (l *Logger) WarnWithFields(msg string, fields map[string]interface{}) {
l.log(WarnLevel, msg, fields)
}
// Error записывает ERROR сообщение
func (l *Logger) Error(msg string) {
l.log(ErrorLevel, msg, nil)
}
// Errorf записывает форматированное ERROR сообщение
func (l *Logger) Errorf(format string, args ...interface{}) {
l.log(ErrorLevel, fmt.Sprintf(format, args...), nil)
}
// ErrorWithFields записывает ERROR сообщение с полями
func (l *Logger) ErrorWithFields(msg string, fields map[string]interface{}) {
l.log(ErrorLevel, msg, fields)
}
// Fatal записывает FATAL сообщение и завершает программу
func (l *Logger) Fatal(msg string) {
l.log(FatalLevel, msg, nil)
l.Close()
os.Exit(1)
}
// Fatalf записывает форматированное FATAL сообщение и завершает программу
func (l *Logger) Fatalf(format string, args ...interface{}) {
l.log(FatalLevel, fmt.Sprintf(format, args...), nil)
l.Close()
os.Exit(1)
}
// FatalWithFields записывает FATAL сообщение с полями и завершает программу
func (l *Logger) FatalWithFields(msg string, fields map[string]interface{}) {
l.log(FatalLevel, msg, fields)
l.Close()
os.Exit(1)
}
// SetLevel устанавливает уровень логирования
func (l *Logger) SetLevel(level LogLevel) {
l.level.Store(int32(level))
}
// GetLevel возвращает текущий уровень логирования
func (l *Logger) GetLevel() LogLevel {
return LogLevel(l.level.Load())
}
// Sync синхронизирует лог с диском
func (l *Logger) Sync() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.file != nil {
return l.file.Sync()
}
return nil
}
// Close закрывает логгер
func (l *Logger) Close() {
close(l.writeChan)
<-l.done
l.mu.Lock()
defer l.mu.Unlock()
if l.file != nil {
l.file.Sync()
l.file.Close()
}
}
// ========== Глобальные функции для удобства ==========
var defaultLogger *Logger
var once sync.Once
// InitDefaultLogger инициализирует глобальный логгер
func InitDefaultLogger(filename string, level string) error {
var err error
once.Do(func() {
defaultLogger, err = NewLogger(filename, level)
})
return err
}
// GetDefaultLogger возвращает глобальный логгер
func GetDefaultLogger() *Logger {
return defaultLogger
}
// SetDefaultLogger устанавливает глобальный логгер
func SetDefaultLogger(logger *Logger) {
defaultLogger = logger
}
// Debug глобальная функция DEBUG
func Debug(msg string) {
if defaultLogger != nil {
defaultLogger.Debug(msg)
}
}
// Info глобальная функция INFO
func Info(msg string) {
if defaultLogger != nil {
defaultLogger.Info(msg)
}
}
// Warn глобальная функция WARN
func Warn(msg string) {
if defaultLogger != nil {
defaultLogger.Warn(msg)
}
}
// Error глобальная функция ERROR
func Error(msg string) {
if defaultLogger != nil {
defaultLogger.Error(msg)
}
}
// Debugf глобальная функция форматированного DEBUG
func Debugf(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.Debugf(format, args...)
}
}
// Infof глобальная функция форматированного INFO
func Infof(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.Infof(format, args...)
}
}
// Warnf глобальная функция форматированного WARN
func Warnf(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.Warnf(format, args...)
}
}
// Errorf глобальная функция форматированного ERROR
func Errorf(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.Errorf(format, args...)
}
}
// DebugWithFields глобальная функция DEBUG с полями
func DebugWithFields(msg string, fields map[string]interface{}) {
if defaultLogger != nil {
defaultLogger.DebugWithFields(msg, fields)
}
}
// InfoWithFields глобальная функция INFO с полями
func InfoWithFields(msg string, fields map[string]interface{}) {
if defaultLogger != nil {
defaultLogger.InfoWithFields(msg, fields)
}
}
// WarnWithFields глобальная функция WARN с полями
func WarnWithFields(msg string, fields map[string]interface{}) {
if defaultLogger != nil {
defaultLogger.WarnWithFields(msg, fields)
}
}
// ErrorWithFields глобальная функция ERROR с полями
func ErrorWithFields(msg string, fields map[string]interface{}) {
if defaultLogger != nil {
defaultLogger.ErrorWithFields(msg, fields)
}
}

File diff suppressed because it is too large Load Diff

3348
internal/plugin/plugin.go Normal file

File diff suppressed because it is too large Load Diff

106
internal/repl/history.go Normal file
View File

@@ -0,0 +1,106 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/repl/history.go
// Назначение: Управление историей команд REPL
package repl
import (
"bufio"
"os"
"path/filepath"
)
// History управляет историей команд
type History struct {
entries []string
maxSize int
filePath string
}
// NewHistory создаёт новый объект истории
func NewHistory(maxSize int) *History {
homeDir, _ := os.UserHomeDir()
filePath := filepath.Join(homeDir, ".futriis_history")
return &History{
entries: make([]string, 0, maxSize),
maxSize: maxSize,
filePath: filePath,
}
}
// Add добавляет команду в историю
func (h *History) Add(cmd string) error {
// Не добавляем дубликаты подряд
if len(h.entries) > 0 && h.entries[len(h.entries)-1] == cmd {
return nil
}
h.entries = append(h.entries, cmd)
// Ограничиваем размер истории
if len(h.entries) > h.maxSize {
h.entries = h.entries[len(h.entries)-h.maxSize:]
}
return nil
}
// Load загружает историю из файла
func (h *History) Load() error {
file, err := os.Open(h.filePath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
cmd := scanner.Text()
if cmd != "" {
h.entries = append(h.entries, cmd)
}
}
// Ограничиваем размер
if len(h.entries) > h.maxSize {
h.entries = h.entries[len(h.entries)-h.maxSize:]
}
return scanner.Err()
}
// Save сохраняет историю в файл
func (h *History) Save() error {
file, err := os.Create(h.filePath)
if err != nil {
return err
}
defer file.Close()
writer := bufio.NewWriter(file)
for _, cmd := range h.entries {
if _, err := writer.WriteString(cmd + "\n"); err != nil {
return err
}
}
return writer.Flush()
}
// GetEntries возвращает все записи истории
func (h *History) GetEntries() []string {
return h.entries
}

2553
internal/repl/repl.go Normal file

File diff suppressed because it is too large Load Diff

View 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
View 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,
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,634 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/storage/document.go
// Назначение: Определение структуры документа, его методов для работы
// с полями, кортежами (вложенными документами) и сериализации в MessagePack.
// Документ является основной единицей хранения в СУБД futriis.
// Lock-free: Document.fields переведён на atomic.Value для wait-free доступа.
package storage
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"futriis/internal/compression"
"futriis/internal/serializer"
"github.com/google/uuid"
)
// Document представляет документ в коллекции (аналог строки в реляционной СУБД)
type Document struct {
ID string `msgpack:"_id"` // Уникальный идентификатор документа
fieldsPtr atomic.Value // map[string]interface{} - lock-free хранилище полей
CreatedAt int64 `msgpack:"created_at"` // Время создания (Unix миллисекунды)
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления
DeletedAt int64 `msgpack:"deleted_at"` // Время удаления (Unix миллисекунды, 0 = не удалён)
Version uint64 `msgpack:"version"` // Версия документа (для оптимистичных блокировок)
Compressed bool `msgpack:"compressed"` // Флаг, сжат ли документ
OriginalSize int64 `msgpack:"original_size"` // Оригинальный размер до сжатия
}
// Tuple представляет вложенный документ (аналог кортежа в реляционной СУБД)
type Tuple struct {
Fields map[string]interface{} `msgpack:"fields"`
CreatedAt int64 `msgpack:"created_at"` // Время создания кортежа
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления кортежа
mu sync.RWMutex
}
// Field представляет отдельное поле документа (аналог колонки)
type Field struct {
Name string `msgpack:"name"`
Type FieldType `msgpack:"type"`
Value interface{} `msgpack:"value"`
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления поля
}
// FieldType определяет тип поля документа
type FieldType int
const (
TypeString FieldType = iota
TypeNumber
TypeBoolean
TypeTuple // Вложенный документ
TypeArray
TypeNull
)
// NewDocument создаёт новый документ с автоматической генерацией ID
func NewDocument() *Document {
now := time.Now().UnixMilli()
d := &Document{
ID: uuid.New().String(),
CreatedAt: now,
UpdatedAt: now,
DeletedAt: 0,
Version: 1,
Compressed: false,
OriginalSize: 0,
}
d.fieldsPtr.Store(make(map[string]interface{}))
return d
}
// NewDocumentWithID создаёт документ с указанным ID
func NewDocumentWithID(id string) *Document {
now := time.Now().UnixMilli()
d := &Document{
ID: id,
CreatedAt: now,
UpdatedAt: now,
DeletedAt: 0,
Version: 1,
Compressed: false,
OriginalSize: 0,
}
d.fieldsPtr.Store(make(map[string]interface{}))
return d
}
// loadFields загружает карту полей (lock-free)
func (d *Document) loadFields() map[string]interface{} {
val := d.fieldsPtr.Load()
if val == nil {
return make(map[string]interface{})
}
return val.(map[string]interface{})
}
// storeFields сохраняет карту полей (lock-free)
func (d *Document) storeFields(newMap map[string]interface{}) {
d.fieldsPtr.Store(newMap)
}
// SetField устанавливает значение поля документа (lock-free)
func (d *Document) SetField(name string, value interface{}) {
for {
oldFields := d.loadFields()
newFields := make(map[string]interface{})
for k, v := range oldFields {
newFields[k] = v
}
newFields[name] = value
if d.compareAndSwapFields(oldFields, newFields) {
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.Compressed = false
// Аудит изменения поля
AuditFieldOperation("UPDATE", "", "", d.ID, name, value)
return
}
}
}
// compareAndSwapFields выполняет CAS операцию для полей (lock-free)
func (d *Document) compareAndSwapFields(old, new map[string]interface{}) bool {
return d.fieldsPtr.CompareAndSwap(old, new)
}
// GetField возвращает значение поля документа (lock-free)
func (d *Document) GetField(name string) (interface{}, error) {
fields := d.loadFields()
if val, ok := fields[name]; ok {
return val, nil
}
return nil, fmt.Errorf("field not found: %s", name)
}
// DeleteField удаляет поле из документа (lock-free)
func (d *Document) DeleteField(name string) {
for {
oldFields := d.loadFields()
if _, exists := oldFields[name]; !exists {
return
}
newFields := make(map[string]interface{})
for k, v := range oldFields {
if k != name {
newFields[k] = v
}
}
if d.compareAndSwapFields(oldFields, newFields) {
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.Compressed = false
// Аудит удаления поля
AuditFieldOperation("DELETE", "", "", d.ID, name, nil)
return
}
}
}
// HasField проверяет наличие поля в документе (lock-free)
func (d *Document) HasField(name string) bool {
fields := d.loadFields()
_, ok := fields[name]
return ok
}
// GetFields возвращает копию всех полей документа (lock-free)
func (d *Document) GetFields() map[string]interface{} {
fields := d.loadFields()
copy := make(map[string]interface{})
for k, v := range fields {
copy[k] = v
}
return copy
}
// ToMap возвращает полное представление документа в виде map
// Включает метаданные (_id, _created_at, _updated_at, _deleted_at, _version) и все поля
func (d *Document) ToMap() map[string]interface{} {
fields := d.GetFields()
result := make(map[string]interface{})
// Добавляем все пользовательские поля
for k, v := range fields {
result[k] = v
}
// Добавляем метаданные
result["_id"] = d.ID
result["_created_at"] = d.CreatedAt
result["_updated_at"] = d.UpdatedAt
result["_deleted_at"] = d.DeletedAt
result["_version"] = d.Version
return result
}
// SetTuple устанавливает вложенный документ (кортеж) в поле
func (d *Document) SetTuple(fieldName string, tuple *Tuple) {
d.SetField(fieldName, tuple)
}
// GetTuple возвращает вложенный документ из поля
func (d *Document) GetTuple(fieldName string) (*Tuple, error) {
val, err := d.GetField(fieldName)
if err != nil {
return nil, err
}
if tuple, ok := val.(*Tuple); ok {
return tuple, nil
}
return nil, fmt.Errorf("field %s is not a tuple", fieldName)
}
// Serialize сериализует документ в MessagePack с поддержкой сжатия
func (d *Document) Serialize() ([]byte, error) {
// Создаём копию для сериализации
fields := d.GetFields()
docCopy := &Document{
ID: d.ID,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
DeletedAt: d.DeletedAt,
Version: d.Version,
Compressed: d.Compressed,
OriginalSize: d.OriginalSize,
}
docCopy.fieldsPtr.Store(fields)
data, err := serializer.Marshal(docCopy)
if err != nil {
return nil, err
}
return data, nil
}
// SerializeCompressed сериализует и сжимает документ
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
data, err := d.Serialize()
if err != nil {
return nil, err
}
// Проверяем, нужно ли сжимать
if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize {
compressed, err := compression.Compress(data, compressionConfig)
if err != nil {
return data, nil
}
return compressed, nil
}
return data, nil
}
// Deserialize десериализует документ из MessagePack (автоматически определяет сжатие)
func (d *Document) Deserialize(data []byte) error {
// Пытаемся определить, сжаты ли данные
decompressed, err := compression.DecompressAuto(data)
if err == nil && len(decompressed) < len(data) {
var doc Document
if err := serializer.Unmarshal(decompressed, &doc); err != nil {
return err
}
d.ID = doc.ID
d.fieldsPtr.Store(doc.loadFields())
d.CreatedAt = doc.CreatedAt
d.UpdatedAt = doc.UpdatedAt
d.DeletedAt = doc.DeletedAt
d.Version = doc.Version
d.Compressed = true
d.OriginalSize = int64(len(decompressed))
} else {
var doc Document
if err := serializer.Unmarshal(data, &doc); err != nil {
return err
}
d.ID = doc.ID
d.fieldsPtr.Store(doc.loadFields())
d.CreatedAt = doc.CreatedAt
d.UpdatedAt = doc.UpdatedAt
d.DeletedAt = doc.DeletedAt
d.Version = doc.Version
d.Compressed = false
d.OriginalSize = 0
}
d.UpdatedAt = time.Now().UnixMilli()
return nil
}
// Clone создаёт глубокую копию документа (lock-free)
func (d *Document) Clone() *Document {
fields := d.GetFields()
clone := &Document{
ID: d.ID,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
DeletedAt: d.DeletedAt,
Version: d.Version,
Compressed: d.Compressed,
OriginalSize: d.OriginalSize,
}
// Глубокое копирование полей
copiedFields := make(map[string]interface{})
for k, v := range fields {
copiedFields[k] = deepCopyValue(v)
}
clone.fieldsPtr.Store(copiedFields)
return clone
}
// Update применяет обновление к документу (атомарно, lock-free)
func (d *Document) Update(updates map[string]interface{}) error {
for {
oldFields := d.loadFields()
newFields := make(map[string]interface{})
for k, v := range oldFields {
newFields[k] = v
}
for k, v := range updates {
newFields[k] = v
}
if d.compareAndSwapFields(oldFields, newFields) {
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
d.Compressed = false
return nil
}
}
}
// SoftDelete мягко удаляет документ (устанавливает метку времени удаления)
func (d *Document) SoftDelete() {
d.DeletedAt = time.Now().UnixMilli()
d.UpdatedAt = d.DeletedAt
d.Version++
}
// IsDeleted проверяет, удалён ли документ (мягкое удаление)
func (d *Document) IsDeleted() bool {
return d.DeletedAt > 0
}
// Restore восстанавливает мягко удалённый документ
func (d *Document) Restore() {
d.DeletedAt = 0
d.UpdatedAt = time.Now().UnixMilli()
d.Version++
}
// GetDeletedAtStr возвращает человекочитаемую строку времени удаления
func (d *Document) GetDeletedAtStr() string {
if d.DeletedAt == 0 {
return ""
}
return time.UnixMilli(d.DeletedAt).Format("2006-01-02 15:04:05.000")
}
// GetCreatedAtStr возвращает человекочитаемую строку времени создания
func (d *Document) GetCreatedAtStr() string {
return time.UnixMilli(d.CreatedAt).Format("2006-01-02 15:04:05.000")
}
// GetUpdatedAtStr возвращает человекочитаемую строку времени обновления
func (d *Document) GetUpdatedAtStr() string {
return time.UnixMilli(d.UpdatedAt).Format("2006-01-02 15:04:05.000")
}
// Compress сжимает документ в памяти
func (d *Document) Compress(config *compression.Config) error {
if d.Compressed {
return nil
}
fields := d.loadFields()
originalSize := len(fields)
if originalSize < config.MinSize {
return nil
}
d.Compressed = true
d.OriginalSize = int64(originalSize)
return nil
}
// Decompress распаковывает документ в памяти
func (d *Document) Decompress() error {
if !d.Compressed {
return nil
}
d.Compressed = false
d.OriginalSize = 0
return nil
}
// GetCompressionRatio возвращает коэффициент сжатия
func (d *Document) GetCompressionRatio() float64 {
if !d.Compressed || d.OriginalSize == 0 {
return 1.0
}
fields := d.loadFields()
currentSize := len(fields)
return float64(currentSize) / float64(d.OriginalSize)
}
// GetMetadata возвращает метаданные документа (временные метки)
func (d *Document) GetMetadata() map[string]int64 {
return map[string]int64{
"created_at": d.CreatedAt,
"updated_at": d.UpdatedAt,
"deleted_at": d.DeletedAt,
"version": int64(d.Version),
}
}
// deepCopyValue выполняет глубокое копирование значения
func deepCopyValue(val interface{}) interface{} {
switch v := val.(type) {
case *Tuple:
return v.Clone()
case map[string]interface{}:
copy := make(map[string]interface{})
for k, val := range v {
copy[k] = deepCopyValue(val)
}
return copy
case []interface{}:
copy := make([]interface{}, len(v))
for i, val := range v {
copy[i] = deepCopyValue(val)
}
return copy
default:
return v
}
}
// NewTuple создаёт новый вложенный документ (кортеж)
func NewTuple() *Tuple {
now := time.Now().UnixMilli()
return &Tuple{
Fields: make(map[string]interface{}),
CreatedAt: now,
UpdatedAt: now,
}
}
// Set устанавливает поле во вложенном документе
func (t *Tuple) Set(name string, value interface{}) {
t.mu.Lock()
defer t.mu.Unlock()
t.Fields[name] = value
t.UpdatedAt = time.Now().UnixMilli()
}
// Get возвращает поле из вложенного документа
func (t *Tuple) Get(name string) (interface{}, error) {
t.mu.RLock()
defer t.mu.RUnlock()
if val, ok := t.Fields[name]; ok {
return val, nil
}
return nil, fmt.Errorf("tuple field not found: %s", name)
}
// Clone создаёт копию кортежа
func (t *Tuple) Clone() *Tuple {
t.mu.RLock()
defer t.mu.RUnlock()
clone := NewTuple()
for k, v := range t.Fields {
clone.Fields[k] = deepCopyValue(v)
}
clone.CreatedAt = t.CreatedAt
clone.UpdatedAt = t.UpdatedAt
return clone
}
// ToMap конвертирует кортеж в map
func (t *Tuple) ToMap() map[string]interface{} {
t.mu.RLock()
defer t.mu.RUnlock()
copy := make(map[string]interface{})
for k, v := range t.Fields {
copy[k] = v
}
return copy
}
// GetTupleMetadata возвращает метаданные кортежа
func (t *Tuple) GetTupleMetadata() map[string]int64 {
t.mu.RLock()
defer t.mu.RUnlock()
return map[string]int64{
"created_at": t.CreatedAt,
"updated_at": t.UpdatedAt,
}
}
// GetNestedField получает значение по точечному пути (например, "user.address.city")
func (d *Document) GetNestedField(path string) (interface{}, error) {
parts := strings.Split(path, ".")
if len(parts) == 0 {
return nil, fmt.Errorf("empty path")
}
current := interface{}(d)
for _, part := range parts {
switch v := current.(type) {
case *Document:
val, err := v.GetField(part)
if err != nil {
return nil, err
}
current = val
case *Tuple:
val, err := v.Get(part)
if err != nil {
return nil, err
}
current = val
case map[string]interface{}:
if val, ok := v[part]; ok {
current = val
} else {
return nil, fmt.Errorf("field not found: %s", part)
}
default:
return nil, fmt.Errorf("cannot navigate into non-document value at %s", part)
}
}
return current, nil
}
// SetNestedField устанавливает значение по точечному пути
func (d *Document) SetNestedField(path string, value interface{}) error {
parts := strings.Split(path, ".")
if len(parts) == 0 {
return fmt.Errorf("empty path")
}
if len(parts) == 1 {
d.SetField(parts[0], value)
return nil
}
// Для простоты реализации используем подход с чтением-модификацией-записью
// В production коде потребуется более сложная lock-free структура
// Сначала проверяем путь
var current interface{} = d
for i := 0; i < len(parts)-1; i++ {
part := parts[i]
switch v := current.(type) {
case *Document:
if !v.HasField(part) {
newTuple := NewTuple()
v.SetField(part, newTuple)
current = newTuple
} else {
field, _ := v.GetField(part)
if tuple, ok := field.(*Tuple); ok {
current = tuple
} else {
return fmt.Errorf("field %s is not a tuple", part)
}
}
case *Tuple:
if val, err := v.Get(part); err == nil {
if tuple, ok := val.(*Tuple); ok {
current = tuple
} else {
return fmt.Errorf("field %s is not a tuple", part)
}
} else {
newTuple := NewTuple()
v.Set(part, newTuple)
current = newTuple
}
default:
return fmt.Errorf("cannot set nested field on non-document value")
}
}
lastPart := parts[len(parts)-1]
switch v := current.(type) {
case *Document:
v.SetField(lastPart, value)
case *Tuple:
v.Set(lastPart, value)
default:
return fmt.Errorf("cannot set field on non-document value")
}
d.UpdatedAt = time.Now().UnixMilli()
d.Compressed = false
return nil
}

971
internal/storage/engine.go Normal file
View File

@@ -0,0 +1,971 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/storage/engine.go
// Назначение: In-memory движок хранения документов с поддержкой коллекций,
// слайсов (аналог БД), тапплов (аналог таблиц), полей и кортежей.
// Полностью wait-free с использованием sync.Map и атомарных операций.
package storage
import (
"encoding/json"
"fmt"
"os"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/raft"
"futriis/internal/log"
"futriis/internal/serializer"
)
// =============================================================================
// INMEM STORE - ВСТРОЕННОЕ ФАЙЛОВОЕ ХРАНИЛИЩЕ ДЛЯ RAFT
// =============================================================================
// InmemStore реализует встроенное файловое хранилище для Raft.
type InmemStore struct {
mu sync.RWMutex
data map[string][]byte
path string
createdAt int64
updatedAt int64
}
// NewInmemStore создаёт новое хранилище.
func NewInmemStore(path string) *InmemStore {
now := time.Now().UnixMilli()
store := &InmemStore{
data: make(map[string][]byte),
path: path,
createdAt: now,
updatedAt: now,
}
store.load()
return store
}
// load загружает данные из файла.
func (s *InmemStore) load() {
if s.path == "" {
return
}
data, err := os.ReadFile(s.path)
if err != nil {
return
}
json.Unmarshal(data, &s.data)
s.updatedAt = time.Now().UnixMilli()
}
// save сохраняет данные в файл.
func (s *InmemStore) save() {
if s.path == "" {
return
}
s.updatedAt = time.Now().UnixMilli()
data, _ := json.Marshal(s.data)
os.WriteFile(s.path, data, 0644)
}
// ==================== Реализация raft.LogStore ====================
// FirstIndex возвращает первый индекс в логе.
func (s *InmemStore) FirstIndex() (uint64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var first uint64 = 0
for key := range s.data {
var idx uint64
if _, err := fmt.Sscanf(key, "log-%d", &idx); err == nil {
if first == 0 || idx < first {
first = idx
}
}
}
return first, nil
}
// LastIndex возвращает последний индекс в логе.
func (s *InmemStore) LastIndex() (uint64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var last uint64 = 0
for key := range s.data {
var idx uint64
if _, err := fmt.Sscanf(key, "log-%d", &idx); err == nil {
if idx > last {
last = idx
}
}
}
return last, nil
}
// GetLog получает запись лога по индексу.
func (s *InmemStore) GetLog(idx uint64, log *raft.Log) error {
s.mu.RLock()
defer s.mu.RUnlock()
key := fmt.Sprintf("log-%d", idx)
data, ok := s.data[key]
if !ok {
return raft.ErrLogNotFound
}
return json.Unmarshal(data, log)
}
// StoreLog сохраняет одну запись лога.
func (s *InmemStore) StoreLog(log *raft.Log) error {
return s.StoreLogs([]*raft.Log{log})
}
// StoreLogs сохраняет несколько записей лога.
func (s *InmemStore) StoreLogs(logs []*raft.Log) error {
s.mu.Lock()
defer s.mu.Unlock()
for _, log := range logs {
key := fmt.Sprintf("log-%d", log.Index)
data, err := json.Marshal(log)
if err != nil {
return err
}
s.data[key] = data
}
s.save()
return nil
}
// DeleteRange удаляет записи лога в диапазоне.
func (s *InmemStore) DeleteRange(min, max uint64) error {
s.mu.Lock()
defer s.mu.Unlock()
for idx := min; idx <= max; idx++ {
key := fmt.Sprintf("log-%d", idx)
delete(s.data, key)
}
s.save()
return nil
}
// ==================== Реализация raft.StableStore ====================
// Get получает значение по ключу.
func (s *InmemStore) Get(key []byte) ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.data[string(key)]
if !ok {
return nil, nil
}
return val, nil
}
// Set устанавливает значение по ключу.
func (s *InmemStore) Set(key []byte, val []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.data[string(key)] = val
s.save()
return nil
}
// SetUint64 устанавливает uint64 значение по ключу.
func (s *InmemStore) SetUint64(key []byte, val uint64) error {
return s.Set(key, []byte(fmt.Sprintf("%d", val)))
}
// GetUint64 получает uint64 значение по ключу.
func (s *InmemStore) GetUint64(key []byte) (uint64, error) {
val, err := s.Get(key)
if err != nil {
return 0, err
}
if val == nil {
return 0, nil
}
var result uint64
fmt.Sscanf(string(val), "%d", &result)
return result, nil
}
// =============================================================================
// ОСНОВНОЕ ХРАНИЛИЩЕ
// =============================================================================
// Storage представляет основное хранилище баз данных
type Storage struct {
databases sync.Map // map[string]*Database
pageSize int64
logger *log.Logger
totalDocs atomic.Int64
createdAt int64
}
// Database представляет базу данных (аналог слайса в реляционных СУБД)
type Database struct {
name string
collections sync.Map // map[string]*Collection
createdAt int64
updatedAt int64
deletedAt int64
mu sync.RWMutex
}
// ExportMetadata содержит метаданные экспорта
type ExportMetadata struct {
DatabaseName string `msgpack:"database_name"`
ExportTime int64 `msgpack:"export_time"`
ExportTimeStr string `msgpack:"export_time_str"`
Version string `msgpack:"version"`
CollectionsCount int `msgpack:"collections_count"`
DocumentsCount int64 `msgpack:"documents_count"`
ExportedBy string `msgpack:"exported_by,omitempty"`
Additional map[string]interface{} `msgpack:"additional,omitempty"`
}
// ImportMetadata содержит метаданные импорта
type ImportMetadata struct {
DatabaseName string `msgpack:"database_name"`
ImportTime int64 `msgpack:"import_time"`
ImportTimeStr string `msgpack:"import_time_str"`
SourceFile string `msgpack:"source_file"`
SourceExportTime int64 `msgpack:"source_export_time,omitempty"`
CollectionsCount int `msgpack:"collections_count"`
DocumentsCount int64 `msgpack:"documents_count"`
SkippedDocuments int64 `msgpack:"skipped_documents"`
FailedDocuments int64 `msgpack:"failed_documents"`
ImportedBy string `msgpack:"imported_by,omitempty"`
}
// NewStorage создаёт новый экземпляр хранилища
func NewStorage(pageSizeMB int, logger *log.Logger) *Storage {
return &Storage{
pageSize: int64(pageSizeMB) * 1024 * 1024,
logger: logger,
createdAt: time.Now().UnixMilli(),
}
}
// CreateDatabase создаёт новую базу данных
func (s *Storage) CreateDatabase(name string) error {
now := time.Now().UnixMilli()
db := &Database{
name: name,
createdAt: now,
updatedAt: now,
deletedAt: 0,
}
if _, exists := s.databases.LoadOrStore(name, db); exists {
return fmt.Errorf("database already exists")
}
AuditDatabaseOperation("CREATE", name)
if s.logger != nil {
s.logger.Info("Database created: " + name)
}
return nil
}
// GetDatabase возвращает базу данных по имени
func (s *Storage) GetDatabase(name string) (*Database, error) {
if val, ok := s.databases.Load(name); ok {
return val.(*Database), nil
}
return nil, fmt.Errorf("database not found")
}
// DropDatabase удаляет базу данных
func (s *Storage) DropDatabase(name string) error {
if val, ok := s.databases.Load(name); ok {
db := val.(*Database)
db.mu.Lock()
db.deletedAt = time.Now().UnixMilli()
db.mu.Unlock()
}
if _, ok := s.databases.LoadAndDelete(name); !ok {
return fmt.Errorf("database not found")
}
AuditDatabaseOperation("DROP", name)
if s.logger != nil {
s.logger.Info("Database dropped: " + name)
}
return nil
}
// ListDatabases возвращает список всех баз данных
func (s *Storage) ListDatabases() []string {
databases := make([]string, 0)
s.databases.Range(func(key, value interface{}) bool {
databases = append(databases, key.(string))
return true
})
return databases
}
// Name возвращает имя базы данных
func (db *Database) Name() string {
return db.name
}
// GetCreatedAt возвращает время создания базы данных
func (db *Database) GetCreatedAt() int64 {
db.mu.RLock()
defer db.mu.RUnlock()
return db.createdAt
}
// GetUpdatedAt возвращает время последнего обновления базы данных
func (db *Database) GetUpdatedAt() int64 {
db.mu.RLock()
defer db.mu.RUnlock()
return db.updatedAt
}
// GetDeletedAt возвращает время удаления базы данных (0 = не удалена)
func (db *Database) GetDeletedAt() int64 {
db.mu.RLock()
defer db.mu.RUnlock()
return db.deletedAt
}
// UpdateTimestamp обновляет временную метку базы данных
func (db *Database) UpdateTimestamp() {
db.mu.Lock()
defer db.mu.Unlock()
db.updatedAt = time.Now().UnixMilli()
}
// CreateCollection создаёт новую коллекцию в базе данных
func (db *Database) CreateCollection(name string) error {
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, nil)); exists {
return fmt.Errorf("collection already exists")
}
db.UpdateTimestamp()
AuditCollectionOperation("CREATE", db.name, name, nil)
return nil
}
// CreateCollectionWithSettings создаёт коллекцию с настройками
func (db *Database) CreateCollectionWithSettings(name string, settings *CollectionSettings) error {
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, settings)); exists {
return fmt.Errorf("collection already exists")
}
db.UpdateTimestamp()
AuditCollectionOperation("CREATE", db.name, name, settings)
return nil
}
// GetCollection возвращает коллекцию по имени
func (db *Database) GetCollection(name string) (*Collection, error) {
if val, ok := db.collections.Load(name); ok {
return val.(*Collection), nil
}
return nil, fmt.Errorf("collection not found")
}
// DropCollection удаляет коллекцию
func (db *Database) DropCollection(name string) error {
if _, ok := db.collections.LoadAndDelete(name); !ok {
return fmt.Errorf("collection not found")
}
db.UpdateTimestamp()
AuditCollectionOperation("DROP", db.name, name, nil)
return nil
}
// ListCollections возвращает список всех коллекций в базе данных
func (db *Database) ListCollections() []string {
collections := make([]string, 0)
db.collections.Range(func(key, value interface{}) bool {
collections = append(collections, key.(string))
return true
})
return collections
}
// GetTotalDocuments возвращает общее количество документов во всех коллекциях
func (s *Storage) GetTotalDocuments() int64 {
return s.totalDocs.Load()
}
// GetPageSize возвращает размер страницы памяти
func (s *Storage) GetPageSize() int64 {
return s.pageSize
}
// GetCreatedAt возвращает время создания хранилища
func (s *Storage) GetCreatedAt() int64 {
return s.createdAt
}
// SerializeDatabaseWithMetadata сериализует базу данных с метаданными экспорта
func (db *Database) SerializeDatabaseWithMetadata(exportedBy string) ([]byte, *ExportMetadata, error) {
dbData := make(map[string]interface{})
totalDocuments := int64(0)
db.collections.Range(func(key, value interface{}) bool {
coll := value.(*Collection)
collData := make(map[string]interface{})
docs := coll.GetAllDocuments()
totalDocuments += int64(len(docs))
collDocs := make([]*Document, 0, len(docs))
for _, doc := range docs {
collDocs = append(collDocs, doc)
}
collData["documents"] = collDocs
collData["metadata"] = coll.GetMetadata()
collData["timestamps"] = coll.GetTimestamps()
collData["indexes"] = coll.GetIndexesInfo()
collData["constraints"] = coll.GetConstraints()
collData["constraint_timestamps"] = coll.GetConstraintTimestamps()
collData["acl_timestamps"] = coll.GetACLTimestamps()
dbData[key.(string)] = collData
return true
})
// Используем функцию GetCurrentTimestamp из audit.go
nowMs, nowStr := GetCurrentTimestamp()
metadata := &ExportMetadata{
DatabaseName: db.name,
ExportTime: nowMs,
ExportTimeStr: nowStr,
Version: "1.0",
CollectionsCount: len(db.ListCollections()),
DocumentsCount: totalDocuments,
ExportedBy: exportedBy,
Additional: map[string]interface{}{
"storage_created_at": time.UnixMilli(db.createdAt).Format("2006-01-02 15:04:05.000"),
},
}
dbData["_metadata"] = map[string]interface{}{
"name": db.name,
"created_at": db.GetCreatedAt(),
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
"updated_at": db.GetUpdatedAt(),
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
"export_metadata": metadata,
}
data, err := serializer.Marshal(dbData)
if err != nil {
return nil, nil, err
}
return data, metadata, nil
}
// SerializeDatabase сериализует всю базу данных в MessagePack
func (db *Database) SerializeDatabase() ([]byte, error) {
data, _, err := db.SerializeDatabaseWithMetadata("")
return data, err
}
// DeserializeDatabaseWithMetadata десериализует базу данных с сохранением метаданных импорта
func (db *Database) DeserializeDatabaseWithMetadata(data []byte, sourceFile string, importedBy string) (*ImportMetadata, error) {
var dbData map[string]interface{}
if err := serializer.Unmarshal(data, &dbData); err != nil {
return nil, err
}
var sourceExportTime int64
var collectionsCount int
var documentsCount int64
if metaRaw, ok := dbData["_metadata"]; ok {
if meta, ok := metaRaw.(map[string]interface{}); ok {
if createdAt, ok := meta["created_at"]; ok {
if v, ok := createdAt.(int64); ok {
db.createdAt = v
}
}
if exportMetaRaw, ok := meta["export_metadata"]; ok {
if exportMeta, ok := exportMetaRaw.(map[string]interface{}); ok {
if sourceTime, ok := exportMeta["export_time"]; ok {
if v, ok := sourceTime.(int64); ok {
sourceExportTime = v
} else if v, ok := sourceTime.(float64); ok {
sourceExportTime = int64(v)
}
}
if collCount, ok := exportMeta["collections_count"]; ok {
if v, ok := collCount.(int); ok {
collectionsCount = v
} else if v, ok := collCount.(int64); ok {
collectionsCount = int(v)
} else if v, ok := collCount.(float64); ok {
collectionsCount = int(v)
}
}
if docCount, ok := exportMeta["documents_count"]; ok {
if v, ok := docCount.(int64); ok {
documentsCount = v
} else if v, ok := docCount.(float64); ok {
documentsCount = int64(v)
}
}
}
}
}
delete(dbData, "_metadata")
}
for collName, collDataRaw := range dbData {
collData, ok := collDataRaw.(map[string]interface{})
if !ok {
continue
}
settings := &CollectionSettings{
MaxDocuments: 0,
ValidateSchema: false,
AutoIndexID: true,
TTLSeconds: 0,
SoftDelete: false,
}
if metaRaw, ok := collData["metadata"]; ok {
if meta, ok := metaRaw.(map[string]interface{}); ok {
if settingsRaw, ok := meta["settings"]; ok {
if settingsMap, ok := settingsRaw.(map[string]interface{}); ok {
if maxDocs, ok := settingsMap["max_documents"]; ok {
if v, ok := maxDocs.(int); ok {
settings.MaxDocuments = v
} else if v, ok := maxDocs.(int64); ok {
settings.MaxDocuments = int(v)
} else if v, ok := maxDocs.(float64); ok {
settings.MaxDocuments = int(v)
}
}
if validateSchema, ok := settingsMap["validate_schema"]; ok {
if v, ok := validateSchema.(bool); ok {
settings.ValidateSchema = v
}
}
if autoIndexID, ok := settingsMap["auto_index_id"]; ok {
if v, ok := autoIndexID.(bool); ok {
settings.AutoIndexID = v
}
}
if ttlSeconds, ok := settingsMap["ttl_seconds"]; ok {
if v, ok := ttlSeconds.(int); ok {
settings.TTLSeconds = v
} else if v, ok := ttlSeconds.(int64); ok {
settings.TTLSeconds = int(v)
} else if v, ok := ttlSeconds.(float64); ok {
settings.TTLSeconds = int(v)
}
}
if softDelete, ok := settingsMap["soft_delete"]; ok {
if v, ok := softDelete.(bool); ok {
settings.SoftDelete = v
}
}
}
}
} else if meta, ok := metaRaw.(*CollectionMetadata); ok {
if meta.Settings != nil {
settings = meta.Settings
}
}
}
coll := NewCollection(db.name, collName, settings)
if timestampsRaw, ok := collData["timestamps"]; ok {
if timestamps, ok := timestampsRaw.(map[string]interface{}); ok {
if createdAt, ok := timestamps["created_at"]; ok {
if v, ok := createdAt.(int64); ok {
coll.metadata.CreatedAt = v
} else if v, ok := createdAt.(float64); ok {
coll.metadata.CreatedAt = int64(v)
}
}
if updatedAt, ok := timestamps["updated_at"]; ok {
if v, ok := updatedAt.(int64); ok {
coll.metadata.UpdatedAt = v
} else if v, ok := updatedAt.(float64); ok {
coll.metadata.UpdatedAt = int64(v)
}
}
}
}
if indexesRaw, ok := collData["indexes"]; ok {
if indexesList, ok := indexesRaw.([]interface{}); ok {
for _, idxRaw := range indexesList {
if idxMap, ok := idxRaw.(map[string]interface{}); ok {
idxName, _ := idxMap["name"].(string)
idxFieldsRaw, _ := idxMap["fields"].([]interface{})
idxFields := make([]string, len(idxFieldsRaw))
for i, f := range idxFieldsRaw {
idxFields[i] = fmt.Sprintf("%v", f)
}
idxUnique, _ := idxMap["unique"].(bool)
if idxName != "" && idxName != "_id_" {
coll.CreateIndex(idxName, idxFields, idxUnique)
}
}
}
}
}
if docsRaw, ok := collData["documents"]; ok {
if docs, ok := docsRaw.([]interface{}); ok {
for _, docRaw := range docs {
if doc, ok := docRaw.(*Document); ok {
coll.Insert(doc)
} else if docMap, ok := docRaw.(map[string]interface{}); ok {
doc := NewDocument()
if id, ok := docMap["ID"].(string); ok {
doc.ID = id
} else if id, ok := docMap["id"].(string); ok {
doc.ID = id
}
if fields, ok := docMap["fields"]; ok {
if fieldsMap, ok := fields.(map[string]interface{}); ok {
for k, v := range fieldsMap {
doc.SetField(k, v)
}
}
}
if createdAt, ok := docMap["created_at"]; ok {
if v, ok := createdAt.(int64); ok {
doc.CreatedAt = v
} else if v, ok := createdAt.(float64); ok {
doc.CreatedAt = int64(v)
}
}
if updatedAt, ok := docMap["updated_at"]; ok {
if v, ok := updatedAt.(int64); ok {
doc.UpdatedAt = v
} else if v, ok := updatedAt.(float64); ok {
doc.UpdatedAt = int64(v)
}
}
if deletedAt, ok := docMap["deleted_at"]; ok {
if v, ok := deletedAt.(int64); ok {
doc.DeletedAt = v
} else if v, ok := deletedAt.(float64); ok {
doc.DeletedAt = int64(v)
}
}
if version, ok := docMap["version"]; ok {
if v, ok := version.(uint64); ok {
doc.Version = v
} else if v, ok := version.(int64); ok {
doc.Version = uint64(v)
} else if v, ok := version.(float64); ok {
doc.Version = uint64(v)
}
}
coll.Insert(doc)
}
}
} else if docs, ok := docsRaw.([]*Document); ok {
for _, doc := range docs {
coll.Insert(doc)
}
}
}
db.collections.Store(collName, coll)
AuditCollectionOperation("RESTORE", db.name, collName, settings)
}
// Используем функцию GetCurrentTimestamp из audit.go
nowMs, nowStr := GetCurrentTimestamp()
importMetadata := &ImportMetadata{
DatabaseName: db.name,
ImportTime: nowMs,
ImportTimeStr: nowStr,
SourceFile: sourceFile,
SourceExportTime: sourceExportTime,
CollectionsCount: collectionsCount,
DocumentsCount: documentsCount,
SkippedDocuments: 0,
FailedDocuments: 0,
ImportedBy: importedBy,
}
db.UpdateTimestamp()
LogAudit("IMPORT", "DATABASE", db.name, map[string]interface{}{
"source_file": sourceFile,
"collections_count": collectionsCount,
"documents_count": documentsCount,
"source_export_time": sourceExportTime,
"imported_by": importedBy,
})
return importMetadata, nil
}
// DeserializeDatabase десериализует базу данных из MessagePack
func (db *Database) DeserializeDatabase(data []byte) error {
_, err := db.DeserializeDatabaseWithMetadata(data, "", "")
return err
}
// ExportDatabaseWithMetadata экспортирует базу данных с метаданными
func (s *Storage) ExportDatabaseWithMetadata(dbName, filePath, exportedBy string) (*ExportMetadata, error) {
if !s.ExistsDatabase(dbName) {
return nil, fmt.Errorf("database '%s' not found", dbName)
}
db, err := s.GetDatabase(dbName)
if err != nil {
return nil, err
}
data, metadata, err := db.SerializeDatabaseWithMetadata(exportedBy)
if err != nil {
return nil, err
}
if err := os.WriteFile(filePath, data, 0644); err != nil {
return nil, fmt.Errorf("failed to write export file: %v", err)
}
if s.logger != nil {
s.logger.Info(fmt.Sprintf("Database '%s' exported to %s at %s", dbName, filePath, metadata.ExportTimeStr))
}
return metadata, nil
}
// ImportDatabaseWithMetadata импортирует базу данных с метаданными
func (s *Storage) ImportDatabaseWithMetadata(dbName, filePath, importedBy string) (*ImportMetadata, error) {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil, fmt.Errorf("import file '%s' not found", filePath)
}
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read import file: %v", err)
}
if !s.ExistsDatabase(dbName) {
if err := s.CreateDatabase(dbName); err != nil {
return nil, fmt.Errorf("failed to create database: %v", err)
}
}
db, err := s.GetDatabase(dbName)
if err != nil {
return nil, err
}
metadata, err := db.DeserializeDatabaseWithMetadata(data, filePath, importedBy)
if err != nil {
return nil, err
}
if s.logger != nil {
s.logger.Info(fmt.Sprintf("Database '%s' imported from %s at %s", dbName, filePath, metadata.ImportTimeStr))
}
return metadata, nil
}
// GetDatabaseNames возвращает имена всех баз данных
func (s *Storage) GetDatabaseNames() []string {
return s.ListDatabases()
}
// ExistsDatabase проверяет существование базы данных
func (s *Storage) ExistsDatabase(name string) bool {
_, ok := s.databases.Load(name)
return ok
}
// GetDatabaseCount возвращает количество баз данных
func (s *Storage) GetDatabaseCount() int {
count := 0
s.databases.Range(func(key, value interface{}) bool {
count++
return true
})
return count
}
// ========== Дополнительные методы для управления хранилищем ==========
// Backup создаёт резервную копию всех данных
func (s *Storage) Backup(backupPath string) error {
if s.logger != nil {
s.logger.Info(fmt.Sprintf("Starting backup to %s", backupPath))
}
backup := make(map[string]interface{})
backup["created_at"] = s.createdAt
backup["backup_time"] = time.Now().UnixMilli()
backup["backup_time_str"] = time.Now().Format("2006-01-02 15:04:05.000")
databases := make(map[string][]byte)
databasesMetadata := make(map[string]interface{})
s.databases.Range(func(key, value interface{}) bool {
dbName := key.(string)
db := value.(*Database)
dbData, err := db.SerializeDatabase()
if err != nil {
if s.logger != nil {
s.logger.Error(fmt.Sprintf("Failed to serialize database %s: %v", dbName, err))
}
return false
}
databases[dbName] = dbData
databasesMetadata[dbName] = map[string]interface{}{
"created_at": db.GetCreatedAt(),
"updated_at": db.GetUpdatedAt(),
"collections": len(db.ListCollections()),
}
return true
})
backup["databases"] = databases
backup["databases_metadata"] = databasesMetadata
backup["total_databases"] = s.GetDatabaseCount()
data, err := serializer.Marshal(backup)
if err != nil {
return fmt.Errorf("failed to marshal backup: %v", err)
}
if err := os.WriteFile(backupPath, data, 0644); err != nil {
return fmt.Errorf("failed to write backup: %v", err)
}
if s.logger != nil {
s.logger.Info(fmt.Sprintf("Backup completed: %s", backupPath))
}
return nil
}
// Restore восстанавливает данные из резервной копии
func (s *Storage) Restore(backupPath string) error {
if s.logger != nil {
s.logger.Info(fmt.Sprintf("Starting restore from %s", backupPath))
}
data, err := os.ReadFile(backupPath)
if err != nil {
return fmt.Errorf("failed to read backup file: %v", err)
}
var backup map[string]interface{}
if err := serializer.Unmarshal(data, &backup); err != nil {
return fmt.Errorf("failed to unmarshal backup: %v", err)
}
if createdAt, ok := backup["created_at"]; ok {
if v, ok := createdAt.(int64); ok {
s.createdAt = v
}
}
databasesRaw, ok := backup["databases"]
if !ok {
return fmt.Errorf("invalid backup format: missing databases")
}
databases, ok := databasesRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid backup format: databases is not a map")
}
for dbName, dbDataRaw := range databases {
dbData, ok := dbDataRaw.([]byte)
if !ok {
continue
}
if !s.ExistsDatabase(dbName) {
if err := s.CreateDatabase(dbName); err != nil {
return fmt.Errorf("failed to create database %s: %v", dbName, err)
}
}
db, err := s.GetDatabase(dbName)
if err != nil {
return fmt.Errorf("failed to get database %s: %v", dbName, err)
}
if err := db.DeserializeDatabase(dbData); err != nil {
return fmt.Errorf("failed to restore database %s: %v", dbName, err)
}
}
if s.logger != nil {
s.logger.Info("Restore completed")
}
return nil
}
// GetStats возвращает статистику хранилища
func (s *Storage) GetStats() map[string]interface{} {
stats := map[string]interface{}{
"total_databases": s.GetDatabaseCount(),
"total_documents": s.GetTotalDocuments(),
"page_size_bytes": s.pageSize,
"created_at": s.createdAt,
"created_at_str": time.UnixMilli(s.createdAt).Format("2006-01-02 15:04:05.000"),
}
databases := make([]map[string]interface{}, 0)
s.databases.Range(func(key, value interface{}) bool {
db := value.(*Database)
dbStats := map[string]interface{}{
"name": db.name,
"collections": len(db.ListCollections()),
"created_at": db.GetCreatedAt(),
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
"updated_at": db.GetUpdatedAt(),
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
"deleted_at": db.GetDeletedAt(),
}
totalDocs := int64(0)
totalSize := int64(0)
db.collections.Range(func(k, v interface{}) bool {
coll := v.(*Collection)
totalDocs += coll.Count()
totalSize += coll.Size()
return true
})
dbStats["documents"] = totalDocs
dbStats["size_bytes"] = totalSize
databases = append(databases, dbStats)
return true
})
stats["databases"] = databases
return stats
}

83
internal/storage/fsync.go Normal file
View 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
}

View File

@@ -0,0 +1,984 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/storage/persistence.go
// Назначение: Персистентное хранение данных на диске с поддержкой checkpoint и recovery
package storage
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
)
// ========== WALReader интерфейс и реализация ==========
// WALEntry представляет запись WAL
type WALEntry struct {
Index uint64 `json:"index"`
Term uint64 `json:"term"`
Type string `json:"type"`
Data []byte `json:"data"`
Timestamp int64 `json:"timestamp"`
}
// WALReader интерфейс для чтения WAL
type WALReader interface {
ReadSince(index uint64) ([]WALEntry, error)
GetCurrentIndex() (uint64, error)
GetLastBackupIndex() uint64
SetLastBackupIndex(index uint64) error
GetSegments() ([]string, error)
ReadSegment(segmentPath string) ([]WALEntry, error)
}
// walReaderImpl реализация WALReader для работы с файлами WAL
type walReaderImpl struct {
segmentsDir string
lastBackupIndex atomic.Uint64
mu sync.RWMutex
logger LoggerInterface
}
// NewWALReaderImpl создаёт новый WALReader
func NewWALReaderImpl(segmentsDir string, logger LoggerInterface) WALReader {
wr := &walReaderImpl{
segmentsDir: segmentsDir,
logger: logger,
}
wr.loadLastBackupIndex()
return wr
}
// loadLastBackupIndex загружает последний индекс из файла
func (wr *walReaderImpl) loadLastBackupIndex() {
indexPath := filepath.Join(wr.segmentsDir, "last_backup_index.json")
data, err := os.ReadFile(indexPath)
if err != nil {
return
}
var meta map[string]uint64
if err := json.Unmarshal(data, &meta); err != nil {
return
}
if lastIdx, ok := meta["last_index"]; ok {
wr.lastBackupIndex.Store(lastIdx)
}
}
// ReadSince читает записи WAL с указанного индекса
func (wr *walReaderImpl) ReadSince(index uint64) ([]WALEntry, error) {
wr.mu.RLock()
defer wr.mu.RUnlock()
entries := make([]WALEntry, 0)
segments, err := wr.GetSegments()
if err != nil {
return nil, err
}
for _, segmentPath := range segments {
segEntries, err := wr.ReadSegment(segmentPath)
if err != nil {
if wr.logger != nil {
wr.logger.Warn(fmt.Sprintf("Failed to read segment %s: %v", segmentPath, err))
}
continue
}
for _, entry := range segEntries {
if entry.Index > index {
entries = append(entries, entry)
}
}
}
return entries, nil
}
// GetCurrentIndex возвращает текущий индекс WAL
func (wr *walReaderImpl) GetCurrentIndex() (uint64, error) {
segments, err := wr.GetSegments()
if err != nil {
return 0, err
}
if len(segments) == 0 {
return 0, nil
}
lastSegment := segments[len(segments)-1]
entries, err := wr.ReadSegment(lastSegment)
if err != nil {
return 0, err
}
if len(entries) == 0 {
return 0, nil
}
return entries[len(entries)-1].Index, nil
}
// GetLastBackupIndex возвращает последний сохранённый индекс бэкапа
func (wr *walReaderImpl) GetLastBackupIndex() uint64 {
return wr.lastBackupIndex.Load()
}
// SetLastBackupIndex устанавливает последний индекс бэкапа
func (wr *walReaderImpl) SetLastBackupIndex(index uint64) error {
wr.lastBackupIndex.Store(index)
indexPath := filepath.Join(wr.segmentsDir, "last_backup_index.json")
data, err := json.Marshal(map[string]uint64{"last_index": index})
if err != nil {
return err
}
return os.WriteFile(indexPath, data, 0644)
}
// GetSegments возвращает список сегментов WAL
func (wr *walReaderImpl) GetSegments() ([]string, error) {
if wr.segmentsDir == "" {
return nil, fmt.Errorf("segments directory not set")
}
pattern := filepath.Join(wr.segmentsDir, "wal_segment_*.log")
files, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
sort.Strings(files)
return files, nil
}
// ReadSegment читает сегмент WAL
func (wr *walReaderImpl) ReadSegment(segmentPath string) ([]WALEntry, error) {
data, err := os.ReadFile(segmentPath)
if err != nil {
return nil, err
}
entries := make([]WALEntry, 0)
pos := 0
for pos < len(data) {
if pos+4 > len(data) {
break
}
length := int(data[pos])<<24 | int(data[pos+1])<<16 | int(data[pos+2])<<8 | int(data[pos+3])
pos += 4
if pos+length > len(data) {
break
}
recordData := data[pos : pos+length]
pos += length
var record struct {
LSN uint64 `json:"lsn"`
Timestamp int64 `json:"timestamp"`
Type byte `json:"type"`
Data []byte `json:"data"`
}
if err := json.Unmarshal(recordData, &record); err != nil {
continue
}
entry := WALEntry{
Index: record.LSN,
Timestamp: record.Timestamp,
Type: fmt.Sprintf("%d", record.Type),
Data: record.Data,
}
entries = append(entries, entry)
}
return entries, nil
}
// ========== PersistenceConfig ==========
// PersistenceConfig конфигурация персистентного хранения
type PersistenceConfig struct {
DataDir string `json:"data_dir"`
CheckpointInterval time.Duration `json:"checkpoint_interval"`
MaxCheckpoints int `json:"max_checkpoints"`
CompressEnabled bool `json:"compress_enabled"`
SyncWrites bool `json:"sync_writes"`
WalPath string `json:"wal_path"`
UseWalForCheckpoint bool `json:"use_wal_for_checkpoint"`
AtomicWrites bool `json:"atomic_writes"`
}
// DefaultPersistenceConfig возвращает конфигурацию по умолчанию
func DefaultPersistenceConfig() *PersistenceConfig {
return &PersistenceConfig{
DataDir: "futriis_data",
CheckpointInterval: 5 * time.Minute,
MaxCheckpoints: 10,
CompressEnabled: true,
SyncWrites: true,
WalPath: "futriis.wal",
UseWalForCheckpoint: true,
AtomicWrites: true,
}
}
// DatabaseSnapshot представляет снапшот базы данных
type DatabaseSnapshot struct {
Name string `json:"name"`
Collections map[string]interface{} `json:"collections"`
CreatedAt int64 `json:"created_at"`
Version uint64 `json:"version"`
Checksum string `json:"checksum"`
WalLSN uint64 `json:"wal_lsn,omitempty"`
WalSegment string `json:"wal_segment,omitempty"`
}
// CheckpointRecoveryInfo содержит информацию о восстановлении из чекпоинта
type CheckpointRecoveryInfo struct {
CheckpointTime int64 `json:"checkpoint_time"`
CheckpointVersion uint64 `json:"checkpoint_version"`
WalLSN uint64 `json:"wal_lsn"`
AppliedWalCount int `json:"applied_wal_count"`
RestoredDocs int64 `json:"restored_docs"`
RestoredColls int `json:"restored_collections"`
DurationMs int64 `json:"duration_ms"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// PersistenceManager управляет персистентным хранением
type PersistenceManager struct {
config *PersistenceConfig
storage *Storage
logger LoggerInterface
mu sync.RWMutex
stopChan chan struct{}
wg sync.WaitGroup
lastCheckpoint int64
checkpointID atomic.Uint64
walManager interface{}
walReader WALReader
recoveryInfo *CheckpointRecoveryInfo
isRestoring atomic.Bool
}
// NewPersistenceManager создаёт новый менеджер персистентности
func NewPersistenceManager(config *PersistenceConfig, storage *Storage, logger LoggerInterface) *PersistenceManager {
if config == nil {
config = DefaultPersistenceConfig()
}
pm := &PersistenceManager{
config: config,
storage: storage,
logger: logger,
stopChan: make(chan struct{}),
lastCheckpoint: time.Now().UnixMilli(),
}
if err := os.MkdirAll(config.DataDir, 0755); err != nil {
if logger != nil {
logger.Error(fmt.Sprintf("Failed to create data directory: %v", err))
}
}
if config.UseWalForCheckpoint && config.WalPath != "" {
pm.walReader = NewWALReaderImpl(filepath.Dir(config.WalPath), logger)
}
return pm
}
// SetWALManager устанавливает WAL менеджер для синхронизации чекпоинтов
func (pm *PersistenceManager) SetWALManager(walManager interface{}) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.walManager = walManager
if walManager != nil && pm.config.UseWalForCheckpoint {
pm.walReader = NewWALReaderImpl(filepath.Dir(pm.config.WalPath), pm.logger)
}
}
// Start запускает фоновое сохранение чекпоинтов
func (pm *PersistenceManager) Start() {
pm.wg.Add(1)
go pm.checkpointLoop()
if pm.logger != nil {
pm.logger.Info(fmt.Sprintf("Persistence manager started, data dir: %s", pm.config.DataDir))
}
}
// Stop останавливает менеджер
func (pm *PersistenceManager) Stop() {
close(pm.stopChan)
pm.wg.Wait()
pm.SaveAll()
if pm.logger != nil {
pm.logger.Info("Persistence manager stopped")
}
}
// checkpointLoop периодически создаёт чекпоинты
func (pm *PersistenceManager) checkpointLoop() {
defer pm.wg.Done()
ticker := time.NewTicker(pm.config.CheckpointInterval)
defer ticker.Stop()
for {
select {
case <-pm.stopChan:
return
case <-ticker.C:
if pm.isRestoring.Load() {
if pm.logger != nil {
pm.logger.Info("Skipping checkpoint during restore")
}
continue
}
if err := pm.SaveAll(); err != nil {
if pm.logger != nil {
pm.logger.Error(fmt.Sprintf("Failed to save checkpoint: %v", err))
}
} else {
pm.cleanupOldCheckpoints()
}
}
}
}
// SaveDatabase сохраняет базу данных на диск с атомарной записью
func (pm *PersistenceManager) SaveDatabase(dbName string) error {
if pm.isRestoring.Load() {
return fmt.Errorf("cannot save checkpoint during restore")
}
pm.mu.Lock()
defer pm.mu.Unlock()
db, err := pm.storage.GetDatabase(dbName)
if err != nil {
return fmt.Errorf("database not found: %s", dbName)
}
var walLSN uint64
var walSegment string
if pm.config.UseWalForCheckpoint && pm.walManager != nil {
if segWal, ok := pm.walManager.(*SegmentedWALManager); ok {
if segWal.currentSegment != nil {
segWal.currentSegment.Writer.Flush()
RealFsync(segWal.currentSegment.File)
walLSN = segWal.currentSegment.EndLSN
walSegment = fmt.Sprintf("%d", segWal.currentSegment.ID)
}
}
}
snapshot := &DatabaseSnapshot{
Name: dbName,
Collections: make(map[string]interface{}),
CreatedAt: time.Now().UnixMilli(),
Version: pm.checkpointID.Add(1),
WalLSN: walLSN,
WalSegment: walSegment,
}
for _, collName := range db.ListCollections() {
coll, err := db.GetCollection(collName)
if err != nil {
continue
}
docs := coll.GetAllDocuments()
collData := make([]map[string]interface{}, 0, len(docs))
for _, doc := range docs {
collData = append(collData, map[string]interface{}{
"_id": doc.ID,
"fields": doc.GetFields(),
"created_at": doc.CreatedAt,
"updated_at": doc.UpdatedAt,
"deleted_at": doc.DeletedAt,
"version": doc.Version,
})
}
snapshot.Collections[collName] = collData
}
data, err := json.Marshal(snapshot)
if err != nil {
return err
}
// Вычисляем контрольную сумму
hash := sha256.Sum256(data)
snapshot.Checksum = hex.EncodeToString(hash[:])
// Пере-сериализуем с контрольной суммой
data, err = json.Marshal(snapshot)
if err != nil {
return err
}
if pm.config.CompressEnabled {
data, err = pm.compress(data)
if err != nil {
return err
}
}
// Атомарная запись с использованием временного файла
filename := pm.getSnapshotFilename(dbName, snapshot.Version)
if err := pm.atomicWriteFile(filename, data); err != nil {
return err
}
pm.lastCheckpoint = snapshot.CreatedAt
if pm.logger != nil {
pm.logger.Info(fmt.Sprintf("Saved database %s checkpoint %d (%d bytes, checksum: %s, WAL LSN: %d)",
dbName, snapshot.Version, len(data), snapshot.Checksum[:8], walLSN))
}
return nil
}
// atomicWriteFile выполняет атомарную запись файла
func (pm *PersistenceManager) atomicWriteFile(filename string, data []byte) error {
if pm.config.AtomicWrites {
// Создаём временный файл в той же директории
tempFile := filename + ".tmp"
if err := os.WriteFile(tempFile, data, 0644); err != nil {
return fmt.Errorf("failed to write temp file: %v", err)
}
// Синхронизируем временный файл
if pm.config.SyncWrites {
if f, err := os.OpenFile(tempFile, os.O_RDWR, 0644); err == nil {
RealFsync(f)
f.Close()
}
}
// Атомарное переименование
if err := os.Rename(tempFile, filename); err != nil {
os.Remove(tempFile)
return fmt.Errorf("failed to rename temp file: %v", err)
}
// Синхронизируем директорию
FsyncDir(pm.config.DataDir)
return nil
}
// Обычная запись (не атомарная)
return os.WriteFile(filename, data, 0644)
}
// SaveAll сохраняет все базы данных
func (pm *PersistenceManager) SaveAll() error {
if pm.isRestoring.Load() {
return fmt.Errorf("cannot save checkpoints during restore")
}
databases := pm.storage.ListDatabases()
var lastErr error
for _, dbName := range databases {
if err := pm.SaveDatabase(dbName); err != nil {
lastErr = err
if pm.logger != nil {
pm.logger.Error(fmt.Sprintf("Failed to save database %s: %v", dbName, err))
}
}
}
return lastErr
}
// LoadDatabase загружает базу данных с диска с применением WAL
func (pm *PersistenceManager) LoadDatabase(dbName string) error {
pm.isRestoring.Store(true)
defer pm.isRestoring.Store(false)
pm.mu.Lock()
defer pm.mu.Unlock()
startTime := time.Now()
recoveryInfo := &CheckpointRecoveryInfo{
CheckpointTime: time.Now().UnixMilli(),
Success: false,
}
snapshot, err := pm.findLatestSnapshot(dbName)
if err != nil {
recoveryInfo.Error = err.Error()
pm.recoveryInfo = recoveryInfo
return err
}
if snapshot == nil {
recoveryInfo.Error = "no snapshot found"
pm.recoveryInfo = recoveryInfo
return fmt.Errorf("no snapshot found for database: %s", dbName)
}
recoveryInfo.CheckpointVersion = snapshot.Version
recoveryInfo.WalLSN = snapshot.WalLSN
// Проверяем целостность чекпоинта
if err := pm.verifySnapshot(snapshot); err != nil {
recoveryInfo.Error = err.Error()
pm.recoveryInfo = recoveryInfo
return fmt.Errorf("snapshot integrity check failed: %v", err)
}
// Создаём базу данных если не существует
if !pm.storage.ExistsDatabase(dbName) {
if err := pm.storage.CreateDatabase(dbName); err != nil {
recoveryInfo.Error = err.Error()
pm.recoveryInfo = recoveryInfo
return err
}
}
db, err := pm.storage.GetDatabase(dbName)
if err != nil {
recoveryInfo.Error = err.Error()
pm.recoveryInfo = recoveryInfo
return err
}
// Восстанавливаем коллекции
restoredColls := 0
restoredDocs := int64(0)
for collName, collDataRaw := range snapshot.Collections {
collData, ok := collDataRaw.([]interface{})
if !ok {
continue
}
// Удаляем существующую коллекцию если есть
if _, err := db.GetCollection(collName); err == nil {
db.DropCollection(collName)
}
if err := db.CreateCollection(collName); err != nil {
if pm.logger != nil {
pm.logger.Error(fmt.Sprintf("Failed to create collection %s: %v", collName, err))
}
continue
}
coll, err := db.GetCollection(collName)
if err != nil {
continue
}
restoredColls++
for _, docRaw := range collData {
docMap, ok := docRaw.(map[string]interface{})
if !ok {
continue
}
docID, ok := docMap["_id"].(string)
if !ok {
continue
}
doc := NewDocumentWithID(docID)
if fields, ok := docMap["fields"].(map[string]interface{}); ok {
for k, v := range fields {
doc.SetField(k, v)
}
}
if createdAt, ok := docMap["created_at"].(float64); ok {
doc.CreatedAt = int64(createdAt)
} else if createdAt, ok := docMap["created_at"].(int64); ok {
doc.CreatedAt = createdAt
}
if updatedAt, ok := docMap["updated_at"].(float64); ok {
doc.UpdatedAt = int64(updatedAt)
} else if updatedAt, ok := docMap["updated_at"].(int64); ok {
doc.UpdatedAt = updatedAt
}
if deletedAt, ok := docMap["deleted_at"].(float64); ok {
doc.DeletedAt = int64(deletedAt)
} else if deletedAt, ok := docMap["deleted_at"].(int64); ok {
doc.DeletedAt = deletedAt
}
if version, ok := docMap["version"].(float64); ok {
doc.Version = uint64(version)
} else if version, ok := docMap["version"].(uint64); ok {
doc.Version = version
}
if err := coll.Insert(doc); err != nil {
if pm.logger != nil {
pm.logger.Warn(fmt.Sprintf("Failed to restore document %s: %v", docID, err))
}
continue
}
restoredDocs++
}
}
recoveryInfo.RestoredDocs = restoredDocs
recoveryInfo.RestoredColls = restoredColls
// Применяем WAL записи после чекпоинта
appliedWalCount := 0
if pm.config.UseWalForCheckpoint && snapshot.WalLSN > 0 && pm.walReader != nil {
currentLSN, err := pm.walReader.GetCurrentIndex()
if err == nil && currentLSN > snapshot.WalLSN {
if pm.logger != nil {
pm.logger.Info(fmt.Sprintf("Database %s: Applying %d WAL entries after checkpoint (LSN %d -> %d)",
dbName, currentLSN-snapshot.WalLSN, snapshot.WalLSN, currentLSN))
}
entries, err := pm.walReader.ReadSince(snapshot.WalLSN)
if err != nil {
if pm.logger != nil {
pm.logger.Error(fmt.Sprintf("Failed to read WAL entries: %v", err))
}
} else {
for _, entry := range entries {
if err := pm.applyWALEntry(db, entry); err != nil {
if pm.logger != nil {
pm.logger.Error(fmt.Sprintf("Failed to apply WAL entry %d: %v", entry.Index, err))
}
continue
}
appliedWalCount++
}
}
}
}
recoveryInfo.AppliedWalCount = appliedWalCount
recoveryInfo.DurationMs = time.Since(startTime).Milliseconds()
recoveryInfo.Success = true
pm.recoveryInfo = recoveryInfo
if pm.logger != nil {
pm.logger.Info(fmt.Sprintf("Loaded database %s from snapshot (version %d, WAL LSN: %d, applied %d WAL entries, %d docs, %d colls, duration: %dms)",
dbName, snapshot.Version, snapshot.WalLSN, appliedWalCount, restoredDocs, restoredColls, recoveryInfo.DurationMs))
}
return nil
}
// applyWALEntry применяет одну запись WAL к базе данных
func (pm *PersistenceManager) applyWALEntry(db *Database, entry WALEntry) error {
// Проверяем тип записи (1 = Transaction) - ИСПРАВЛЕНО
if entry.Type != "1" {
return nil
}
var txRecord struct {
ID uint64 `json:"id"`
State int32 `json:"state"`
Timestamp int64 `json:"timestamp"`
Operations []struct {
Type string `json:"type"`
Database string `json:"database"`
Collection string `json:"collection"`
DocumentID string `json:"document_id"`
Data map[string]interface{} `json:"data"`
Version uint64 `json:"version"`
} `json:"operations"`
}
if err := json.Unmarshal(entry.Data, &txRecord); err != nil {
return err
}
// Применяем только закоммиченные транзакции (state = 1)
if txRecord.State != 1 { // TransactionCommitted
return nil
}
for _, op := range txRecord.Operations {
if op.Database != db.name {
continue
}
coll, err := db.GetCollection(op.Collection)
if err != nil {
continue
}
switch op.Type {
case "insert":
doc := NewDocumentWithID(op.DocumentID)
for k, v := range op.Data {
doc.SetField(k, v)
}
doc.Version = op.Version
if err := coll.Insert(doc); err != nil {
return err
}
case "update":
if err := coll.Update(op.DocumentID, op.Data); err != nil {
return err
}
case "delete":
if err := coll.Delete(op.DocumentID); err != nil {
return err
}
}
}
return nil
}
// verifySnapshot проверяет целостность снапшота
func (pm *PersistenceManager) verifySnapshot(snapshot *DatabaseSnapshot) error {
if snapshot.Checksum == "" {
return fmt.Errorf("snapshot has no checksum")
}
// Пересоздаём данные без контрольной суммы для проверки
tempSnapshot := &DatabaseSnapshot{
Name: snapshot.Name,
Collections: snapshot.Collections,
CreatedAt: snapshot.CreatedAt,
Version: snapshot.Version,
WalLSN: snapshot.WalLSN,
WalSegment: snapshot.WalSegment,
}
data, err := json.Marshal(tempSnapshot)
if err != nil {
return fmt.Errorf("failed to marshal for checksum: %v", err)
}
hash := sha256.Sum256(data)
checksum := hex.EncodeToString(hash[:])
if checksum != snapshot.Checksum {
return fmt.Errorf("checksum mismatch: expected %s, got %s", snapshot.Checksum[:8], checksum[:8])
}
return nil
}
// LoadAll загружает все базы данных с диска
func (pm *PersistenceManager) LoadAll() error {
files, err := filepath.Glob(filepath.Join(pm.config.DataDir, "snapshot_*.json*"))
if err != nil {
return err
}
databases := make(map[string]bool)
for _, file := range files {
base := filepath.Base(file)
parts := strings.Split(base, "_")
if len(parts) >= 2 {
dbName := parts[1]
databases[dbName] = true
}
}
for dbName := range databases {
if err := pm.LoadDatabase(dbName); err != nil {
if pm.logger != nil {
pm.logger.Error(fmt.Sprintf("Failed to load database %s: %v", dbName, err))
}
}
}
return nil
}
// getSnapshotFilename возвращает имя файла для снапшота
func (pm *PersistenceManager) getSnapshotFilename(dbName string, version uint64) string {
filename := fmt.Sprintf("snapshot_%s_%d.json", dbName, version)
if pm.config.CompressEnabled {
filename += ".gz"
}
return filepath.Join(pm.config.DataDir, filename)
}
// findLatestSnapshot находит последний снапшот базы данных
func (pm *PersistenceManager) findLatestSnapshot(dbName string) (*DatabaseSnapshot, error) {
pattern := filepath.Join(pm.config.DataDir, fmt.Sprintf("snapshot_%s_*.json*", dbName))
files, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, nil
}
sort.Slice(files, func(i, j int) bool {
infoI, _ := os.Stat(files[i])
infoJ, _ := os.Stat(files[j])
if infoI == nil || infoJ == nil {
return false
}
return infoI.ModTime().After(infoJ.ModTime())
})
data, err := os.ReadFile(files[0])
if err != nil {
return nil, err
}
if pm.config.CompressEnabled && strings.HasSuffix(files[0], ".gz") {
data, err = pm.decompress(data)
if err != nil {
return nil, err
}
}
var snapshot DatabaseSnapshot
if err := json.Unmarshal(data, &snapshot); err != nil {
return nil, err
}
return &snapshot, nil
}
// compress сжимает данные с помощью gzip
func (pm *PersistenceManager) compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, fmt.Errorf("failed to write compressed data: %v", err)
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("failed to close gzip writer: %v", err)
}
return buf.Bytes(), nil
}
// decompress разжимает данные с помощью gzip
func (pm *PersistenceManager) decompress(data []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %v", err)
}
defer reader.Close()
var buf bytes.Buffer
if _, err := buf.ReadFrom(reader); err != nil {
return nil, fmt.Errorf("failed to decompress data: %v", err)
}
return buf.Bytes(), nil
}
// cleanupOldCheckpoints удаляет старые чекпоинты
func (pm *PersistenceManager) cleanupOldCheckpoints() {
pattern := filepath.Join(pm.config.DataDir, "snapshot_*.json*")
files, err := filepath.Glob(pattern)
if err != nil {
return
}
if len(files) <= pm.config.MaxCheckpoints {
return
}
sort.Slice(files, func(i, j int) bool {
infoI, _ := os.Stat(files[i])
infoJ, _ := os.Stat(files[j])
if infoI == nil || infoJ == nil {
return false
}
return infoI.ModTime().Before(infoJ.ModTime())
})
toDelete := files[:len(files)-pm.config.MaxCheckpoints]
for _, f := range toDelete {
os.Remove(f)
if pm.logger != nil {
pm.logger.Debug(fmt.Sprintf("Removed old checkpoint: %s", f))
}
}
}
// GetLastCheckpointInfo возвращает информацию о последнем чекпоинте
func (pm *PersistenceManager) GetLastCheckpointInfo() map[string]interface{} {
pm.mu.RLock()
defer pm.mu.RUnlock()
info := map[string]interface{}{
"last_checkpoint_time": pm.lastCheckpoint,
"last_checkpoint_time_str": time.UnixMilli(pm.lastCheckpoint).Format("2006-01-02 15:04:05.000"),
"checkpoint_id": pm.checkpointID.Load(),
"wal_path": pm.config.WalPath,
"use_wal": pm.config.UseWalForCheckpoint,
"atomic_writes": pm.config.AtomicWrites,
"is_restoring": pm.isRestoring.Load(),
}
if pm.recoveryInfo != nil {
info["last_recovery"] = pm.recoveryInfo
}
return info
}
// GetRecoveryInfo возвращает информацию о последнем восстановлении
func (pm *PersistenceManager) GetRecoveryInfo() *CheckpointRecoveryInfo {
pm.mu.RLock()
defer pm.mu.RUnlock()
return pm.recoveryInfo
}
// IsRestoring возвращает статус восстановления
func (pm *PersistenceManager) IsRestoring() bool {
return pm.isRestoring.Load()
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/storage/runtime_limits.go
// Назначение: Ограничения на размер коллекции/документа в рантайме
package storage
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// LoggerInterface определяет интерфейс для логирования
type LoggerInterface interface {
Debug(msg string)
Info(msg string)
Error(msg string)
Warn(msg string)
}
// RuntimeLimitsManager управляет runtime-ограничениями
type RuntimeLimitsManager struct {
mu sync.RWMutex
globalMaxDocSize int64 // Максимальный размер документа (байт)
globalMaxCollSize int64 // Максимальный размер коллекции (байт)
globalMaxDocsPerColl int64 // Максимальное количество документов в коллекции
collectionOverrides map[string]*CollectionLimits // Переопределения для коллекций
metrics *LimitMetrics
logger LoggerInterface
enabled bool
}
// CollectionLimits содержит лимиты для конкретной коллекции
type CollectionLimits struct {
MaxDocSize int64
MaxCollectionSize int64
MaxDocuments int64
LastUpdated int64
}
// LimitMetrics хранит метрики ограничений
type LimitMetrics struct {
RejectedBySize atomic.Uint64
RejectedByDocCount atomic.Uint64
RejectedByCollSize atomic.Uint64
LastCheckTime atomic.Int64
}
// RuntimeLimitsConfig содержит конфигурацию ограничений
type RuntimeLimitsConfig struct {
Enabled bool `json:"enabled"`
GlobalMaxDocSizeMB int `json:"global_max_doc_size_mb"`
GlobalMaxCollSizeMB int64 `json:"global_max_coll_size_mb"`
GlobalMaxDocsPerColl int64 `json:"global_max_docs_per_coll"`
}
// DefaultRuntimeLimitsConfig возвращает конфигурацию по умолчанию
func DefaultRuntimeLimitsConfig() *RuntimeLimitsConfig {
return &RuntimeLimitsConfig{
Enabled: true,
GlobalMaxDocSizeMB: 16, // 16 MB на документ
GlobalMaxCollSizeMB: 10240, // 10 GB на коллекцию
GlobalMaxDocsPerColl: 10000000, // 10 млн документов
}
}
// NewRuntimeLimitsManager создаёт новый менеджер ограничений
func NewRuntimeLimitsManager(cfg *RuntimeLimitsConfig, logger LoggerInterface) *RuntimeLimitsManager {
if cfg == nil {
cfg = DefaultRuntimeLimitsConfig()
}
rlm := &RuntimeLimitsManager{
globalMaxDocSize: int64(cfg.GlobalMaxDocSizeMB) * 1024 * 1024,
globalMaxCollSize: cfg.GlobalMaxCollSizeMB * 1024 * 1024,
globalMaxDocsPerColl: cfg.GlobalMaxDocsPerColl,
collectionOverrides: make(map[string]*CollectionLimits),
metrics: &LimitMetrics{},
logger: logger,
enabled: cfg.Enabled,
}
if logger != nil {
logger.Debug(fmt.Sprintf("Runtime limits manager initialized: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d",
cfg.GlobalMaxDocSizeMB, cfg.GlobalMaxCollSizeMB, cfg.GlobalMaxDocsPerColl))
}
return rlm
}
// ValidateDocumentSize проверяет размер документа
func (rlm *RuntimeLimitsManager) ValidateDocumentSize(dbName, collName string, docSize int64) error {
if !rlm.enabled {
return nil
}
// Проверяем переопределение для коллекции
limit := rlm.getCollectionLimit(dbName, collName)
maxSize := rlm.globalMaxDocSize
if limit != nil && limit.MaxDocSize > 0 {
maxSize = limit.MaxDocSize
}
if docSize > maxSize {
rlm.metrics.RejectedBySize.Add(1)
return fmt.Errorf("document size %d bytes exceeds limit %d bytes", docSize, maxSize)
}
return nil
}
// ValidateCollectionSize проверяет размер коллекции
func (rlm *RuntimeLimitsManager) ValidateCollectionSize(coll *Collection, newDocSize int64) error {
if !rlm.enabled {
return nil
}
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
maxSize := rlm.globalMaxCollSize
if limit != nil && limit.MaxCollectionSize > 0 {
maxSize = limit.MaxCollectionSize
}
currentSize := coll.Size()
if currentSize+newDocSize > maxSize {
rlm.metrics.RejectedByCollSize.Add(1)
return fmt.Errorf("collection size would exceed limit %d bytes (current: %d, new: %d)",
maxSize, currentSize, newDocSize)
}
return nil
}
// ValidateDocumentCount проверяет количество документов в коллекции
func (rlm *RuntimeLimitsManager) ValidateDocumentCount(coll *Collection) error {
if !rlm.enabled {
return nil
}
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
maxDocs := rlm.globalMaxDocsPerColl
if limit != nil && limit.MaxDocuments > 0 {
maxDocs = limit.MaxDocuments
}
currentCount := coll.Count()
if currentCount >= maxDocs {
rlm.metrics.RejectedByDocCount.Add(1)
return fmt.Errorf("collection has reached maximum document count %d", maxDocs)
}
return nil
}
// getCollectionLimit возвращает лимиты для коллекции
func (rlm *RuntimeLimitsManager) getCollectionLimit(dbName, collName string) *CollectionLimits {
rlm.mu.RLock()
defer rlm.mu.RUnlock()
key := fmt.Sprintf("%s.%s", dbName, collName)
if limits, ok := rlm.collectionOverrides[key]; ok {
return limits
}
return nil
}
// SetCollectionLimits устанавливает лимиты для коллекции
func (rlm *RuntimeLimitsManager) SetCollectionLimits(dbName, collName string, maxDocSizeMB int, maxCollSizeMB int64, maxDocuments int64) {
rlm.mu.Lock()
defer rlm.mu.Unlock()
key := fmt.Sprintf("%s.%s", dbName, collName)
rlm.collectionOverrides[key] = &CollectionLimits{
MaxDocSize: int64(maxDocSizeMB) * 1024 * 1024,
MaxCollectionSize: maxCollSizeMB * 1024 * 1024,
MaxDocuments: maxDocuments,
LastUpdated: time.Now().UnixMilli(),
}
if rlm.logger != nil {
rlm.logger.Info(fmt.Sprintf("Set limits for %s: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d",
key, maxDocSizeMB, maxCollSizeMB, maxDocuments))
}
}
// RemoveCollectionLimits удаляет переопределения для коллекции
func (rlm *RuntimeLimitsManager) RemoveCollectionLimits(dbName, collName string) {
rlm.mu.Lock()
defer rlm.mu.Unlock()
key := fmt.Sprintf("%s.%s", dbName, collName)
delete(rlm.collectionOverrides, key)
if rlm.logger != nil {
rlm.logger.Info(fmt.Sprintf("Removed limits override for %s", key))
}
}
// GetMetrics возвращает метрики
func (rlm *RuntimeLimitsManager) GetMetrics() map[string]interface{} {
return map[string]interface{}{
"rejected_by_size": rlm.metrics.RejectedBySize.Load(),
"rejected_by_doc_count": rlm.metrics.RejectedByDocCount.Load(),
"rejected_by_coll_size": rlm.metrics.RejectedByCollSize.Load(),
"global_max_doc_size_mb": rlm.globalMaxDocSize / (1024 * 1024),
"global_max_coll_size_mb": rlm.globalMaxCollSize / (1024 * 1024),
"global_max_docs_per_coll": rlm.globalMaxDocsPerColl,
"enabled": rlm.enabled,
}
}

File diff suppressed because it is too large Load Diff

816
internal/storage/trigger.go Normal file
View 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
}