first commit
This commit is contained in:
281
internal/acl/manger.go
Normal file
281
internal/acl/manger.go
Normal file
@@ -0,0 +1,281 @@
|
||||
// Файл: 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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
sessionRoles sync.Map // map[string]string - sessionID -> role
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
// Создаём сессию
|
||||
sessionID := uuid.New().String()
|
||||
m.sessionRoles.Store(sessionID, user.Roles)
|
||||
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// Logout завершает сессию
|
||||
func (m *ACLManager) Logout(sessionID string) {
|
||||
m.sessionRoles.Delete(sessionID)
|
||||
}
|
||||
|
||||
// CheckPermission проверяет разрешение для сессии
|
||||
func (m *ACLManager) CheckPermission(sessionID, database, collection, operation string) bool {
|
||||
rolesVal, ok := m.sessionRoles.Load(sessionID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
roles := rolesVal.([]string)
|
||||
for _, roleName := range 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 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверка ресурса
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
user.Roles = append(user.Roles, roleName)
|
||||
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
|
||||
}
|
||||
|
||||
// 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, "*"}
|
||||
}
|
||||
569
internal/api/http.go
Normal file
569
internal/api/http.go
Normal file
@@ -0,0 +1,569 @@
|
||||
// Файл: internal/api/http.go
|
||||
// Назначение: HTTP RESTful API для взаимодействия с СУБД через curl.
|
||||
// Поддерживает CRUD операции, управление индексами, ACL и ограничениями.
|
||||
// Реализован с минимальными блокировками, использует wait-free структуры.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/acl"
|
||||
"futriis/internal/cluster"
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/storage"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
store *storage.Storage
|
||||
coordinator *cluster.RaftCoordinator
|
||||
aclManager *acl.ACLManager
|
||||
logger *log.Logger
|
||||
server *http.Server
|
||||
port int
|
||||
}
|
||||
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewHTTPServer создаёт новый HTTP сервер
|
||||
func NewHTTPServer(port int, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *HTTPServer {
|
||||
s := &HTTPServer{
|
||||
store: store,
|
||||
coordinator: coord,
|
||||
aclManager: aclMgr,
|
||||
logger: logger,
|
||||
port: port,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Middleware для аутентификации
|
||||
mux.HandleFunc("/api/auth/login", s.handleLogin)
|
||||
mux.HandleFunc("/api/auth/logout", s.handleLogout)
|
||||
|
||||
// CRUD операции
|
||||
mux.HandleFunc("/api/db/", s.handleDatabaseRequest)
|
||||
|
||||
// Индексы
|
||||
mux.HandleFunc("/api/index/", s.handleIndexRequest)
|
||||
|
||||
// ACL
|
||||
mux.HandleFunc("/api/acl/", s.handleACLRequest)
|
||||
|
||||
// Constraints
|
||||
mux.HandleFunc("/api/constraint/", s.handleConstraintRequest)
|
||||
|
||||
// Cluster
|
||||
mux.HandleFunc("/api/cluster/", s.handleClusterRequest)
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start запускает HTTP сервер
|
||||
func (s *HTTPServer) Start() error {
|
||||
s.logger.Info("Starting HTTP API server on port " + strconv.Itoa(s.port))
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Stop останавливает HTTP сервер
|
||||
func (s *HTTPServer) Stop() error {
|
||||
return s.server.Close()
|
||||
}
|
||||
|
||||
// handleLogin обрабатывает аутентификацию
|
||||
func (s *HTTPServer) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var creds struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
||||
s.sendError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, err := s.aclManager.Authenticate(creds.Username, creds.Password)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"session_id": sessionID})
|
||||
}
|
||||
|
||||
// handleLogout обрабатывает выход
|
||||
func (s *HTTPServer) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if sessionID != "" {
|
||||
s.aclManager.Logout(sessionID)
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "logged out"})
|
||||
}
|
||||
|
||||
// handleDatabaseRequest обрабатывает запросы к БД
|
||||
func (s *HTTPServer) handleDatabaseRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// URL: /api/db/{database}/{collection}/{document_id}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/db/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 2 {
|
||||
s.sendError(w, "Invalid path. Use /api/db/{database}/{collection}[/{id}]", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
database := parts[0]
|
||||
collection := parts[1]
|
||||
docID := ""
|
||||
if len(parts) > 2 {
|
||||
docID = parts[2]
|
||||
}
|
||||
|
||||
// Проверка аутентификации
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if sessionID == "" {
|
||||
s.sendError(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.handleGetDocument(w, r, sessionID, database, collection, docID)
|
||||
case http.MethodPost:
|
||||
s.handleInsertDocument(w, r, sessionID, database, collection)
|
||||
case http.MethodPut:
|
||||
s.handleUpdateDocument(w, r, sessionID, database, collection, docID)
|
||||
case http.MethodDelete:
|
||||
s.handleDeleteDocument(w, r, sessionID, database, collection, docID)
|
||||
default:
|
||||
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetDocument обрабатывает GET запросы
|
||||
func (s *HTTPServer) handleGetDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
|
||||
// Проверка прав
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "read") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Поиск по индексу или ID
|
||||
query := r.URL.Query()
|
||||
if indexName := query.Get("index"); indexName != "" {
|
||||
indexValue := query.Get("value")
|
||||
docs, err := coll.FindByIndex(indexName, indexValue)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, docs)
|
||||
return
|
||||
}
|
||||
|
||||
if docID == "" {
|
||||
// Возвращаем все документы
|
||||
docs := coll.GetAllDocuments()
|
||||
s.sendSuccess(w, docs)
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := coll.Find(docID)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, doc)
|
||||
}
|
||||
|
||||
// handleInsertDocument обрабатывает POST запросы
|
||||
func (s *HTTPServer) handleInsertDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection string) {
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "write") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
// Создаём БД если не существует
|
||||
if err := s.store.CreateDatabase(database); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
db, _ = s.store.GetDatabase(database)
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
if err := db.CreateCollection(collection); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
coll, _ = db.GetCollection(collection)
|
||||
}
|
||||
|
||||
var doc map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&doc); err != nil {
|
||||
s.sendError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := coll.InsertFromMap(doc); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"status": "inserted"})
|
||||
}
|
||||
|
||||
// handleUpdateDocument обрабатывает PUT запросы
|
||||
func (s *HTTPServer) handleUpdateDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
|
||||
if docID == "" {
|
||||
s.sendError(w, "Document ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "write") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
s.sendError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := coll.Update(docID, updates); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
// handleDeleteDocument обрабатывает DELETE запросы
|
||||
func (s *HTTPServer) handleDeleteDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
|
||||
if docID == "" {
|
||||
s.sendError(w, "Document ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "delete") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := coll.Delete(docID); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// handleIndexRequest обрабатывает запросы к индексам
|
||||
func (s *HTTPServer) handleIndexRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// URL: /api/index/{database}/{collection}/{action}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/index/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 3 {
|
||||
s.sendError(w, "Invalid path. Use /api/index/{database}/{collection}/{action}", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
database := parts[0]
|
||||
collection := parts[1]
|
||||
action := parts[2]
|
||||
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "list":
|
||||
indexes := coll.GetIndexes()
|
||||
s.sendSuccess(w, indexes)
|
||||
|
||||
case "create":
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Fields []string `json:"fields"`
|
||||
Unique bool `json:"unique"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "index created"})
|
||||
|
||||
case "drop":
|
||||
indexName := parts[3]
|
||||
if err := coll.DropIndex(indexName); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "index dropped"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Unknown action", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// handleACLRequest обрабатывает запросы ACL
|
||||
func (s *HTTPServer) handleACLRequest(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/acl/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 1 {
|
||||
s.sendError(w, "Invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
action := parts[0]
|
||||
|
||||
switch action {
|
||||
case "users":
|
||||
users := s.aclManager.ListUsers()
|
||||
s.sendSuccess(w, users)
|
||||
|
||||
case "user":
|
||||
if len(parts) < 2 {
|
||||
s.sendError(w, "Username required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
username := parts[1]
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
var req struct {
|
||||
Password string `json:"password"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := s.aclManager.CreateUser(username, req.Password, req.Roles); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "user created"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
case "roles":
|
||||
roles := s.aclManager.ListRoles()
|
||||
s.sendSuccess(w, roles)
|
||||
|
||||
case "grant":
|
||||
if len(parts) < 3 {
|
||||
s.sendError(w, "Role and permission required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
roleName := parts[1]
|
||||
permission := parts[2]
|
||||
if err := s.aclManager.GrantPermission(roleName, permission); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "permission granted"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Unknown action", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConstraintRequest обрабатывает запросы к ограничениям
|
||||
func (s *HTTPServer) handleConstraintRequest(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/constraint/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 3 {
|
||||
s.sendError(w, "Invalid path. Use /api/constraint/{database}/{collection}/{action}", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
database := parts[0]
|
||||
collection := parts[1]
|
||||
action := parts[2]
|
||||
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "required":
|
||||
if len(parts) < 4 {
|
||||
s.sendError(w, "Field name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
coll.AddRequiredField(field)
|
||||
s.sendSuccess(w, map[string]string{"status": "required field added"})
|
||||
|
||||
case "unique":
|
||||
if len(parts) < 4 {
|
||||
s.sendError(w, "Field name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
coll.AddUniqueConstraint(field)
|
||||
s.sendSuccess(w, map[string]string{"status": "unique constraint added"})
|
||||
|
||||
case "min":
|
||||
if len(parts) < 5 {
|
||||
s.sendError(w, "Field name and value required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
minVal, _ := strconv.ParseFloat(parts[4], 64)
|
||||
coll.AddMinConstraint(field, minVal)
|
||||
s.sendSuccess(w, map[string]string{"status": "min constraint added"})
|
||||
|
||||
case "max":
|
||||
if len(parts) < 5 {
|
||||
s.sendError(w, "Field name and value required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
maxVal, _ := strconv.ParseFloat(parts[4], 64)
|
||||
coll.AddMaxConstraint(field, maxVal)
|
||||
s.sendSuccess(w, map[string]string{"status": "max constraint added"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Unknown action", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// handleClusterRequest обрабатывает запросы к кластеру
|
||||
func (s *HTTPServer) handleClusterRequest(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if s.coordinator == nil {
|
||||
s.sendError(w, "Cluster not available", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
status := s.coordinator.GetClusterStatus()
|
||||
s.sendSuccess(w, status)
|
||||
}
|
||||
|
||||
// sendSuccess отправляет успешный ответ
|
||||
func (s *HTTPServer) sendSuccess(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// sendError отправляет ответ с ошибкой
|
||||
func (s *HTTPServer) sendError(w http.ResponseWriter, errMsg string, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: false,
|
||||
Error: errMsg,
|
||||
})
|
||||
}
|
||||
379
internal/cluster/node.go
Normal file
379
internal/cluster/node.go
Normal file
@@ -0,0 +1,379 @@
|
||||
// Файл: internal/cluster/node.go
|
||||
// Назначение: Реализация узла кластера (node) для распределённой СУБД.
|
||||
// Реализация узла кластера (node) для распределённой СУБД.
|
||||
// Полностью lock-free с использованием атомарных операций.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/storage"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// NodeStatus представляет состояние узла кластера
|
||||
type NodeStatus int32
|
||||
|
||||
const (
|
||||
StatusOffline NodeStatus = iota
|
||||
StatusActive
|
||||
StatusSyncing
|
||||
StatusFailed
|
||||
)
|
||||
|
||||
// Node представляет отдельный узел в распределённой системе
|
||||
type Node struct {
|
||||
ID string // Уникальный идентификатор узла
|
||||
IP string // IP-адрес узла
|
||||
Port int // Порт для коммуникации
|
||||
Status atomic.Int32 // Атомарный статус узла (NodeStatus)
|
||||
Storage *storage.Storage
|
||||
logger *log.Logger
|
||||
coordinator *RaftCoordinator // Ссылка на координатора (теперь RaftCoordinator)
|
||||
lastSeen atomic.Int64 // Время последнего heartbeat (Unix nano)
|
||||
incomingConn chan net.Conn // Канал для входящих соединений (wait-free)
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NodeConfig содержит конфигурацию для создания узла
|
||||
type NodeConfig struct {
|
||||
IP string
|
||||
Port int
|
||||
Storage *storage.Storage
|
||||
Logger *log.Logger
|
||||
Coordinator *RaftCoordinator
|
||||
}
|
||||
|
||||
// NewNode создаёт новый экземпляр узла кластера
|
||||
func NewNode(ip string, port int, store *storage.Storage, logger *log.Logger) *Node {
|
||||
node := &Node{
|
||||
ID: uuid.New().String(),
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Storage: store,
|
||||
logger: logger,
|
||||
incomingConn: make(chan net.Conn, 1000), // Буферизованный канал для wait-free приёма
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
node.Status.Store(int32(StatusActive))
|
||||
node.lastSeen.Store(time.Now().UnixNano())
|
||||
|
||||
// Запуск сервера для приёма межузловых соединений
|
||||
go node.startTCPServer()
|
||||
|
||||
// Запуск обработчика входящих соединений
|
||||
go node.handleIncomingConnections()
|
||||
|
||||
// Запуск heartbeat-отправки (если координатор известен)
|
||||
go node.heartbeatLoop()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// startTCPServer запускает TCP-сервер для приёма запросов от других узлов
|
||||
func (n *Node) startTCPServer() {
|
||||
addr := fmt.Sprintf("%s:%d", n.IP, n.Port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to start TCP server: %v", n.ID, err))
|
||||
}
|
||||
n.Status.Store(int32(StatusFailed))
|
||||
return
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s listening on %s", n.ID, addr))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
default:
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s accept error: %v", n.ID, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Неблокирующая отправка в канал
|
||||
select {
|
||||
case n.incomingConn <- conn:
|
||||
default:
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Node %s incoming connection queue full, dropping connection", n.ID))
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleIncomingConnections обрабатывает входящие соединения wait-free способом
|
||||
func (n *Node) handleIncomingConnections() {
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
case conn := <-n.incomingConn:
|
||||
go n.handleNodeRequest(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleNodeRequest обрабатывает конкретный запрос от другого узла
|
||||
func (n *Node) handleNodeRequest(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
decoder := json.NewDecoder(conn)
|
||||
var req NodeRequest
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to decode request: %v", n.ID, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем время последнего контакта
|
||||
n.lastSeen.Store(time.Now().UnixNano())
|
||||
|
||||
// Маршрутизация запроса в зависимости от типа
|
||||
switch req.Type {
|
||||
case "replicate":
|
||||
n.handleReplicateRequest(req.Data)
|
||||
case "query":
|
||||
n.handleQueryRequest(req.Data, conn)
|
||||
case "sync":
|
||||
n.handleSyncRequest(req.Data, conn)
|
||||
default:
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Node %s unknown request type: %s", n.ID, req.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleReplicateRequest обрабатывает запрос на репликацию документа
|
||||
func (n *Node) handleReplicateRequest(data []byte) {
|
||||
var repData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
Document map[string]interface{} `json:"document"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &repData); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to unmarshal replicate data: %v", n.ID, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := n.Storage.GetDatabase(repData.Database)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s database not found for replication: %s", n.ID, repData.Database))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем коллекцию
|
||||
coll, err := db.GetCollection(repData.Collection)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s collection not found for replication: %s", n.ID, repData.Collection))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Создаём документ и вставляем
|
||||
doc := &storage.Document{
|
||||
ID: repData.Document["_id"].(string),
|
||||
Fields: repData.Document,
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to replicate document: %v", n.ID, err))
|
||||
}
|
||||
} else {
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Node %s replicated document %s", n.ID, doc.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleQueryRequest обрабатывает запрос на чтение данных с узла
|
||||
func (n *Node) handleQueryRequest(data []byte, conn net.Conn) {
|
||||
var queryData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
DocumentID string `json:"document_id"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &queryData); err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := n.Storage.GetDatabase(queryData.Database)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем коллекцию
|
||||
coll, err := db.GetCollection(queryData.Collection)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Находим документ
|
||||
doc, err := coll.Find(queryData.DocumentID)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Отправляем успешный ответ
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
"data": doc,
|
||||
}
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// handleSyncRequest обрабатывает запрос на синхронизацию всей коллекции
|
||||
func (n *Node) handleSyncRequest(data []byte, conn net.Conn) {
|
||||
var syncData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &syncData); err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := n.Storage.GetDatabase(syncData.Database)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем коллекцию
|
||||
coll, err := db.GetCollection(syncData.Collection)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем все документы
|
||||
docs := coll.GetAllDocuments()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
"docs": docs,
|
||||
"count": len(docs),
|
||||
}
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// sendErrorResponse отправляет ошибку в ответ на запрос
|
||||
func (n *Node) sendErrorResponse(conn net.Conn, errMsg string) {
|
||||
response := map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": errMsg,
|
||||
}
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// heartbeatLoop отправляет периодические сигналы жизни координатору
|
||||
func (n *Node) heartbeatLoop() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if n.coordinator != nil {
|
||||
n.coordinator.SendHeartbeat(n.ID)
|
||||
n.lastSeen.Store(time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetNodeStatus возвращает текущий статус узла (атомарно)
|
||||
func (n *Node) GetNodeStatus() NodeStatus {
|
||||
return NodeStatus(n.Status.Load())
|
||||
}
|
||||
|
||||
// IsActive проверяет, активен ли узел
|
||||
func (n *Node) IsActive() bool {
|
||||
return NodeStatus(n.Status.Load()) == StatusActive
|
||||
}
|
||||
|
||||
// SetCoordinator устанавливает координатора для узла
|
||||
func (n *Node) SetCoordinator(coord *RaftCoordinator) {
|
||||
n.coordinator = coord
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s connected to coordinator", n.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает работу узла
|
||||
func (n *Node) Stop() {
|
||||
n.Status.Store(int32(StatusOffline))
|
||||
close(n.stopChan)
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s stopped", n.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAddress возвращает адрес узла в формате "ip:port"
|
||||
func (n *Node) GetAddress() string {
|
||||
return fmt.Sprintf("%s:%d", n.IP, n.Port)
|
||||
}
|
||||
|
||||
// ReplicateDocument отправляет документ на репликацию всем активным узлам
|
||||
func (n *Node) ReplicateDocument(database, collection string, doc *storage.Document) error {
|
||||
if n.coordinator == nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Warn("No coordinator set, skipping replication")
|
||||
}
|
||||
return fmt.Errorf("no coordinator set")
|
||||
}
|
||||
|
||||
// Получаем список всех узлов от координатора
|
||||
nodes := n.coordinator.GetActiveNodes()
|
||||
|
||||
for _, nodeInfo := range nodes {
|
||||
if nodeInfo.ID == n.ID {
|
||||
continue // Пропускаем себя
|
||||
}
|
||||
|
||||
// В реальной реализации здесь была бы отправка на узел
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Would replicate to node %s", nodeInfo.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
722
internal/cluster/raft_coordinator.go
Normal file
722
internal/cluster/raft_coordinator.go
Normal file
@@ -0,0 +1,722 @@
|
||||
// Файл: internal/cluster/raft_coordinator.go
|
||||
// Назначение: Реализация координатора распределённого кластера на основе Raft консенсус-алгоритма.
|
||||
// Обеспечивает управление узлами кластера, выборы лидера, репликацию данных и отказоустойчивость.
|
||||
// Поддерживает как одноузловой режим работы, так и многокластерную конфигурацию с синхронной/асинхронной репликацией.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/raft"
|
||||
raftboltdb "github.com/hashicorp/raft-boltdb/v2"
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/config"
|
||||
)
|
||||
|
||||
// RaftClusterState представляет состояние кластера для Raft FSM
|
||||
type RaftClusterState struct {
|
||||
Nodes map[string]*NodeInfo `json:"nodes"`
|
||||
ReplicationFactor int32 `json:"replication_factor"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// RaftCoordinator реализует координацию кластера через Raft
|
||||
type RaftCoordinator struct {
|
||||
raft *raft.Raft
|
||||
fsm *RaftFSM
|
||||
address string
|
||||
raftAddr string
|
||||
clusterName string
|
||||
logger *log.Logger
|
||||
config *config.Config
|
||||
stopChan chan struct{}
|
||||
nodes sync.Map
|
||||
replicationFactor atomic.Int32
|
||||
replicationEnabled bool
|
||||
masterMasterEnabled bool
|
||||
syncReplication bool
|
||||
isLeader atomic.Bool
|
||||
leaderMonitor chan bool
|
||||
}
|
||||
|
||||
// RaftFSM реализует конечный автомат для Raft
|
||||
type RaftFSM struct {
|
||||
state *RaftClusterState
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NodeRegistrationCommand команда регистрации узла
|
||||
type NodeRegistrationCommand struct {
|
||||
Type string `json:"type"`
|
||||
Node NodeInfo `json:"node,omitempty"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
Factor int32 `json:"factor,omitempty"`
|
||||
}
|
||||
|
||||
// getLocalIP получает локальный IP адрес
|
||||
func getLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
// NewRaftCoordinator создаёт новый Raft координатор
|
||||
func NewRaftCoordinator(cfg *config.Config, logger *log.Logger) (*RaftCoordinator, error) {
|
||||
// Используем IP из конфига или автоматически определяем
|
||||
nodeIP := cfg.Cluster.NodeIP
|
||||
if nodeIP == "" || nodeIP == "0.0.0.0" {
|
||||
nodeIP = getLocalIP()
|
||||
}
|
||||
|
||||
raftAddr := fmt.Sprintf("%s:%d", nodeIP, cfg.Cluster.RaftPort)
|
||||
|
||||
logger.Debug(fmt.Sprintf("Creating Raft coordinator at %s", raftAddr))
|
||||
|
||||
rc := &RaftCoordinator{
|
||||
address: fmt.Sprintf("%s:%d", nodeIP, cfg.Cluster.NodePort),
|
||||
raftAddr: raftAddr,
|
||||
clusterName: cfg.Cluster.Name,
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
stopChan: make(chan struct{}),
|
||||
leaderMonitor: make(chan bool, 1),
|
||||
replicationEnabled: cfg.Replication.Enabled,
|
||||
masterMasterEnabled: cfg.Replication.MasterMaster,
|
||||
syncReplication: cfg.Replication.SyncReplication,
|
||||
}
|
||||
rc.replicationFactor.Store(int32(3))
|
||||
|
||||
// Создаём FSM
|
||||
rc.fsm = &RaftFSM{
|
||||
state: &RaftClusterState{
|
||||
Nodes: make(map[string]*NodeInfo),
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Настраиваем Raft
|
||||
raftConfig := raft.DefaultConfig()
|
||||
raftConfig.LocalID = raft.ServerID(fmt.Sprintf("%s-%s", rc.clusterName, nodeIP))
|
||||
raftConfig.HeartbeatTimeout = 1000 * time.Millisecond
|
||||
raftConfig.ElectionTimeout = 1000 * time.Millisecond
|
||||
raftConfig.CommitTimeout = 500 * time.Millisecond
|
||||
raftConfig.LeaderLeaseTimeout = 500 * time.Millisecond
|
||||
|
||||
// Для одноузлового кластера используем специальные настройки и подавляем предупреждения
|
||||
singleNodeMode := len(cfg.Cluster.Nodes) <= 1 || cfg.Cluster.Bootstrap
|
||||
if singleNodeMode {
|
||||
raftConfig.HeartbeatTimeout = 500 * time.Millisecond
|
||||
raftConfig.ElectionTimeout = 500 * time.Millisecond
|
||||
raftConfig.LeaderLeaseTimeout = 500 * time.Millisecond
|
||||
// Подавляем вывод предупреждений для одноузлового режима
|
||||
raftConfig.LogOutput = io.Discard
|
||||
logger.Debug("Running in single-node mode (warnings suppressed)")
|
||||
} else {
|
||||
raftConfig.LogOutput = os.Stderr
|
||||
}
|
||||
|
||||
// Создаём директорию для Raft данных
|
||||
dataDir := cfg.Cluster.RaftDataDir
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create raft data dir: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("Raft data directory: %s", dataDir))
|
||||
|
||||
// Создаём хранилище для логов
|
||||
logStore, err := raftboltdb.NewBoltStore(filepath.Join(dataDir, "raft-log.bolt"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log store: %v", err)
|
||||
}
|
||||
|
||||
// Создаём хранилище для стабильных данных
|
||||
stableStore, err := raftboltdb.NewBoltStore(filepath.Join(dataDir, "raft-stable.bolt"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stable store: %v", err)
|
||||
}
|
||||
|
||||
// Создаём снапшот хранилище
|
||||
snapshotStore, err := raft.NewFileSnapshotStore(dataDir, 2, os.Stderr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create snapshot store: %v", err)
|
||||
}
|
||||
|
||||
// Создаём транспорт
|
||||
transport, err := raft.NewTCPTransport(raftAddr, nil, 3, 10*time.Second, os.Stderr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport: %v", err)
|
||||
}
|
||||
|
||||
// Создаём Raft инстанс
|
||||
r, err := raft.NewRaft(raftConfig, rc.fsm, logStore, stableStore, snapshotStore, transport)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create raft: %v", err)
|
||||
}
|
||||
|
||||
rc.raft = r
|
||||
|
||||
// Ждём некоторое время для инициализации Raft
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Проверяем, нужно ли делать bootstrap
|
||||
bootstrapPath := filepath.Join(dataDir, "raft-log.bolt")
|
||||
_, statErr := os.Stat(bootstrapPath)
|
||||
needsBootstrap := os.IsNotExist(statErr)
|
||||
|
||||
if needsBootstrap && singleNodeMode {
|
||||
logger.Debug("Bootstrapping single-node cluster...")
|
||||
|
||||
configuration := raft.Configuration{
|
||||
Servers: []raft.Server{
|
||||
{
|
||||
ID: raftConfig.LocalID,
|
||||
Address: transport.LocalAddr(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
future := r.BootstrapCluster(configuration)
|
||||
if err := future.Error(); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Bootstrap error: %v", err))
|
||||
} else {
|
||||
logger.Debug("Single-node cluster bootstrapped successfully")
|
||||
}
|
||||
|
||||
// Ждём после bootstrap
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Принудительно становимся лидером в одноузловом режиме
|
||||
logger.Debug("Setting as leader in single-node mode...")
|
||||
rc.isLeader.Store(true)
|
||||
|
||||
} else if needsBootstrap && len(cfg.Cluster.Nodes) > 1 {
|
||||
logger.Debug("Bootstrapping multi-node cluster...")
|
||||
|
||||
servers := make([]raft.Server, 0, len(cfg.Cluster.Nodes))
|
||||
for i, nodeAddr := range cfg.Cluster.Nodes {
|
||||
serverID := raft.ServerID(fmt.Sprintf("%s-node%d", rc.clusterName, i+1))
|
||||
servers = append(servers, raft.Server{
|
||||
ID: serverID,
|
||||
Address: raft.ServerAddress(nodeAddr),
|
||||
})
|
||||
}
|
||||
|
||||
configuration := raft.Configuration{
|
||||
Servers: servers,
|
||||
}
|
||||
|
||||
future := r.BootstrapCluster(configuration)
|
||||
if err := future.Error(); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Bootstrap error: %v", err))
|
||||
} else {
|
||||
logger.Debug("Multi-node cluster bootstrapped successfully")
|
||||
}
|
||||
|
||||
// Запускаем мониторинг лидера
|
||||
go rc.monitorLeadership()
|
||||
|
||||
// Ждём выборов лидера
|
||||
logger.Debug("Waiting for leader election...")
|
||||
timeout := time.After(5 * time.Second)
|
||||
leaderElected := false
|
||||
|
||||
for !leaderElected {
|
||||
select {
|
||||
case isLeader := <-rc.leaderMonitor:
|
||||
if isLeader {
|
||||
leaderElected = true
|
||||
rc.isLeader.Store(true)
|
||||
logger.Debug("This node is now the cluster leader")
|
||||
}
|
||||
case <-timeout:
|
||||
logger.Warn("Leader election timeout")
|
||||
leaderElected = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Существующее состояние, просто подключаемся
|
||||
logger.Debug("Existing Raft state found, joining cluster...")
|
||||
go rc.monitorLeadership()
|
||||
|
||||
// Проверяем, не являемся ли мы лидером
|
||||
if r.State() == raft.Leader {
|
||||
rc.isLeader.Store(true)
|
||||
logger.Debug("This node is the cluster leader")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("Raft coordinator started at %s, IsLeader: %v", raftAddr, rc.isLeader.Load()))
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// monitorLeadership отслеживает изменения лидера
|
||||
func (rc *RaftCoordinator) monitorLeadership() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
wasLeader := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rc.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if rc.raft == nil {
|
||||
continue
|
||||
}
|
||||
isLeader := rc.raft.State() == raft.Leader
|
||||
if isLeader != wasLeader {
|
||||
wasLeader = isLeader
|
||||
select {
|
||||
case rc.leaderMonitor <- isLeader:
|
||||
default:
|
||||
}
|
||||
if isLeader {
|
||||
rc.isLeader.Store(true)
|
||||
rc.logger.Debug("Leadership acquired")
|
||||
} else {
|
||||
rc.isLeader.Store(false)
|
||||
rc.logger.Debug("Leadership lost")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply применяет команду к FSM
|
||||
func (f *RaftFSM) Apply(log *raft.Log) interface{} {
|
||||
var cmd NodeRegistrationCommand
|
||||
if err := json.Unmarshal(log.Data, &cmd); err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to unmarshal raft command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.mu.Lock()
|
||||
defer f.state.mu.Unlock()
|
||||
|
||||
switch cmd.Type {
|
||||
case "register":
|
||||
f.state.Nodes[cmd.Node.ID] = &cmd.Node
|
||||
f.logger.Debug(fmt.Sprintf("Raft: Node registered: %s", cmd.Node.ID))
|
||||
case "remove":
|
||||
delete(f.state.Nodes, cmd.NodeID)
|
||||
f.logger.Debug(fmt.Sprintf("Raft: Node removed: %s", cmd.NodeID))
|
||||
case "set_replication_factor":
|
||||
f.state.ReplicationFactor = cmd.Factor
|
||||
f.logger.Debug(fmt.Sprintf("Raft: Replication factor set to %d", cmd.Factor))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot реализует создание снапшота
|
||||
func (f *RaftFSM) Snapshot() (raft.FSMSnapshot, error) {
|
||||
f.state.mu.RLock()
|
||||
defer f.state.mu.RUnlock()
|
||||
|
||||
stateCopy := &RaftClusterState{
|
||||
Nodes: make(map[string]*NodeInfo),
|
||||
ReplicationFactor: f.state.ReplicationFactor,
|
||||
}
|
||||
for k, v := range f.state.Nodes {
|
||||
stateCopy.Nodes[k] = v
|
||||
}
|
||||
|
||||
return &RaftSnapshot{state: stateCopy}, nil
|
||||
}
|
||||
|
||||
// Restore восстанавливает состояние из снапшота
|
||||
func (f *RaftFSM) Restore(snapshot io.ReadCloser) error {
|
||||
defer snapshot.Close()
|
||||
|
||||
var state RaftClusterState
|
||||
decoder := json.NewDecoder(snapshot)
|
||||
if err := decoder.Decode(&state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.mu.Lock()
|
||||
defer f.state.mu.Unlock()
|
||||
f.state.Nodes = state.Nodes
|
||||
f.state.ReplicationFactor = state.ReplicationFactor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RaftSnapshot реализует интерфейс FSMSnapshot
|
||||
type RaftSnapshot struct {
|
||||
state *RaftClusterState
|
||||
}
|
||||
|
||||
// Persist сохраняет снапшот
|
||||
func (s *RaftSnapshot) Persist(sink raft.SnapshotSink) error {
|
||||
err := func() error {
|
||||
data, err := json.Marshal(s.state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sink.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sink.Close()
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
sink.Cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release освобождает ресурсы
|
||||
func (s *RaftSnapshot) Release() {}
|
||||
|
||||
// RegisterNode регистрирует узел через Raft
|
||||
func (rc *RaftCoordinator) RegisterNode(node *Node) error {
|
||||
// В одноузловом режиме всегда считаем себя лидером
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
rc.logger.Debug("Single-node mode: registering node without Raft consensus")
|
||||
|
||||
// Просто сохраняем узел локально
|
||||
rc.nodes.Store(node.ID, &NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
})
|
||||
|
||||
// Также сохраняем в FSM
|
||||
rc.fsm.state.mu.Lock()
|
||||
rc.fsm.state.Nodes[node.ID] = &NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
}
|
||||
rc.fsm.state.mu.Unlock()
|
||||
|
||||
rc.logger.Debug(fmt.Sprintf("Node registered locally in single-node mode: %s", node.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Проверяем, является ли текущий узел лидером
|
||||
if !rc.IsLeader() {
|
||||
leader := rc.GetLeader()
|
||||
if leader != nil {
|
||||
rc.logger.Warn(fmt.Sprintf("Current node is not leader. Leader is %s:%d", leader.IP, leader.Port))
|
||||
return fmt.Errorf("node is not the leader. Please connect to leader at %s:%d", leader.IP, leader.Port)
|
||||
}
|
||||
return fmt.Errorf("node is not the leader and no leader found")
|
||||
}
|
||||
|
||||
cmd := NodeRegistrationCommand{
|
||||
Type: "register",
|
||||
Node: NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
future := rc.raft.Apply(data, 5*time.Second)
|
||||
if err := future.Error(); err != nil {
|
||||
return fmt.Errorf("failed to register node via raft: %v", err)
|
||||
}
|
||||
|
||||
rc.nodes.Store(node.ID, &NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
})
|
||||
|
||||
rc.logger.Debug(fmt.Sprintf("Node registered via Raft: %s", node.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNode удаляет узел через Raft
|
||||
func (rc *RaftCoordinator) RemoveNode(nodeID string) error {
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
rc.nodes.Delete(nodeID)
|
||||
rc.fsm.state.mu.Lock()
|
||||
delete(rc.fsm.state.Nodes, nodeID)
|
||||
rc.fsm.state.mu.Unlock()
|
||||
rc.logger.Debug(fmt.Sprintf("Node removed locally in single-node mode: %s", nodeID))
|
||||
return nil
|
||||
}
|
||||
|
||||
if !rc.IsLeader() {
|
||||
return fmt.Errorf("node is not the leader")
|
||||
}
|
||||
|
||||
cmd := NodeRegistrationCommand{
|
||||
Type: "remove",
|
||||
NodeID: nodeID,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
future := rc.raft.Apply(data, 5*time.Second)
|
||||
if err := future.Error(); err != nil {
|
||||
return fmt.Errorf("failed to remove node via raft: %v", err)
|
||||
}
|
||||
|
||||
rc.nodes.Delete(nodeID)
|
||||
rc.logger.Debug(fmt.Sprintf("Node removed via Raft: %s", nodeID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveNodes возвращает активные узлы
|
||||
func (rc *RaftCoordinator) GetActiveNodes() []*NodeInfo {
|
||||
nodes := make([]*NodeInfo, 0)
|
||||
now := time.Now().Unix()
|
||||
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
for _, nodeInfo := range state.Nodes {
|
||||
if now-nodeInfo.LastSeen < 30 {
|
||||
nodes = append(nodes, nodeInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// GetAllNodes возвращает все узлы
|
||||
func (rc *RaftCoordinator) GetAllNodes() []*NodeInfo {
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
nodes := make([]*NodeInfo, 0, len(state.Nodes))
|
||||
for _, node := range state.Nodes {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// GetLeader возвращает лидера
|
||||
func (rc *RaftCoordinator) GetLeader() *NodeInfo {
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
// В одноузловом режиме возвращаем единственный узел
|
||||
nodes := rc.GetAllNodes()
|
||||
if len(nodes) > 0 {
|
||||
return nodes[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
leaderAddr := rc.raft.Leader()
|
||||
if leaderAddr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
for _, node := range state.Nodes {
|
||||
nodeAddr := fmt.Sprintf("%s:%d", node.IP, node.Port)
|
||||
if nodeAddr == string(leaderAddr) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLeader проверяет, является ли текущий узел лидером
|
||||
func (rc *RaftCoordinator) IsLeader() bool {
|
||||
// В одноузловом режиме всегда лидер
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
return true
|
||||
}
|
||||
return rc.isLeader.Load()
|
||||
}
|
||||
|
||||
// SendHeartbeat обновляет heartbeat узла
|
||||
func (rc *RaftCoordinator) SendHeartbeat(nodeID string) {
|
||||
if val, ok := rc.nodes.Load(nodeID); ok {
|
||||
nodeInfo := val.(*NodeInfo)
|
||||
nodeInfo.LastSeen = time.Now().Unix()
|
||||
rc.nodes.Store(nodeID, nodeInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// GetClusterStatus возвращает статус кластера
|
||||
func (rc *RaftCoordinator) GetClusterStatus() *ClusterStatus {
|
||||
nodes := rc.GetAllNodes()
|
||||
activeNodes := rc.GetActiveNodes()
|
||||
|
||||
syncingNodes := 0
|
||||
for _, node := range nodes {
|
||||
if node.Status == "syncing" {
|
||||
syncingNodes++
|
||||
}
|
||||
}
|
||||
|
||||
leader := rc.GetLeader()
|
||||
leaderID := ""
|
||||
if leader != nil {
|
||||
leaderID = leader.ID
|
||||
}
|
||||
|
||||
return &ClusterStatus{
|
||||
Name: rc.clusterName,
|
||||
TotalNodes: len(nodes),
|
||||
ActiveNodes: len(activeNodes),
|
||||
SyncingNodes: syncingNodes,
|
||||
FailedNodes: len(nodes) - len(activeNodes),
|
||||
ReplicationFactor: int(rc.replicationFactor.Load()),
|
||||
LeaderID: leaderID,
|
||||
Health: rc.calculateHealth(),
|
||||
}
|
||||
}
|
||||
|
||||
// calculateHealth вычисляет здоровье кластера
|
||||
func (rc *RaftCoordinator) calculateHealth() string {
|
||||
activeNodes := rc.GetActiveNodes()
|
||||
totalNodes := rc.GetAllNodes()
|
||||
|
||||
if len(totalNodes) == 0 {
|
||||
return "critical"
|
||||
}
|
||||
|
||||
ratio := float64(len(activeNodes)) / float64(len(totalNodes))
|
||||
if ratio >= 0.8 {
|
||||
return "healthy"
|
||||
} else if ratio >= 0.5 {
|
||||
return "degraded"
|
||||
}
|
||||
return "critical"
|
||||
}
|
||||
|
||||
// GetReplicationFactor возвращает фактор репликации
|
||||
func (rc *RaftCoordinator) GetReplicationFactor() int {
|
||||
return int(rc.replicationFactor.Load())
|
||||
}
|
||||
|
||||
// SetReplicationFactor устанавливает фактор репликации через Raft
|
||||
func (rc *RaftCoordinator) SetReplicationFactor(factor int) error {
|
||||
if !rc.IsLeader() {
|
||||
return fmt.Errorf("node is not the leader")
|
||||
}
|
||||
|
||||
cmd := NodeRegistrationCommand{
|
||||
Type: "set_replication_factor",
|
||||
Factor: int32(factor),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
future := rc.raft.Apply(data, 5*time.Second)
|
||||
if err := future.Error(); err != nil {
|
||||
return fmt.Errorf("failed to set replication factor via raft: %v", err)
|
||||
}
|
||||
|
||||
rc.replicationFactor.Store(int32(factor))
|
||||
rc.logger.Debug(fmt.Sprintf("Replication factor set to %d via Raft", factor))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClusterHealth возвращает детальную информацию о здоровье кластера
|
||||
func (rc *RaftCoordinator) GetClusterHealth() *ClusterHealth {
|
||||
health := &ClusterHealth{
|
||||
Nodes: make(map[string]*NodeHealth),
|
||||
OverallScore: 100.0,
|
||||
Recommendations: "",
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
for nodeID, nodeInfo := range state.Nodes {
|
||||
nodeHealth := &NodeHealth{
|
||||
Status: nodeInfo.Status,
|
||||
LatencyMs: 0,
|
||||
LastCheck: now,
|
||||
}
|
||||
|
||||
if now-nodeInfo.LastSeen > 30 {
|
||||
nodeHealth.Status = "offline"
|
||||
health.OverallScore -= 10
|
||||
} else if nodeInfo.Status == "syncing" {
|
||||
health.OverallScore -= 5
|
||||
}
|
||||
|
||||
health.Nodes[nodeID] = nodeHealth
|
||||
}
|
||||
|
||||
if health.OverallScore < 50 {
|
||||
health.Recommendations = "Critical: Check network connectivity and node health immediately"
|
||||
} else if health.OverallScore < 80 {
|
||||
health.Recommendations = "Warning: Some nodes are offline or syncing, consider adding more nodes"
|
||||
} else {
|
||||
health.Recommendations = "Cluster is healthy, all systems operational"
|
||||
}
|
||||
|
||||
return health
|
||||
}
|
||||
|
||||
// IsReplicationEnabled возвращает статус репликации
|
||||
func (rc *RaftCoordinator) IsReplicationEnabled() bool {
|
||||
return rc.replicationEnabled
|
||||
}
|
||||
|
||||
// IsMasterMasterEnabled возвращает статус мастер-мастер репликации
|
||||
func (rc *RaftCoordinator) IsMasterMasterEnabled() bool {
|
||||
return rc.masterMasterEnabled
|
||||
}
|
||||
|
||||
// IsSyncReplicationEnabled возвращает статус синхронной репликации
|
||||
func (rc *RaftCoordinator) IsSyncReplicationEnabled() bool {
|
||||
return rc.syncReplication
|
||||
}
|
||||
|
||||
// Stop останавливает координатор
|
||||
func (rc *RaftCoordinator) Stop() {
|
||||
close(rc.stopChan)
|
||||
if rc.raft != nil {
|
||||
rc.raft.Shutdown()
|
||||
}
|
||||
rc.logger.Debug("Raft coordinator stopped")
|
||||
}
|
||||
47
internal/cluster/types.go
Normal file
47
internal/cluster/types.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Файл: internal/cluster/types.go
|
||||
// Назначение: Общие типы данных для кластерных операций
|
||||
|
||||
package cluster
|
||||
|
||||
// NodeInfo представляет информацию об узле для координатора
|
||||
type NodeInfo struct {
|
||||
ID string `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
Status string `json:"status"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
}
|
||||
|
||||
// ClusterStatus представляет статус кластера
|
||||
type ClusterStatus struct {
|
||||
Name string `json:"name"`
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
ActiveNodes int `json:"active_nodes"`
|
||||
SyncingNodes int `json:"syncing_nodes"`
|
||||
FailedNodes int `json:"failed_nodes"`
|
||||
ReplicationFactor int `json:"replication_factor"`
|
||||
LeaderID string `json:"leader_id"`
|
||||
Health string `json:"health"`
|
||||
}
|
||||
|
||||
// ClusterHealth представляет информацию о здоровье кластера
|
||||
type ClusterHealth struct {
|
||||
Nodes map[string]*NodeHealth `json:"nodes"`
|
||||
OverallScore float64 `json:"overall_score"`
|
||||
Recommendations string `json:"recommendations"`
|
||||
}
|
||||
|
||||
// NodeHealth представляет здоровье отдельного узла
|
||||
type NodeHealth struct {
|
||||
Status string `json:"status"`
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
LastCheck int64 `json:"last_check"`
|
||||
}
|
||||
|
||||
// NodeRequest представляет запрос от одного узла к другому
|
||||
type NodeRequest struct {
|
||||
Type string `json:"type"` // replicate, query, sync, heartbeat
|
||||
Data []byte `json:"data"` // Данные запроса
|
||||
FromNode string `json:"from_node"` // ID узла-отправителя
|
||||
RequestID string `json:"request_id"` // Уникальный ID запроса
|
||||
}
|
||||
336
internal/commands/cluster.go
Normal file
336
internal/commands/cluster.go
Normal file
@@ -0,0 +1,336 @@
|
||||
// Файл: 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)
|
||||
}
|
||||
82
internal/commands/commands.go
Normal file
82
internal/commands/commands.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Файл: 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
|
||||
|
||||
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/
|
||||
|
||||
UTILITIES:
|
||||
help - Show this help message
|
||||
exit / quit - Exit database
|
||||
|
||||
`
|
||||
utils.Println(helpText)
|
||||
}
|
||||
337
internal/commands/crud.go
Normal file
337
internal/commands/crud.go
Normal file
@@ -0,0 +1,337 @@
|
||||
// Файл: internal/commands/crud.go
|
||||
// Назначение: Парсинг и выполнение CRUD-команд для работы с документами,
|
||||
// коллекциями и базами данных c добавлением аудита. Поддерживает MongoDB-подобный синтаксис.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/storage"
|
||||
"futriis/internal/cluster"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// Execute выполняет команду CRUD
|
||||
func Execute(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
// Простейший парсинг для демонстрации
|
||||
if strings.HasPrefix(cmd, "use ") {
|
||||
dbName := strings.TrimPrefix(cmd, "use ")
|
||||
if err := store.CreateDatabase(dbName); err != nil && err.Error() != "database already exists" {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("USE", dbName)
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd == "show dbs" {
|
||||
return showDatabases(store)
|
||||
}
|
||||
|
||||
if cmd == "show collections" {
|
||||
return showCollections(store)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmd, "db.") {
|
||||
return executeDatabaseCommand(store, coord, cmd)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// ExecuteTransaction выполняет команды транзакций MongoDB-подобного синтаксиса
|
||||
func ExecuteTransaction(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
if strings.Contains(cmd, "startSession()") {
|
||||
if err := storage.InitTransactionManager("futriis.wal"); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Session started")
|
||||
storage.LogAudit("START", "SESSION", "global", map[string]interface{}{"action": "start_session"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "startTransaction()") {
|
||||
_ = storage.BeginTransaction()
|
||||
utils.Println("Transaction started")
|
||||
storage.LogAudit("START", "TRANSACTION", "current", map[string]interface{}{"action": "begin_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "commitTransaction()") {
|
||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction committed successfully")
|
||||
storage.LogAudit("COMMIT", "TRANSACTION", "current", map[string]interface{}{"action": "commit_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "abortTransaction()") {
|
||||
if err := storage.AbortCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction aborted")
|
||||
storage.LogAudit("ABORT", "TRANSACTION", "current", map[string]interface{}{"action": "abort_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown transaction command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// showDatabases отображает список всех баз данных
|
||||
func showDatabases(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nDatabases:")
|
||||
for _, db := range databases {
|
||||
utils.Println(" - " + db)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// showCollections отображает список коллекций в текущей базе данных
|
||||
func showCollections(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collections := db.ListCollections()
|
||||
if len(collections) == 0 {
|
||||
utils.Println("No collections found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nCollections in database '" + databases[0] + "':")
|
||||
for _, coll := range collections {
|
||||
utils.Println(" - " + coll)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeDatabaseCommand выполняет команду вида db.<collection>.<operation>()
|
||||
func executeDatabaseCommand(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
parts := strings.SplitN(cmd, ".", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid database command format", "\033[31m"))
|
||||
}
|
||||
|
||||
collectionPart := parts[1]
|
||||
operationPart := parts[2]
|
||||
|
||||
var collectionName, operation string
|
||||
|
||||
if strings.Contains(collectionPart, ".") {
|
||||
collParts := strings.SplitN(collectionPart, ".", 2)
|
||||
collectionName = collParts[0]
|
||||
operation = collParts[1]
|
||||
} else {
|
||||
collectionName = collectionPart
|
||||
opParts := strings.SplitN(operationPart, "(", 2)
|
||||
if len(opParts) < 1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid operation format", "\033[31m"))
|
||||
}
|
||||
operation = opParts[0]
|
||||
}
|
||||
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
if err := store.CreateDatabase("test"); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("CREATE", "test")
|
||||
databases = store.ListDatabases()
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collectionName)
|
||||
if err != nil {
|
||||
if err := db.CreateCollection(collectionName); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditCollectionOperation("CREATE", databases[0], collectionName, nil)
|
||||
coll, _ = db.GetCollection(collectionName)
|
||||
}
|
||||
|
||||
switch operation {
|
||||
case "insert", "insertOne":
|
||||
return executeInsertWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "find", "findOne":
|
||||
return executeFindWithTransaction(coll, operationPart)
|
||||
case "update", "updateOne":
|
||||
return executeUpdateWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "remove", "delete", "deleteOne":
|
||||
return executeDeleteWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
default:
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown operation: "+operation, "\033[31m"))
|
||||
}
|
||||
}
|
||||
|
||||
func executeInsertWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
start := strings.Index(operationPart, "(")
|
||||
end := strings.LastIndex(operationPart, ")")
|
||||
if start == -1 || end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid insert syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
dataStr := operationPart[start+1 : end]
|
||||
dataStr = strings.TrimSpace(dataStr)
|
||||
|
||||
if dataStr == "" || dataStr == "{}" {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("empty document", "\033[31m"))
|
||||
}
|
||||
|
||||
doc := storage.NewDocument()
|
||||
|
||||
dataStr = strings.Trim(dataStr, "{}")
|
||||
if dataStr != "" {
|
||||
fields := strings.Split(dataStr, ",")
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(field, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
value = strings.Trim(value, "\"'")
|
||||
doc.SetField(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
if err := storage.AddToTransaction(coll, "insert", doc); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Document staged for transaction")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Аудит операции вставки документа
|
||||
storage.AuditDocumentOperation("INSERT", dbName, collName, doc.ID, doc.GetFields())
|
||||
|
||||
utils.Println("Inserted document with _id: " + doc.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeFindWithTransaction(coll *storage.Collection, operationPart string) error {
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
docs := coll.GetAllDocuments()
|
||||
if len(docs) == 0 {
|
||||
utils.Println("No documents found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nFound " + utils.ColorizeTextInt(len(docs)) + " documents:")
|
||||
for _, doc := range docs {
|
||||
utils.Println(" _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid find syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
doc, err := storage.FindInTransaction(coll, idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Found document (in transaction): _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
doc, err := coll.Find(idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Println("Found document: _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeUpdateWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Update operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "UPDATE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки обновления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("update requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid update syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
storage.AuditDocumentOperation("UPDATE", dbName, collName, idPart, nil)
|
||||
utils.Println("Update operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeDeleteWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Delete operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "DELETE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки удаления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("delete requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid delete syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if err := coll.Delete(idPart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storage.AuditDocumentOperation("DELETE", dbName, collName, idPart, nil)
|
||||
utils.Println("Delete operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
242
internal/commands/export_import.go
Normal file
242
internal/commands/export_import.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Файл: 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",
|
||||
}
|
||||
|
||||
// Сериализуем в 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
|
||||
|
||||
// Импортируем коллекции
|
||||
for key, value := range importData {
|
||||
if key == "_metadata" {
|
||||
continue
|
||||
}
|
||||
|
||||
collName := key
|
||||
collData, ok := value.([]interface{})
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
fmt.Printf(" Warning: failed to get collection '%s': %v\n", collName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Импортируем документы
|
||||
for _, docRaw := range collData {
|
||||
docMap, ok := docRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Создаём документ
|
||||
doc := storage.NewDocument()
|
||||
|
||||
if id, ok := docMap["_id"].(string); ok {
|
||||
doc.ID = id
|
||||
}
|
||||
|
||||
if fields, ok := docMap["fields"].(map[string]interface{}); ok {
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if createdAt, ok := docMap["created_at"].(int64); ok {
|
||||
doc.CreatedAt = createdAt
|
||||
}
|
||||
|
||||
if updatedAt, ok := docMap["updated_at"].(int64); ok {
|
||||
doc.UpdatedAt = updatedAt
|
||||
}
|
||||
|
||||
if version, ok := docMap["version"].(uint64); ok {
|
||||
doc.Version = version
|
||||
}
|
||||
|
||||
// Вставляем документ
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
fmt.Printf(" Warning: failed to insert document %s: %v\n", doc.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
importedDocuments++
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
223
internal/compression/compression.go
Normal file
223
internal/compression/compression.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Файл: internal/compression/compression.go
|
||||
// Назначение: Реализация сжатия данных с использованием различных алгоритмов.
|
||||
// Поддерживаемые алгоритмы: Snappy (по умолчанию), LZ4, Zstandard.
|
||||
// Обеспечивает прозрачное сжатие/распаковку для документов.
|
||||
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/snappy"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/pierrec/lz4/v4"
|
||||
)
|
||||
|
||||
// Config представляет конфигурацию сжатия
|
||||
type Config struct {
|
||||
Enabled bool // Включено ли сжатие
|
||||
Algorithm string // Алгоритм сжатия: snappy, lz4, 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
|
||||
CompressionLZ4 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 "lz4":
|
||||
buf := bytes.NewBuffer(nil)
|
||||
lz4Writer := lz4.NewWriter(buf)
|
||||
|
||||
// Установка уровня сжатия для LZ4
|
||||
if config.Level > 0 {
|
||||
// LZ4 уровни: 0-9, где 0=быстрый, 9=максимальное сжатие
|
||||
compressionLevel := lz4.CompressionLevel(config.Level)
|
||||
if err := lz4Writer.Apply(lz4.CompressionLevelOption(compressionLevel)); err != nil {
|
||||
return nil, fmt.Errorf("failed to set LZ4 compression level: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := lz4Writer.Write(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lz4Writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compressed = buf.Bytes()
|
||||
compType = CompressionLZ4
|
||||
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 CompressionLZ4:
|
||||
decompressed = make([]byte, originalSize)
|
||||
lz4Reader := lz4.NewReader(bytes.NewReader(compressedData))
|
||||
n, err := lz4Reader.Read(decompressed)
|
||||
if err != nil && err.Error() != "EOF" {
|
||||
return nil, fmt.Errorf("lz4 decode failed: %v", err)
|
||||
}
|
||||
if n != int(originalSize) {
|
||||
// Некоторые данные могли быть прочитаны, но не все
|
||||
decompressed = decompressed[:n]
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Проверяем размер распакованных данных
|
||||
if len(decompressed) != int(originalSize) {
|
||||
// Не критично, но логируем
|
||||
_ = len(decompressed)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
99
internal/config/config.go
Normal file
99
internal/config/config.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Файл: internal/config/config.go
|
||||
// Назначение: Загрузка и парсинг TOML-конфигурации, валидация параметров,
|
||||
// предоставление доступа к настройкам кластера, хранилища и REPL.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Cluster ClusterConfig `toml:"cluster"`
|
||||
Storage StorageConfig `toml:"storage"`
|
||||
Repl ReplConfig `toml:"repl"`
|
||||
Log LogConfig `toml:"log"`
|
||||
Replication ReplicationConfig `toml:"replication"`
|
||||
Plugins PluginsConfig `toml:"plugins"`
|
||||
Compression CompressionConfig `toml:"compression"`
|
||||
}
|
||||
|
||||
type ClusterConfig struct {
|
||||
Name string `toml:"name"`
|
||||
NodeIP string `toml:"node_ip"`
|
||||
NodePort int `toml:"node_port"`
|
||||
RaftPort int `toml:"raft_port"`
|
||||
RaftDataDir string `toml:"raft_data_dir"`
|
||||
Bootstrap bool `toml:"bootstrap"` // Флаг бутстрапа кластера
|
||||
Nodes []string `toml:"nodes"` // Список узлов кластера
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
PageSizeMB int `toml:"page_size_mb"`
|
||||
MaxCollections int `toml:"max_collections"`
|
||||
MaxDocumentsPerCollection int `toml:"max_documents_per_collection"`
|
||||
}
|
||||
|
||||
type ReplConfig struct {
|
||||
PromptColor string `toml:"prompt_color"`
|
||||
HistorySize int `toml:"history_size"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
LogFile string `toml:"log_file"`
|
||||
LogLevel string `toml:"log_level"`
|
||||
}
|
||||
|
||||
type ReplicationConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MasterMaster bool `toml:"master_master"`
|
||||
SyncReplication bool `toml:"sync_replication"`
|
||||
ReplicationTimeoutMs int `toml:"replication_timeout_ms"`
|
||||
}
|
||||
|
||||
type PluginsConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
ScriptDir string `toml:"script_dir"`
|
||||
AllowList []string `toml:"allow_list"`
|
||||
}
|
||||
|
||||
type CompressionConfig struct {
|
||||
Enabled bool `toml:"enabled"` // Включено ли сжатие
|
||||
Algorithm string `toml:"algorithm"` // Алгоритм сжатия (snappy, lz4, zstd)
|
||||
Level int `toml:"level"` // Уровень сжатия (1-9, зависит от алгоритма)
|
||||
MinSize int `toml:"min_size"` // Минимальный размер для сжатия (байт)
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
var cfg Config
|
||||
if _, err := toml.DecodeFile(path, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Установка значений по умолчанию, если не указаны
|
||||
if cfg.Cluster.RaftPort == 0 {
|
||||
cfg.Cluster.RaftPort = 9878
|
||||
}
|
||||
if cfg.Cluster.RaftDataDir == "" {
|
||||
cfg.Cluster.RaftDataDir = "raft_data"
|
||||
}
|
||||
if cfg.Replication.ReplicationTimeoutMs == 0 {
|
||||
cfg.Replication.ReplicationTimeoutMs = 5000
|
||||
}
|
||||
if cfg.Plugins.ScriptDir == "" {
|
||||
cfg.Plugins.ScriptDir = "plugins"
|
||||
}
|
||||
|
||||
// Установка значений по умолчанию для сжатия
|
||||
if cfg.Compression.Algorithm == "" {
|
||||
cfg.Compression.Algorithm = "snappy"
|
||||
}
|
||||
if cfg.Compression.MinSize == 0 {
|
||||
cfg.Compression.MinSize = 1024 // 1KB - сжимаем только документы больше 1KB
|
||||
}
|
||||
if cfg.Compression.Level == 0 {
|
||||
cfg.Compression.Level = 3 // Средний уровень сжатия
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
89
internal/log/logger.go
Normal file
89
internal/log/logger.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Файл: internal/log/logger.go
|
||||
// Назначение: Асинхронная, wait-free запись логов в файл с меткой времени
|
||||
// в миллисекундах. Поддержка уровней логирования и ротации.
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogLevel int32
|
||||
|
||||
const (
|
||||
DebugLevel LogLevel = iota
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
file *os.File
|
||||
level atomic.Int32
|
||||
writeChan chan string
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewLogger(filename string, levelStr string) (*Logger, error) {
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
level := InfoLevel
|
||||
switch levelStr {
|
||||
case "debug":
|
||||
level = DebugLevel
|
||||
case "warn":
|
||||
level = WarnLevel
|
||||
case "error":
|
||||
level = ErrorLevel
|
||||
}
|
||||
|
||||
l := &Logger{
|
||||
file: file,
|
||||
writeChan: make(chan string, 10000),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
l.level.Store(int32(level))
|
||||
|
||||
// Запуск wait-free writer
|
||||
go l.writerLoop()
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l *Logger) writerLoop() {
|
||||
for msg := range l.writeChan {
|
||||
l.file.WriteString(msg + "\n")
|
||||
}
|
||||
close(l.done)
|
||||
}
|
||||
|
||||
func (l *Logger) log(level LogLevel, levelStr, msg string) {
|
||||
if level < LogLevel(l.level.Load()) {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
timestamp := now.Format("2006-01-02 15:04:05") + fmt.Sprintf(".%03d", now.Nanosecond()/1e6)
|
||||
logMsg := fmt.Sprintf("[%s] %s: %s", timestamp, levelStr, msg)
|
||||
select {
|
||||
case l.writeChan <- logMsg:
|
||||
default:
|
||||
// Неблокирующая запись, старый лог теряется - wait-free
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(msg string) { l.log(DebugLevel, "DEBUG", msg) }
|
||||
func (l *Logger) Info(msg string) { l.log(InfoLevel, "INFO", msg) }
|
||||
func (l *Logger) Warn(msg string) { l.log(WarnLevel, "WARN", msg) }
|
||||
func (l *Logger) Error(msg string) { l.log(ErrorLevel, "ERROR", msg) }
|
||||
|
||||
func (l *Logger) Close() {
|
||||
close(l.writeChan)
|
||||
<-l.done
|
||||
l.file.Close()
|
||||
}
|
||||
732
internal/plugin/plugin.go
Normal file
732
internal/plugin/plugin.go
Normal file
@@ -0,0 +1,732 @@
|
||||
// Файл: internal/plugin/plugin.go
|
||||
// Назначение: Система плагинов на основе Lua для расширения функциональности СУБД.
|
||||
// Позволяет загружать Lua-скрипты как плагины, выполнять их в изолированном окружении,
|
||||
// взаимодействовать с данными СУБД и логировать действия плагинов в общий лог-файл.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/storage"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// PluginStatus представляет состояние плагина
|
||||
type PluginStatus int32
|
||||
|
||||
const (
|
||||
StatusLoaded PluginStatus = iota
|
||||
StatusRunning
|
||||
StatusStopped
|
||||
StatusError
|
||||
)
|
||||
|
||||
// Plugin представляет загруженный Lua-плагин
|
||||
type Plugin struct {
|
||||
Name string
|
||||
FilePath string
|
||||
Status atomic.Int32
|
||||
LState *lua.LState
|
||||
logger *log.Logger
|
||||
storage *storage.Storage
|
||||
mu sync.RWMutex
|
||||
loadedAt time.Time
|
||||
version string
|
||||
author string
|
||||
description string
|
||||
}
|
||||
|
||||
// PluginManager управляет всеми загруженными плагинами
|
||||
type PluginManager struct {
|
||||
plugins sync.Map // map[string]*Plugin
|
||||
logger *log.Logger
|
||||
storage *storage.Storage
|
||||
pluginsDir string
|
||||
eventBus chan PluginEvent
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// PluginEvent представляет событие от плагина
|
||||
type PluginEvent struct {
|
||||
PluginName string
|
||||
EventType string
|
||||
Data interface{}
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
// NewPluginManager создаёт новый менеджер плагинов
|
||||
func NewPluginManager(pluginsDir string, logger *log.Logger, store *storage.Storage, enabled bool) *PluginManager {
|
||||
pm := &PluginManager{
|
||||
logger: logger,
|
||||
storage: store,
|
||||
pluginsDir: pluginsDir,
|
||||
eventBus: make(chan PluginEvent, 1000),
|
||||
enabled: enabled,
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
logger.Info("Plugin system is disabled")
|
||||
return pm
|
||||
}
|
||||
|
||||
// Запускаем обработчик событий плагинов
|
||||
go pm.eventLoop()
|
||||
|
||||
// Автоматически загружаем плагины из директории
|
||||
go pm.autoLoadPlugins()
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
// autoLoadPlugins автоматически загружает все .lua файлы из директории плагинов
|
||||
func (pm *PluginManager) autoLoadPlugins() {
|
||||
if !pm.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
<-ticker.C
|
||||
entries, err := os.ReadDir(pm.pluginsDir)
|
||||
if err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to read plugins directory: %v", err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".lua") {
|
||||
pluginName := strings.TrimSuffix(entry.Name(), ".lua")
|
||||
if _, exists := pm.plugins.Load(pluginName); !exists {
|
||||
pluginPath := filepath.Join(pm.pluginsDir, entry.Name())
|
||||
if err := pm.LoadPlugin(pluginName, pluginPath); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to auto-load plugin %s: %v", pluginName, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPlugin загружает Lua-плагин из файла
|
||||
func (pm *PluginManager) LoadPlugin(name, filePath string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
// Читаем файл плагина
|
||||
script, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read plugin file: %v", err)
|
||||
}
|
||||
|
||||
// Создаём новое Lua-состояние
|
||||
L := lua.NewState()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
L.Close()
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Plugin %s panic: %v", name, r))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Открываем стандартные библиотеки Lua
|
||||
lua.OpenBase(L)
|
||||
lua.OpenString(L)
|
||||
lua.OpenTable(L)
|
||||
lua.OpenMath(L)
|
||||
|
||||
// Регистрируем функции СУБД для Lua
|
||||
pm.registerDatabaseFunctions(L)
|
||||
|
||||
// Выполняем скрипт
|
||||
if err := L.DoString(string(script)); err != nil {
|
||||
L.Close()
|
||||
return fmt.Errorf("failed to execute plugin script: %v", err)
|
||||
}
|
||||
|
||||
// Извлекаем метаданные плагина
|
||||
version := pm.getPluginMetadata(L, "version")
|
||||
author := pm.getPluginMetadata(L, "author")
|
||||
description := pm.getPluginMetadata(L, "description")
|
||||
|
||||
// Создаём объект плагина
|
||||
plugin := &Plugin{
|
||||
Name: name,
|
||||
FilePath: filePath,
|
||||
LState: L,
|
||||
logger: pm.logger,
|
||||
storage: pm.storage,
|
||||
loadedAt: time.Now(),
|
||||
version: version,
|
||||
author: author,
|
||||
description: description,
|
||||
}
|
||||
plugin.Status.Store(int32(StatusLoaded))
|
||||
|
||||
// Сохраняем плагин
|
||||
pm.plugins.Store(name, plugin)
|
||||
|
||||
// Логируем загрузку
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin loaded: %s v%s by %s - %s", name, version, author, description))
|
||||
}
|
||||
|
||||
// Вызываем функцию инициализации плагина, если она есть
|
||||
if err := pm.callPluginFunction(plugin, "on_load"); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_load error: %v", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerDatabaseFunctions регистрирует функции доступа к СУБД в Lua
|
||||
func (pm *PluginManager) registerDatabaseFunctions(L *lua.LState) {
|
||||
// Регистрируем функцию для получения базы данных
|
||||
L.SetGlobal("get_database", L.NewFunction(func(L *lua.LState) int {
|
||||
dbName := L.CheckString(1)
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Создаём пользовательский тип для базы данных
|
||||
ud := L.NewUserData()
|
||||
ud.Value = db
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("database"))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
|
||||
// Регистрируем функцию для получения коллекции
|
||||
L.SetGlobal("get_collection", L.NewFunction(func(L *lua.LState) int {
|
||||
dbName := L.CheckString(1)
|
||||
collName := L.CheckString(2)
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
ud := L.NewUserData()
|
||||
ud.Value = coll
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("collection"))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
|
||||
// Регистрируем функцию логирования для плагинов
|
||||
L.SetGlobal("plugin_log", L.NewFunction(func(L *lua.LState) int {
|
||||
level := L.CheckString(1)
|
||||
message := L.CheckString(2)
|
||||
|
||||
if pm.logger != nil {
|
||||
logMsg := fmt.Sprintf("[PLUGIN] %s: %s", level, message)
|
||||
switch level {
|
||||
case "debug":
|
||||
pm.logger.Debug(logMsg)
|
||||
case "info":
|
||||
pm.logger.Info(logMsg)
|
||||
case "warn":
|
||||
pm.logger.Warn(logMsg)
|
||||
case "error":
|
||||
pm.logger.Error(logMsg)
|
||||
default:
|
||||
pm.logger.Info(logMsg)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}))
|
||||
|
||||
// Регистрируем функцию для отправки событий
|
||||
L.SetGlobal("emit_event", L.NewFunction(func(L *lua.LState) int {
|
||||
eventType := L.CheckString(1)
|
||||
eventData := L.CheckAny(2)
|
||||
|
||||
// Получаем имя плагина из контекста (нужно передавать при вызове)
|
||||
event := PluginEvent{
|
||||
EventType: eventType,
|
||||
Data: pm.luaValueToGo(eventData),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
select {
|
||||
case pm.eventBus <- event:
|
||||
default:
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn("Plugin event bus full, event dropped")
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}))
|
||||
|
||||
// Устанавливаем метатаблицы для методов баз данных и коллекций
|
||||
pm.setupDatabaseMetatable(L)
|
||||
pm.setupCollectionMetatable(L)
|
||||
}
|
||||
|
||||
// setupDatabaseMetatable настраивает методы для объекта базы данных в Lua
|
||||
func (pm *PluginManager) setupDatabaseMetatable(L *lua.LState) {
|
||||
mt := L.NewTypeMetatable("database")
|
||||
L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||
db := L.CheckUserData(1).Value.(*storage.Database)
|
||||
method := L.CheckString(2)
|
||||
|
||||
switch method {
|
||||
case "create_collection":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
err := db.CreateCollection(name)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "get_collection":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
coll, err := db.GetCollection(name)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
ud := L.NewUserData()
|
||||
ud.Value = coll
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("collection"))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
case "name":
|
||||
L.Push(lua.LString(db.Name()))
|
||||
default:
|
||||
L.Push(lua.LNil)
|
||||
}
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
|
||||
// setupCollectionMetatable настраивает методы для объекта коллекции в Lua
|
||||
func (pm *PluginManager) setupCollectionMetatable(L *lua.LState) {
|
||||
mt := L.NewTypeMetatable("collection")
|
||||
L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||
coll := L.CheckUserData(1).Value.(*storage.Collection)
|
||||
method := L.CheckString(2)
|
||||
|
||||
switch method {
|
||||
case "insert":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
doc := L.CheckTable(1)
|
||||
// Конвертируем Lua table в map
|
||||
fields := make(map[string]interface{})
|
||||
doc.ForEach(func(key, value lua.LValue) {
|
||||
if key.Type() == lua.LTString {
|
||||
fields[key.String()] = pm.luaValueToGo(value)
|
||||
}
|
||||
})
|
||||
|
||||
// Вставляем документ
|
||||
err := coll.InsertFromMap(fields)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "find":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
id := L.CheckString(1)
|
||||
doc, err := coll.Find(id)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
// Конвертируем документ в Lua table
|
||||
table := L.NewTable()
|
||||
for k, v := range doc.GetFields() {
|
||||
table.RawSetString(k, pm.goValueToLua(L, v))
|
||||
}
|
||||
L.Push(table)
|
||||
return 1
|
||||
}))
|
||||
case "update":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
id := L.CheckString(1)
|
||||
updates := L.CheckTable(2)
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
updates.ForEach(func(key, value lua.LValue) {
|
||||
if key.Type() == lua.LTString {
|
||||
fields[key.String()] = pm.luaValueToGo(value)
|
||||
}
|
||||
})
|
||||
|
||||
err := coll.Update(id, fields)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "delete":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
id := L.CheckString(1)
|
||||
err := coll.Delete(id)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "count":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
count := coll.Count()
|
||||
L.Push(lua.LNumber(count))
|
||||
return 1
|
||||
}))
|
||||
default:
|
||||
L.Push(lua.LNil)
|
||||
}
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
|
||||
// luaValueToGo конвертирует Lua-значение в Go-значение
|
||||
func (pm *PluginManager) luaValueToGo(val lua.LValue) interface{} {
|
||||
if val == nil || val == lua.LNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case lua.LString:
|
||||
return string(v)
|
||||
case lua.LNumber:
|
||||
return float64(v)
|
||||
case lua.LBool:
|
||||
return bool(v)
|
||||
case *lua.LTable:
|
||||
result := make(map[string]interface{})
|
||||
v.ForEach(func(key, value lua.LValue) {
|
||||
keyStr := "unknown"
|
||||
if key.Type() == lua.LTString {
|
||||
keyStr = key.String()
|
||||
} else if key.Type() == lua.LTNumber {
|
||||
keyStr = fmt.Sprintf("%d", int64(key.(lua.LNumber)))
|
||||
}
|
||||
result[keyStr] = pm.luaValueToGo(value)
|
||||
})
|
||||
return result
|
||||
default:
|
||||
return v.String()
|
||||
}
|
||||
}
|
||||
|
||||
// goValueToLua конвертирует Go-значение в Lua-значение
|
||||
func (pm *PluginManager) goValueToLua(L *lua.LState, val interface{}) lua.LValue {
|
||||
if val == nil {
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
return lua.LString(v)
|
||||
case int:
|
||||
return lua.LNumber(float64(v))
|
||||
case int64:
|
||||
return lua.LNumber(float64(v))
|
||||
case float32:
|
||||
return lua.LNumber(float64(v))
|
||||
case float64:
|
||||
return lua.LNumber(v)
|
||||
case bool:
|
||||
return lua.LBool(v)
|
||||
case map[string]interface{}:
|
||||
table := L.NewTable()
|
||||
for k, val := range v {
|
||||
table.RawSetString(k, pm.goValueToLua(L, val))
|
||||
}
|
||||
return table
|
||||
case []interface{}:
|
||||
table := L.NewTable()
|
||||
for i, val := range v {
|
||||
table.RawSetInt(i+1, pm.goValueToLua(L, val))
|
||||
}
|
||||
return table
|
||||
default:
|
||||
return lua.LString(fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// getPluginMetadata извлекает метаданные из загруженного Lua-скрипта
|
||||
func (pm *PluginManager) getPluginMetadata(L *lua.LState, field string) string {
|
||||
// Пытаемся получить глобальную переменную с метаданными
|
||||
val := L.GetGlobal(field)
|
||||
if str, ok := val.(lua.LString); ok {
|
||||
return string(str)
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// callPluginFunction вызывает функцию плагина по имени
|
||||
func (pm *PluginManager) callPluginFunction(plugin *Plugin, funcName string) error {
|
||||
plugin.mu.RLock()
|
||||
defer plugin.mu.RUnlock()
|
||||
|
||||
L := plugin.LState
|
||||
fn := L.GetGlobal(funcName)
|
||||
if fn == lua.LNil {
|
||||
return nil // Функция не определена
|
||||
}
|
||||
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to call %s: %v", funcName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventLoop обрабатывает события от плагинов
|
||||
func (pm *PluginManager) eventLoop() {
|
||||
for event := range pm.eventBus {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Debug(fmt.Sprintf("Plugin event [%s]: %+v", event.EventType, event.Data))
|
||||
}
|
||||
|
||||
// Можно реализовать подписку плагинов на события
|
||||
pm.plugins.Range(func(key, value interface{}) bool {
|
||||
plugin := value.(*Plugin)
|
||||
// Асинхронно уведомляем плагины о событии
|
||||
go pm.notifyPlugin(plugin, event)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// notifyPlugin уведомляет конкретный плагин о событии
|
||||
func (pm *PluginManager) notifyPlugin(plugin *Plugin, event PluginEvent) {
|
||||
plugin.mu.RLock()
|
||||
defer plugin.mu.RUnlock()
|
||||
|
||||
L := plugin.LState
|
||||
if L == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fn := L.GetGlobal("on_event")
|
||||
if fn == lua.LNil {
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем имя плагина в событие
|
||||
event.PluginName = plugin.Name
|
||||
|
||||
// Создаём таблицу с данными события
|
||||
eventTable := L.NewTable()
|
||||
eventTable.RawSetString("type", lua.LString(event.EventType))
|
||||
eventTable.RawSetString("plugin_name", lua.LString(event.PluginName))
|
||||
eventTable.RawSetString("timestamp", lua.LNumber(event.Timestamp))
|
||||
eventTable.RawSetString("data", pm.goValueToLua(L, event.Data))
|
||||
|
||||
L.SetGlobal("event", eventTable)
|
||||
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Plugin %s on_event error: %v", plugin.Name, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecutePlugin выполняет пользовательскую функцию плагина
|
||||
func (pm *PluginManager) ExecutePlugin(pluginName, funcName string, args ...interface{}) (interface{}, error) {
|
||||
if !pm.enabled {
|
||||
return nil, fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(pluginName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("plugin not found: %s", pluginName)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
if PluginStatus(plugin.Status.Load()) != StatusRunning {
|
||||
return nil, fmt.Errorf("plugin %s is not running", pluginName)
|
||||
}
|
||||
|
||||
plugin.mu.RLock()
|
||||
defer plugin.mu.RUnlock()
|
||||
|
||||
L := plugin.LState
|
||||
if L == nil {
|
||||
return nil, fmt.Errorf("plugin %s has no Lua state", pluginName)
|
||||
}
|
||||
|
||||
fn := L.GetGlobal(funcName)
|
||||
if fn == lua.LNil {
|
||||
return nil, fmt.Errorf("function %s not found in plugin %s", funcName, pluginName)
|
||||
}
|
||||
|
||||
// Подготавливаем аргументы для вызова
|
||||
luaArgs := make([]lua.LValue, len(args))
|
||||
for i, arg := range args {
|
||||
luaArgs[i] = pm.goValueToLua(L, arg)
|
||||
}
|
||||
|
||||
// Вызываем функцию
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, luaArgs...); err != nil {
|
||||
return nil, fmt.Errorf("plugin execution failed: %v", err)
|
||||
}
|
||||
|
||||
ret := L.Get(-1)
|
||||
L.Pop(1)
|
||||
|
||||
return pm.luaValueToGo(ret), nil
|
||||
}
|
||||
|
||||
// UnloadPlugin выгружает плагин
|
||||
func (pm *PluginManager) UnloadPlugin(name string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin not found: %s", name)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
|
||||
// Вызываем функцию выгрузки
|
||||
if err := pm.callPluginFunction(plugin, "on_unload"); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_unload error: %v", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Закрываем Lua-состояние
|
||||
if plugin.LState != nil {
|
||||
plugin.LState.Close()
|
||||
}
|
||||
plugin.Status.Store(int32(StatusStopped))
|
||||
|
||||
pm.plugins.Delete(name)
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin unloaded: %s", name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPlugin запускает плагин
|
||||
func (pm *PluginManager) StartPlugin(name string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin not found: %s", name)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
plugin.Status.Store(int32(StatusRunning))
|
||||
|
||||
if err := pm.callPluginFunction(plugin, "on_start"); err != nil {
|
||||
plugin.Status.Store(int32(StatusError))
|
||||
return fmt.Errorf("failed to start plugin: %v", err)
|
||||
}
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin started: %s", name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopPlugin останавливает плагин
|
||||
func (pm *PluginManager) StopPlugin(name string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin not found: %s", name)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
|
||||
if err := pm.callPluginFunction(plugin, "on_stop"); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_stop error: %v", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
plugin.Status.Store(int32(StatusStopped))
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin stopped: %s", name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListPlugins возвращает список всех загруженных плагинов
|
||||
func (pm *PluginManager) ListPlugins() []*Plugin {
|
||||
plugins := make([]*Plugin, 0)
|
||||
pm.plugins.Range(func(key, value interface{}) bool {
|
||||
plugins = append(plugins, value.(*Plugin))
|
||||
return true
|
||||
})
|
||||
return plugins
|
||||
}
|
||||
|
||||
// IsEnabled возвращает статус системы плагинов
|
||||
func (pm *PluginManager) IsEnabled() bool {
|
||||
return pm.enabled
|
||||
}
|
||||
96
internal/repl/history.go
Normal file
96
internal/repl/history.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Файл: 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
|
||||
}
|
||||
1326
internal/repl/repl.go
Normal file
1326
internal/repl/repl.go
Normal file
File diff suppressed because it is too large
Load Diff
17
internal/serializer/msgpack.go
Normal file
17
internal/serializer/msgpack.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Файл: 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)
|
||||
}
|
||||
116
internal/storage/audit.go
Normal file
116
internal/storage/audit.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Файл: 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
|
||||
DataType string `msgpack:"data_type"` // DATABASE, COLLECTION, DOCUMENT, FIELD, TUPLE, SESSION, TRANSACTION, CLUSTER
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
736
internal/storage/collection.go
Normal file
736
internal/storage/collection.go
Normal file
@@ -0,0 +1,736 @@
|
||||
// Файл: internal/storage/collection.go
|
||||
// Назначение: Реализация коллекции с индексами (первичными и вторичными).
|
||||
// Индексы хранятся отдельно от документов, обеспечивают wait-free доступ.
|
||||
// Исправлено: корректная работа уникальных индексов, удаление из индексов при обновлении.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// Collection представляет коллекцию документов (аналог таблицы)
|
||||
type Collection struct {
|
||||
name string
|
||||
docs sync.Map // map[string]*Document - wait-free хранилище документов
|
||||
indexes sync.Map // map[string]*Index - индексы для быстрого поиска
|
||||
metadata *CollectionMetadata // Метаданные коллекции
|
||||
docCount atomic.Int64 // Атомарный счётчик документов
|
||||
sizeBytes atomic.Int64 // Атомарный размер коллекции в байтах
|
||||
mu sync.RWMutex // Для операций, изменяющих структуру коллекции
|
||||
constraints *Constraints // Ограничения коллекции
|
||||
acl *CollectionACL // ACL для коллекции
|
||||
}
|
||||
|
||||
// CollectionMetadata содержит метаданные коллекции
|
||||
type CollectionMetadata struct {
|
||||
Name string `msgpack:"name"`
|
||||
CreatedAt int64 `msgpack:"created_at"`
|
||||
UpdatedAt int64 `msgpack:"updated_at"`
|
||||
DocumentCount int64 `msgpack:"document_count"`
|
||||
SizeBytes int64 `msgpack:"size_bytes"`
|
||||
IndexCount int `msgpack:"index_count"`
|
||||
Settings *CollectionSettings `msgpack:"settings"`
|
||||
}
|
||||
|
||||
// CollectionSettings содержит настройки коллекции
|
||||
type CollectionSettings struct {
|
||||
MaxDocuments int `msgpack:"max_documents"` // Максимальное количество документов (0 = безлимит)
|
||||
ValidateSchema bool `msgpack:"validate_schema"` // Валидировать схему документов
|
||||
AutoIndexID bool `msgpack:"auto_index_id"` // Автоматически индексировать поле _id
|
||||
TTLSeconds int `msgpack:"ttl_seconds"` // Время жизни документов (0 = бессрочно)
|
||||
}
|
||||
|
||||
// Index представляет индекс для ускорения поиска (хранится отдельно от документов)
|
||||
type Index struct {
|
||||
Name string `msgpack:"name"`
|
||||
Fields []string `msgpack:"fields"` // Поля для индексации
|
||||
Unique bool `msgpack:"unique"` // Уникальный индекс
|
||||
data sync.Map // map[interface{}]string - значение индекса -> ID документа
|
||||
}
|
||||
|
||||
// Constraints представляет ограничения на коллекцию
|
||||
type Constraints struct {
|
||||
mu sync.RWMutex
|
||||
RequiredFields map[string]bool // Обязательные поля
|
||||
UniqueFields map[string]bool // Уникальные поля (дополнительно к индексам)
|
||||
MinValues map[string]float64 // Минимальные значения для числовых полей
|
||||
MaxValues map[string]float64 // Максимальные значения для числовых полей
|
||||
PatternFields map[string]string // Regexp паттерны для полей
|
||||
EnumFields map[string][]interface{} // Допустимые значения для полей
|
||||
}
|
||||
|
||||
// CollectionACL представляет список контроля доступа для коллекции
|
||||
type CollectionACL struct {
|
||||
mu sync.RWMutex
|
||||
ReadRoles map[string]bool // Роли, имеющие доступ на чтение
|
||||
WriteRoles map[string]bool // Роли, имеющие доступ на запись
|
||||
DeleteRoles map[string]bool // Роли, имеющие доступ на удаление
|
||||
AdminRoles map[string]bool // Роли, имеющие полный доступ
|
||||
}
|
||||
|
||||
// NewCollection создаёт новую коллекцию
|
||||
func NewCollection(name string, settings *CollectionSettings) *Collection {
|
||||
if settings == nil {
|
||||
settings = &CollectionSettings{
|
||||
MaxDocuments: 0,
|
||||
ValidateSchema: false,
|
||||
AutoIndexID: true,
|
||||
TTLSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
coll := &Collection{
|
||||
name: name,
|
||||
metadata: &CollectionMetadata{
|
||||
Name: name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DocumentCount: 0,
|
||||
SizeBytes: 0,
|
||||
IndexCount: 0,
|
||||
Settings: settings,
|
||||
},
|
||||
constraints: &Constraints{
|
||||
RequiredFields: make(map[string]bool),
|
||||
UniqueFields: make(map[string]bool),
|
||||
MinValues: make(map[string]float64),
|
||||
MaxValues: make(map[string]float64),
|
||||
PatternFields: make(map[string]string),
|
||||
EnumFields: make(map[string][]interface{}),
|
||||
},
|
||||
acl: &CollectionACL{
|
||||
ReadRoles: make(map[string]bool),
|
||||
WriteRoles: make(map[string]bool),
|
||||
DeleteRoles: make(map[string]bool),
|
||||
AdminRoles: make(map[string]bool),
|
||||
},
|
||||
}
|
||||
|
||||
// Автоматически создаём первичный индекс по _id
|
||||
if settings.AutoIndexID {
|
||||
coll.CreateIndex("_id_", []string{"_id"}, true)
|
||||
}
|
||||
|
||||
// Запускаем фоновую задачу для удаления просроченных документов
|
||||
if settings.TTLSeconds > 0 {
|
||||
go coll.ttlCleanupLoop()
|
||||
}
|
||||
|
||||
return coll
|
||||
}
|
||||
|
||||
// Name возвращает имя коллекции
|
||||
func (c *Collection) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// Insert вставляет документ в коллекцию (wait-free)
|
||||
func (c *Collection) Insert(doc *Document) error {
|
||||
// Проверка ограничений
|
||||
if err := c.constraints.ValidateDocument(doc); err != nil {
|
||||
return fmt.Errorf("constraint violation: %v", err)
|
||||
}
|
||||
|
||||
// Проверка ACL (будет вызвано из верхнего уровня с ролью)
|
||||
|
||||
// Проверка на максимальное количество документов
|
||||
if c.metadata.Settings.MaxDocuments > 0 {
|
||||
if c.docCount.Load() >= int64(c.metadata.Settings.MaxDocuments) {
|
||||
return fmt.Errorf("collection is full: max documents %d reached", c.metadata.Settings.MaxDocuments)
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация схемы (если включена)
|
||||
if c.metadata.Settings.ValidateSchema {
|
||||
if err := c.validateDocument(doc); err != nil {
|
||||
return fmt.Errorf("document validation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка уникальных индексов
|
||||
if err := c.checkUniqueConstraints(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Сериализация для проверки (опционально)
|
||||
data, err := serializer.Marshal(doc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize document: %v", err)
|
||||
}
|
||||
|
||||
// Атомарное сохранение документа
|
||||
if _, loaded := c.docs.LoadOrStore(doc.ID, doc); loaded {
|
||||
return fmt.Errorf("document with id %s already exists", doc.ID)
|
||||
}
|
||||
|
||||
// Обновление индексов (wait-free)
|
||||
c.updateIndexes(doc, true)
|
||||
|
||||
// Обновление метаданных
|
||||
c.docCount.Add(1)
|
||||
c.sizeBytes.Add(int64(len(data)))
|
||||
c.metadata.DocumentCount = c.docCount.Load()
|
||||
c.metadata.SizeBytes = c.sizeBytes.Load()
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertFromMap создаёт и вставляет документ из map
|
||||
func (c *Collection) InsertFromMap(fields map[string]interface{}) error {
|
||||
doc := NewDocument()
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
return c.Insert(doc)
|
||||
}
|
||||
|
||||
// Find находит документ по ID (с использованием первичного индекса)
|
||||
func (c *Collection) Find(id string) (*Document, error) {
|
||||
if val, ok := c.docs.Load(id); ok {
|
||||
doc := val.(*Document)
|
||||
// Проверяем, не истёк ли TTL
|
||||
if c.metadata.Settings.TTLSeconds > 0 {
|
||||
if time.Now().UnixMilli()-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
|
||||
c.Delete(id) // Автоматически удаляем просроченный документ
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
// FindByIndex находит документы по значению индексированного поля
|
||||
// Исправлено: корректный поиск для неуникальных индексов
|
||||
func (c *Collection) FindByIndex(indexName string, value interface{}) ([]*Document, error) {
|
||||
idxVal, ok := c.indexes.Load(indexName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("index not found: %s", indexName)
|
||||
}
|
||||
|
||||
index := idxVal.(*Index)
|
||||
docs := make([]*Document, 0)
|
||||
|
||||
if index.Unique {
|
||||
// Уникальный индекс возвращает один документ
|
||||
if docID, ok := index.data.Load(value); ok {
|
||||
if doc, err := c.Find(docID.(string)); err == nil {
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Исправлено: для неуникального индекса нужно найти все документы с данным значением
|
||||
index.data.Range(func(key, val interface{}) bool {
|
||||
// key - значение индекса, val - ID документа
|
||||
if fmt.Sprintf("%v", key) == fmt.Sprintf("%v", value) {
|
||||
if doc, err := c.Find(val.(string)); err == nil {
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// FindByIndexPrefix находит документы по префиксу индекса (для строковых полей)
|
||||
func (c *Collection) FindByIndexPrefix(indexName string, prefix string) ([]*Document, error) {
|
||||
idxVal, ok := c.indexes.Load(indexName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("index not found: %s", indexName)
|
||||
}
|
||||
|
||||
index := idxVal.(*Index)
|
||||
docs := make([]*Document, 0)
|
||||
|
||||
index.data.Range(func(key, val interface{}) bool {
|
||||
if keyStr, ok := key.(string); ok {
|
||||
if strings.HasPrefix(keyStr, prefix) {
|
||||
if doc, err := c.Find(val.(string)); err == nil {
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// Update обновляет документ по ID
|
||||
// Исправлено: корректное обновление индексов при изменении индексированных полей
|
||||
func (c *Collection) Update(id string, updates map[string]interface{}) error {
|
||||
val, ok := c.docs.Load(id)
|
||||
if !ok {
|
||||
return fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
oldDoc := val.(*Document)
|
||||
|
||||
// Создаём копию для проверки уникальности
|
||||
newDoc := oldDoc.Clone()
|
||||
if err := newDoc.Update(updates); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверяем ограничения
|
||||
if err := c.constraints.ValidateDocument(newDoc); err != nil {
|
||||
return fmt.Errorf("constraint violation: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем уникальные индексы
|
||||
if err := c.checkUniqueConstraintsUpdate(oldDoc, newDoc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Исправлено: сначала удаляем старые индексы, потом добавляем новые
|
||||
c.removeFromIndexes(oldDoc)
|
||||
c.addToIndexes(newDoc)
|
||||
|
||||
// Сохраняем обновлённый документ
|
||||
c.docs.Store(id, newDoc)
|
||||
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete удаляет документ по ID
|
||||
// Исправлено: удаление из всех индексов перед удалением документа
|
||||
func (c *Collection) Delete(id string) error {
|
||||
val, ok := c.docs.Load(id)
|
||||
if !ok {
|
||||
return fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
doc := val.(*Document)
|
||||
|
||||
// Удаляем из индексов
|
||||
c.removeFromIndexes(doc)
|
||||
|
||||
// Удаляем документ
|
||||
c.docs.Delete(id)
|
||||
|
||||
// Обновляем метаданные
|
||||
c.docCount.Add(-1)
|
||||
// Размер не обновляем для простоты (можно пересчитать при необходимости)
|
||||
c.metadata.DocumentCount = c.docCount.Load()
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeFromIndexes удаляет документ из всех индексов (wait-free)
|
||||
func (c *Collection) removeFromIndexes(doc *Document) {
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
indexValue := c.extractIndexValue(doc, index.Fields)
|
||||
index.data.Delete(indexValue)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// addToIndexes добавляет документ во все индексы (wait-free)
|
||||
func (c *Collection) addToIndexes(doc *Document) {
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
indexValue := c.extractIndexValue(doc, index.Fields)
|
||||
if index.Unique {
|
||||
index.data.LoadOrStore(indexValue, doc.ID)
|
||||
} else {
|
||||
index.data.Store(indexValue, doc.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// CreateIndex создаёт новый индекс на коллекции (wait-free friendly)
|
||||
func (c *Collection) CreateIndex(name string, fields []string, unique bool) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if _, exists := c.indexes.Load(name); exists {
|
||||
return fmt.Errorf("index %s already exists", name)
|
||||
}
|
||||
|
||||
index := &Index{
|
||||
Name: name,
|
||||
Fields: fields,
|
||||
Unique: unique,
|
||||
}
|
||||
|
||||
// Строим индекс на существующих документах (wait-free)
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
doc := value.(*Document)
|
||||
indexValue := c.extractIndexValue(doc, fields)
|
||||
if unique {
|
||||
if _, loaded := index.data.LoadOrStore(indexValue, doc.ID); loaded {
|
||||
// Найден дубликат - откатываем создание индекса
|
||||
c.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
index.data.Store(indexValue, doc.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
c.indexes.Store(name, index)
|
||||
c.metadata.IndexCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropIndex удаляет индекс
|
||||
func (c *Collection) DropIndex(name string) error {
|
||||
if _, exists := c.indexes.LoadAndDelete(name); !exists {
|
||||
return fmt.Errorf("index not found: %s", name)
|
||||
}
|
||||
c.metadata.IndexCount--
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIndexes возвращает список всех индексов
|
||||
func (c *Collection) GetIndexes() []string {
|
||||
names := make([]string, 0)
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
names = append(names, key.(string))
|
||||
return true
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
// extractIndexValue извлекает значение из документа для индексации
|
||||
func (c *Collection) extractIndexValue(doc *Document, fields []string) interface{} {
|
||||
if len(fields) == 1 {
|
||||
val, _ := doc.GetField(fields[0])
|
||||
return val
|
||||
}
|
||||
|
||||
// Составной индекс - возвращаем строковое представление
|
||||
parts := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
parts = append(parts, fmt.Sprintf("%v", val))
|
||||
} else {
|
||||
parts = append(parts, "NULL")
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
// updateIndexes обновляет индексы для документа (исправлено: используем отдельные методы)
|
||||
func (c *Collection) updateIndexes(doc *Document, add bool) {
|
||||
if add {
|
||||
c.addToIndexes(doc)
|
||||
} else {
|
||||
c.removeFromIndexes(doc)
|
||||
}
|
||||
}
|
||||
|
||||
// checkUniqueConstraints проверяет уникальные индексы перед вставкой
|
||||
func (c *Collection) checkUniqueConstraints(doc *Document) error {
|
||||
var errs []string
|
||||
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
if index.Unique {
|
||||
indexValue := c.extractIndexValue(doc, index.Fields)
|
||||
if _, exists := index.data.Load(indexValue); exists {
|
||||
errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, indexValue))
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkUniqueConstraintsUpdate проверяет уникальность при обновлении
|
||||
func (c *Collection) checkUniqueConstraintsUpdate(oldDoc, newDoc *Document) error {
|
||||
var errs []string
|
||||
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
if index.Unique {
|
||||
oldValue := c.extractIndexValue(oldDoc, index.Fields)
|
||||
newValue := c.extractIndexValue(newDoc, index.Fields)
|
||||
|
||||
if fmt.Sprintf("%v", oldValue) != fmt.Sprintf("%v", newValue) {
|
||||
if _, exists := index.data.Load(newValue); exists {
|
||||
errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, newValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDocument валидирует документ согласно схеме коллекции
|
||||
func (c *Collection) validateDocument(doc *Document) error {
|
||||
if doc.ID == "" {
|
||||
return fmt.Errorf("document must have _id field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ttlCleanupLoop периодически удаляет просроченные документы
|
||||
func (c *Collection) ttlCleanupLoop() {
|
||||
ticker := time.NewTicker(time.Duration(c.metadata.Settings.TTLSeconds/2) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
now := time.Now().UnixMilli()
|
||||
toDelete := make([]string, 0)
|
||||
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
doc := value.(*Document)
|
||||
if now-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
|
||||
toDelete = append(toDelete, doc.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, id := range toDelete {
|
||||
c.Delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count возвращает количество документов в коллекции
|
||||
func (c *Collection) Count() int64 {
|
||||
return c.docCount.Load()
|
||||
}
|
||||
|
||||
// Size возвращает размер коллекции в байтах
|
||||
func (c *Collection) Size() int64 {
|
||||
return c.sizeBytes.Load()
|
||||
}
|
||||
|
||||
// GetAllDocuments возвращает все документы коллекции
|
||||
func (c *Collection) GetAllDocuments() []*Document {
|
||||
docs := make([]*Document, 0, c.docCount.Load())
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
docs = append(docs, value.(*Document))
|
||||
return true
|
||||
})
|
||||
return docs
|
||||
}
|
||||
|
||||
// FindByFilter находит документы по произвольному фильтру
|
||||
func (c *Collection) FindByFilter(filter func(*Document) bool) []*Document {
|
||||
results := make([]*Document, 0)
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
doc := value.(*Document)
|
||||
if filter(doc) {
|
||||
results = append(results, doc)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
// GetMetadata возвращает метаданные коллекции
|
||||
func (c *Collection) GetMetadata() *CollectionMetadata {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.metadata
|
||||
}
|
||||
|
||||
// Drop удаляет все документы из коллекции
|
||||
func (c *Collection) Drop() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.docs = sync.Map{}
|
||||
c.indexes = sync.Map{}
|
||||
|
||||
if c.metadata.Settings.AutoIndexID {
|
||||
c.CreateIndex("_id_", []string{"_id"}, true)
|
||||
}
|
||||
|
||||
c.docCount.Store(0)
|
||||
c.sizeBytes.Store(0)
|
||||
c.metadata.DocumentCount = 0
|
||||
c.metadata.SizeBytes = 0
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== Constraints Methods ==========
|
||||
|
||||
// AddRequiredField добавляет обязательное поле
|
||||
func (c *Collection) AddRequiredField(field string) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.RequiredFields[field] = true
|
||||
}
|
||||
|
||||
// AddUniqueConstraint добавляет ограничение уникальности
|
||||
func (c *Collection) AddUniqueConstraint(field string) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.UniqueFields[field] = true
|
||||
// Также создаём уникальный индекс
|
||||
c.CreateIndex("unique_"+field, []string{field}, true)
|
||||
}
|
||||
|
||||
// AddMinConstraint добавляет минимальное значение
|
||||
func (c *Collection) AddMinConstraint(field string, min float64) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.MinValues[field] = min
|
||||
}
|
||||
|
||||
// AddMaxConstraint добавляет максимальное значение
|
||||
func (c *Collection) AddMaxConstraint(field string, max float64) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.MaxValues[field] = max
|
||||
}
|
||||
|
||||
// AddPatternConstraint добавляет regexp паттерн
|
||||
func (c *Collection) AddPatternConstraint(field string, pattern string) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.PatternFields[field] = pattern
|
||||
}
|
||||
|
||||
// AddEnumConstraint добавляет допустимые значения
|
||||
func (c *Collection) AddEnumConstraint(field string, values []interface{}) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.EnumFields[field] = values
|
||||
}
|
||||
|
||||
// ValidateDocument проверяет документ на соответствие ограничениям
|
||||
func (cons *Constraints) ValidateDocument(doc *Document) error {
|
||||
cons.mu.RLock()
|
||||
defer cons.mu.RUnlock()
|
||||
|
||||
// Проверка обязательных полей
|
||||
for field := range cons.RequiredFields {
|
||||
if !doc.HasField(field) {
|
||||
return fmt.Errorf("required field '%s' is missing", field)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка уникальности (дополнительно к индексам)
|
||||
// (основная проверка в индексах)
|
||||
|
||||
// Проверка числовых ограничений
|
||||
for field, minVal := range cons.MinValues {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
if numVal, ok := toFloat64(val); ok {
|
||||
if numVal < minVal {
|
||||
return fmt.Errorf("field '%s' value %v is less than minimum %v", field, numVal, minVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for field, maxVal := range cons.MaxValues {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
if numVal, ok := toFloat64(val); ok {
|
||||
if numVal > maxVal {
|
||||
return fmt.Errorf("field '%s' value %v exceeds maximum %v", field, numVal, maxVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка enum
|
||||
for field, allowedValues := range cons.EnumFields {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
found := false
|
||||
for _, allowed := range allowedValues {
|
||||
if fmt.Sprintf("%v", val) == fmt.Sprintf("%v", allowed) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("field '%s' value '%v' not in allowed list", field, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== ACL Methods ==========
|
||||
|
||||
// SetACL устанавливает ACL для коллекции
|
||||
func (c *Collection) SetACL(role string, canRead, canWrite, canDelete, isAdmin bool) {
|
||||
c.acl.mu.Lock()
|
||||
defer c.acl.mu.Unlock()
|
||||
|
||||
if canRead {
|
||||
c.acl.ReadRoles[role] = true
|
||||
}
|
||||
if canWrite {
|
||||
c.acl.WriteRoles[role] = true
|
||||
}
|
||||
if canDelete {
|
||||
c.acl.DeleteRoles[role] = true
|
||||
}
|
||||
if isAdmin {
|
||||
c.acl.AdminRoles[role] = true
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPermission проверяет наличие разрешения у роли
|
||||
func (c *Collection) CheckPermission(role, operation string) bool {
|
||||
c.acl.mu.RLock()
|
||||
defer c.acl.mu.RUnlock()
|
||||
|
||||
// Администратор имеет все права
|
||||
if c.acl.AdminRoles[role] {
|
||||
return true
|
||||
}
|
||||
|
||||
switch operation {
|
||||
case "read":
|
||||
return c.acl.ReadRoles[role]
|
||||
case "write":
|
||||
return c.acl.WriteRoles[role]
|
||||
case "delete":
|
||||
return c.acl.DeleteRoles[role]
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// toFloat64 конвертирует interface{} в float64
|
||||
func toFloat64(val interface{}) (float64, bool) {
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return float64(v), true
|
||||
case int64:
|
||||
return float64(v), true
|
||||
case float64:
|
||||
return v, true
|
||||
case float32:
|
||||
return float64(v), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
480
internal/storage/document.go
Normal file
480
internal/storage/document.go
Normal file
@@ -0,0 +1,480 @@
|
||||
// Файл: internal/storage/document.go
|
||||
// Назначение: Определение структуры документа, его методов для работы
|
||||
// с полями, кортежами (вложенными документами) и сериализации в MessagePack.
|
||||
// Документ является основной единицей хранения в СУБД futriis.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"futriis/internal/compression"
|
||||
"futriis/internal/serializer"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Document представляет документ в коллекции (аналог строки в реляционной СУБД)
|
||||
type Document struct {
|
||||
ID string `msgpack:"_id"` // Уникальный идентификатор документа
|
||||
Fields map[string]interface{} `msgpack:"fields"` // Поля документа (аналог колонок)
|
||||
CreatedAt int64 `msgpack:"created_at"` // Время создания (Unix миллисекунды)
|
||||
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления
|
||||
Version uint64 `msgpack:"version"` // Версия документа (для оптимистичных блокировок)
|
||||
Compressed bool `msgpack:"compressed"` // Флаг, сжат ли документ
|
||||
OriginalSize int64 `msgpack:"original_size"` // Оригинальный размер до сжатия
|
||||
mu sync.RWMutex `msgpack:"-"` // Блокировка для wait-free операций
|
||||
}
|
||||
|
||||
// Tuple представляет вложенный документ (аналог кортежа в реляционной СУБД)
|
||||
type Tuple struct {
|
||||
Fields map[string]interface{} `msgpack:"fields"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Field представляет отдельное поле документа (аналог колонки)
|
||||
type Field struct {
|
||||
Name string `msgpack:"name"`
|
||||
Type FieldType `msgpack:"type"`
|
||||
Value interface{} `msgpack:"value"`
|
||||
}
|
||||
|
||||
// FieldType определяет тип поля документа
|
||||
type FieldType int
|
||||
|
||||
const (
|
||||
TypeString FieldType = iota
|
||||
TypeNumber
|
||||
TypeBoolean
|
||||
TypeTuple // Вложенный документ
|
||||
TypeArray
|
||||
TypeNull
|
||||
)
|
||||
|
||||
// NewDocument создаёт новый документ с автоматической генерацией ID
|
||||
func NewDocument() *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
return &Document{
|
||||
ID: uuid.New().String(),
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDocumentWithID создаёт документ с указанным ID
|
||||
func NewDocumentWithID(id string) *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
return &Document{
|
||||
ID: id,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SetField устанавливает значение поля документа (wait-free)
|
||||
func (d *Document) SetField(name string, value interface{}) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.Fields[name] = value
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // При изменении документа снимаем флаг сжатия
|
||||
}
|
||||
|
||||
// GetField возвращает значение поля документа
|
||||
func (d *Document) GetField(name string) (interface{}, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if val, ok := d.Fields[name]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field not found: %s", name)
|
||||
}
|
||||
|
||||
// DeleteField удаляет поле из документа
|
||||
func (d *Document) DeleteField(name string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
delete(d.Fields, name)
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false
|
||||
}
|
||||
|
||||
// HasField проверяет наличие поля в документе
|
||||
func (d *Document) HasField(name string) bool {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
_, ok := d.Fields[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetFields возвращает копию всех полей документа
|
||||
func (d *Document) GetFields() map[string]interface{} {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
copy := make(map[string]interface{})
|
||||
for k, v := range d.Fields {
|
||||
copy[k] = v
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// SetTuple устанавливает вложенный документ (кортеж) в поле
|
||||
func (d *Document) SetTuple(fieldName string, tuple *Tuple) {
|
||||
d.SetField(fieldName, tuple)
|
||||
}
|
||||
|
||||
// GetTuple возвращает вложенный документ из поля
|
||||
func (d *Document) GetTuple(fieldName string) (*Tuple, error) {
|
||||
val, err := d.GetField(fieldName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tuple, ok := val.(*Tuple); ok {
|
||||
return tuple, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field %s is not a tuple", fieldName)
|
||||
}
|
||||
|
||||
// Serialize сериализует документ в MessagePack с поддержкой сжатия
|
||||
func (d *Document) Serialize() ([]byte, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
data, err := serializer.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SerializeCompressed сериализует и сжимает документ
|
||||
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
// Сериализуем документ
|
||||
data, err := serializer.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, нужно ли сжимать
|
||||
if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize {
|
||||
compressed, err := compression.Compress(data, compressionConfig)
|
||||
if err != nil {
|
||||
// При ошибке сжатия возвращаем несжатые данные
|
||||
return data, nil
|
||||
}
|
||||
return compressed, nil
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Deserialize десериализует документ из MessagePack (автоматически определяет сжатие)
|
||||
func (d *Document) Deserialize(data []byte) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// Пытаемся определить, сжаты ли данные
|
||||
// Для этого пробуем распаковать, если не получается - данные несжатые
|
||||
decompressed, err := compression.DecompressAuto(data)
|
||||
if err == nil && len(decompressed) < len(data) {
|
||||
// Данные были сжаты, используем распакованную версию
|
||||
if err := serializer.Unmarshal(decompressed, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(len(decompressed))
|
||||
} else {
|
||||
// Данные не сжаты или не удалось распаковать
|
||||
if err := serializer.Unmarshal(data, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
}
|
||||
|
||||
// Обновляем временные метки при десериализации
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone создаёт глубокую копию документа
|
||||
func (d *Document) Clone() *Document {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
clone := &Document{
|
||||
ID: d.ID,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
Version: d.Version,
|
||||
Compressed: d.Compressed,
|
||||
OriginalSize: d.OriginalSize,
|
||||
}
|
||||
|
||||
// Глубокое копирование полей
|
||||
for k, v := range d.Fields {
|
||||
clone.Fields[k] = deepCopyValue(v)
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// Update применяет обновление к документу (атомарно)
|
||||
func (d *Document) Update(updates map[string]interface{}) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
for k, v := range updates {
|
||||
d.Fields[k] = v
|
||||
}
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // После обновления документ больше не сжат
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compress сжимает документ в памяти
|
||||
func (d *Document) Compress(config *compression.Config) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Сохраняем текущее состояние
|
||||
originalSize := len(d.Fields)
|
||||
if originalSize < config.MinSize {
|
||||
return nil // Не сжимаем маленькие документы
|
||||
}
|
||||
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(originalSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decompress распаковывает документ в памяти
|
||||
func (d *Document) Decompress() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if !d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompressionRatio возвращает коэффициент сжатия
|
||||
func (d *Document) GetCompressionRatio() float64 {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if !d.Compressed || d.OriginalSize == 0 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
currentSize := len(d.Fields)
|
||||
return float64(currentSize) / float64(d.OriginalSize)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &Tuple{
|
||||
Fields: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Set устанавливает поле во вложенном документе
|
||||
func (t *Tuple) Set(name string, value interface{}) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.Fields[name] = value
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// GetNestedField получает значение по точечному пути (например, "user.address.city")
|
||||
func (d *Document) GetNestedField(path string) (interface{}, error) {
|
||||
parts := strings.Split(path, ".")
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
current := interface{}(d)
|
||||
for _, part := range parts {
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
val, err := v.GetField(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = val
|
||||
case *Tuple:
|
||||
val, err := v.Get(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = val
|
||||
case map[string]interface{}:
|
||||
if val, ok := v[part]; ok {
|
||||
current = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("field not found: %s", part)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot navigate into non-document value at %s", part)
|
||||
}
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
// SetNestedField устанавливает значение по точечному пути
|
||||
func (d *Document) SetNestedField(path string, value interface{}) error {
|
||||
parts := strings.Split(path, ".")
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
// Если путь состоит из одного элемента, просто устанавливаем поле
|
||||
if len(parts) == 1 {
|
||||
d.SetField(parts[0], value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Иначе нужно создать промежуточные структуры
|
||||
current := interface{}(d)
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
part := parts[i]
|
||||
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
if !v.HasField(part) {
|
||||
// Создаём новый кортеж, если поле не существует
|
||||
newTuple := NewTuple()
|
||||
v.SetField(part, newTuple)
|
||||
current = newTuple
|
||||
} else {
|
||||
field, _ := v.GetField(part)
|
||||
if tuple, ok := field.(*Tuple); ok {
|
||||
current = tuple
|
||||
} else {
|
||||
return fmt.Errorf("field %s is not a tuple", part)
|
||||
}
|
||||
}
|
||||
case *Tuple:
|
||||
if val, err := v.Get(part); err == nil {
|
||||
if tuple, ok := val.(*Tuple); ok {
|
||||
current = tuple
|
||||
} else {
|
||||
return fmt.Errorf("field %s is not a tuple", part)
|
||||
}
|
||||
} else {
|
||||
newTuple := NewTuple()
|
||||
v.Set(part, newTuple)
|
||||
current = newTuple
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("cannot set nested field on non-document value")
|
||||
}
|
||||
}
|
||||
|
||||
// Устанавливаем значение в последний элемент пути
|
||||
lastPart := parts[len(parts)-1]
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
v.SetField(lastPart, value)
|
||||
case *Tuple:
|
||||
v.Set(lastPart, value)
|
||||
default:
|
||||
return fmt.Errorf("cannot set field on non-document value")
|
||||
}
|
||||
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Compressed = false
|
||||
return nil
|
||||
}
|
||||
224
internal/storage/engine.go
Normal file
224
internal/storage/engine.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Файл: internal/storage/engine.go
|
||||
// Назначение: In-memory движок хранения документов с поддержкой коллекций,
|
||||
// слайсов (аналог БД), тапплов (аналог таблиц), полей и кортежей.
|
||||
// Полностью wait-free с использованием sync.Map и атомарных операций.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// Storage представляет основное хранилище баз данных
|
||||
type Storage struct {
|
||||
databases sync.Map // map[string]*Database
|
||||
pageSize int64
|
||||
logger *log.Logger
|
||||
totalDocs atomic.Int64
|
||||
}
|
||||
|
||||
// Database представляет базу данных (аналог слайса в реляционных СУБД)
|
||||
type Database struct {
|
||||
name string
|
||||
collections sync.Map // map[string]*Collection
|
||||
}
|
||||
|
||||
// NewStorage создаёт новый экземпляр хранилища
|
||||
func NewStorage(pageSizeMB int, logger *log.Logger) *Storage {
|
||||
return &Storage{
|
||||
pageSize: int64(pageSizeMB) * 1024 * 1024,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDatabase создаёт новую базу данных
|
||||
func (s *Storage) CreateDatabase(name string) error {
|
||||
if _, exists := s.databases.LoadOrStore(name, &Database{name: name}); exists {
|
||||
return fmt.Errorf("database already exists")
|
||||
}
|
||||
AuditDatabaseOperation("CREATE", name)
|
||||
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 _, ok := s.databases.LoadAndDelete(name); !ok {
|
||||
return fmt.Errorf("database not found")
|
||||
}
|
||||
AuditDatabaseOperation("DROP", name)
|
||||
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
|
||||
}
|
||||
|
||||
// CreateCollection создаёт новую коллекцию в базе данных
|
||||
func (db *Database) CreateCollection(name string) error {
|
||||
if _, exists := db.collections.LoadOrStore(name, NewCollection(name, nil)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
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(name, settings)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
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")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// SerializeDatabase сериализует всю базу данных в MessagePack
|
||||
func (db *Database) SerializeDatabase() ([]byte, error) {
|
||||
dbData := make(map[string]interface{})
|
||||
|
||||
db.collections.Range(func(key, value interface{}) bool {
|
||||
coll := value.(*Collection)
|
||||
collData := make(map[string]interface{})
|
||||
|
||||
// Собираем все документы коллекции
|
||||
docs := coll.GetAllDocuments()
|
||||
collDocs := make([]*Document, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
collDocs = append(collDocs, doc)
|
||||
}
|
||||
collData["documents"] = collDocs
|
||||
collData["metadata"] = coll.GetMetadata()
|
||||
|
||||
dbData[key.(string)] = collData
|
||||
return true
|
||||
})
|
||||
|
||||
return serializer.Marshal(dbData)
|
||||
}
|
||||
|
||||
// DeserializeDatabase десериализует базу данных из MessagePack
|
||||
func (db *Database) DeserializeDatabase(data []byte) error {
|
||||
var dbData map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &dbData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for collName, collDataRaw := range dbData {
|
||||
collData := collDataRaw.(map[string]interface{})
|
||||
|
||||
// Создаём коллекцию
|
||||
settings := &CollectionSettings{
|
||||
MaxDocuments: 0,
|
||||
ValidateSchema: false,
|
||||
AutoIndexID: true,
|
||||
TTLSeconds: 0,
|
||||
}
|
||||
|
||||
if metaRaw, ok := collData["metadata"]; ok {
|
||||
if meta, ok := metaRaw.(*CollectionMetadata); ok {
|
||||
if meta.Settings != nil {
|
||||
settings = meta.Settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coll := NewCollection(collName, settings)
|
||||
|
||||
// Восстанавливаем документы
|
||||
if docsRaw, ok := collData["documents"]; ok {
|
||||
if docs, ok := docsRaw.([]*Document); ok {
|
||||
for _, doc := range docs {
|
||||
coll.Insert(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.collections.Store(collName, coll)
|
||||
AuditCollectionOperation("RESTORE", db.name, collName, settings)
|
||||
}
|
||||
|
||||
return 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
|
||||
}
|
||||
382
internal/storage/transaction.go
Normal file
382
internal/storage/transaction.go
Normal file
@@ -0,0 +1,382 @@
|
||||
// Файл: internal/storage/transaction.go
|
||||
// Назначение: Реализация транзакций с поддержкой MVCC (Multi-Version Concurrency Control)
|
||||
// и WAL (Write-Ahead Logging) без блокировок. Использует атомарные операции и версионирование.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// TransactionID представляет уникальный идентификатор транзакции
|
||||
type TransactionID uint64
|
||||
|
||||
// TransactionState представляет состояние транзакции
|
||||
type TransactionState int32
|
||||
|
||||
const (
|
||||
TransactionActive TransactionState = iota
|
||||
TransactionCommitted
|
||||
TransactionAborted
|
||||
)
|
||||
|
||||
// TransactionRecord представляет запись в WAL
|
||||
type TransactionRecord struct {
|
||||
ID TransactionID `msgpack:"id"`
|
||||
State TransactionState `msgpack:"state"`
|
||||
Timestamp int64 `msgpack:"timestamp"`
|
||||
Operations []Operation `msgpack:"operations"`
|
||||
}
|
||||
|
||||
// Operation представляет одну операцию в транзакции
|
||||
type Operation struct {
|
||||
Type string `msgpack:"type"` // "insert", "update", "delete"
|
||||
Database string `msgpack:"database"`
|
||||
Collection string `msgpack:"collection"`
|
||||
DocumentID string `msgpack:"document_id"`
|
||||
Data map[string]interface{} `msgpack:"data"`
|
||||
Version uint64 `msgpack:"version"`
|
||||
}
|
||||
|
||||
// DocumentVersion представляет версию документа для MVCC
|
||||
type DocumentVersion struct {
|
||||
Document *Document `msgpack:"document"`
|
||||
Timestamp int64 `msgpack:"timestamp"`
|
||||
TxID TransactionID `msgpack:"tx_id"`
|
||||
}
|
||||
|
||||
// TransactionManager управляет транзакциями
|
||||
type TransactionManager struct {
|
||||
activeTransactions sync.Map // map[TransactionID]*Transaction
|
||||
nextTxID atomic.Uint64
|
||||
wal *WriteAheadLog
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Transaction представляет одну транзакцию
|
||||
type Transaction struct {
|
||||
ID TransactionID
|
||||
State atomic.Int32
|
||||
Operations []Operation
|
||||
StartTime int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// WriteAheadLog реализует журнал предзаписи
|
||||
type WriteAheadLog struct {
|
||||
file *os.File
|
||||
writeChan chan []byte
|
||||
done chan struct{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
globalTxManager *TransactionManager
|
||||
txManagerOnce sync.Once
|
||||
currentTx atomic.Value // *Transaction
|
||||
)
|
||||
|
||||
// InitTransactionManager инициализирует глобальный менеджер транзакций
|
||||
func InitTransactionManager(walPath string) error {
|
||||
var err error
|
||||
txManagerOnce.Do(func() {
|
||||
globalTxManager = &TransactionManager{
|
||||
nextTxID: atomic.Uint64{},
|
||||
}
|
||||
globalTxManager.nextTxID.Store(1)
|
||||
err = globalTxManager.initWAL(walPath)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// initWAL инициализирует Write-Ahead Log
|
||||
func (tm *TransactionManager) initWAL(walPath string) error {
|
||||
wal, err := NewWriteAheadLog(walPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tm.wal = wal
|
||||
|
||||
// Восстанавливаем состояние из WAL при запуске
|
||||
go tm.recoverFromWAL()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewWriteAheadLog создаёт новый WAL
|
||||
func NewWriteAheadLog(path string) (*WriteAheadLog, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wal := &WriteAheadLog{
|
||||
file: file,
|
||||
writeChan: make(chan []byte, 10000),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
go wal.writerLoop()
|
||||
|
||||
return wal, nil
|
||||
}
|
||||
|
||||
// writerLoop асинхронно записывает данные в WAL
|
||||
func (wal *WriteAheadLog) writerLoop() {
|
||||
for data := range wal.writeChan {
|
||||
wal.mu.Lock()
|
||||
// Формат записи: [длина (4 байта)][данные]
|
||||
lenBuf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(lenBuf, uint32(len(data)))
|
||||
|
||||
if _, err := wal.file.Write(lenBuf); err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err := wal.file.Write(data); err != nil {
|
||||
continue
|
||||
}
|
||||
wal.file.Sync()
|
||||
wal.mu.Unlock()
|
||||
}
|
||||
close(wal.done)
|
||||
}
|
||||
|
||||
// Write записывает запись в WAL
|
||||
func (wal *WriteAheadLog) Write(record *TransactionRecord) error {
|
||||
data, err := serializer.Marshal(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case wal.writeChan <- data:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("WAL buffer full")
|
||||
}
|
||||
}
|
||||
|
||||
// Close закрывает WAL
|
||||
func (wal *WriteAheadLog) Close() error {
|
||||
close(wal.writeChan)
|
||||
<-wal.done
|
||||
return wal.file.Close()
|
||||
}
|
||||
|
||||
// BeginTransaction начинает новую транзакцию
|
||||
func BeginTransaction() *Transaction {
|
||||
if globalTxManager == nil {
|
||||
InitTransactionManager("futriis.wal")
|
||||
}
|
||||
|
||||
tx := &Transaction{
|
||||
ID: TransactionID(globalTxManager.nextTxID.Add(1) - 1),
|
||||
StartTime: time.Now().UnixMilli(),
|
||||
Operations: make([]Operation, 0),
|
||||
}
|
||||
tx.State.Store(int32(TransactionActive))
|
||||
|
||||
globalTxManager.activeTransactions.Store(tx.ID, tx)
|
||||
|
||||
// Сохраняем как текущую транзакцию для горутины
|
||||
currentTx.Store(tx)
|
||||
|
||||
// Записываем начало транзакции в WAL
|
||||
record := &TransactionRecord{
|
||||
ID: tx.ID,
|
||||
State: TransactionActive,
|
||||
Timestamp: tx.StartTime,
|
||||
Operations: tx.Operations,
|
||||
}
|
||||
globalTxManager.wal.Write(record)
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
// AddToTransaction добавляет операцию в текущую транзакцию
|
||||
func AddToTransaction(coll *Collection, opType string, doc *Document) error {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return fmt.Errorf("no active transaction")
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
if TransactionState(tx.State.Load()) != TransactionActive {
|
||||
return fmt.Errorf("transaction is not active")
|
||||
}
|
||||
|
||||
op := Operation{
|
||||
Type: opType,
|
||||
Database: coll.Name(), // В реальной реализации нужно передавать имя БД
|
||||
Collection: coll.Name(),
|
||||
DocumentID: doc.ID,
|
||||
Data: doc.GetFields(),
|
||||
Version: doc.Version,
|
||||
}
|
||||
|
||||
tx.mu.Lock()
|
||||
tx.Operations = append(tx.Operations, op)
|
||||
tx.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitCurrentTransaction коммитит текущую транзакцию
|
||||
func CommitCurrentTransaction() error {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return fmt.Errorf("no active transaction")
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
if TransactionState(tx.State.Load()) != TransactionActive {
|
||||
return fmt.Errorf("transaction is not active")
|
||||
}
|
||||
|
||||
// Применяем все операции атомарно
|
||||
for _, op := range tx.Operations {
|
||||
if err := applyOperation(op); err != nil {
|
||||
// Откатываем при ошибке
|
||||
AbortCurrentTransaction()
|
||||
return fmt.Errorf("transaction commit failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
tx.State.Store(int32(TransactionCommitted))
|
||||
|
||||
// Записываем коммит в WAL
|
||||
record := &TransactionRecord{
|
||||
ID: tx.ID,
|
||||
State: TransactionCommitted,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Operations: tx.Operations,
|
||||
}
|
||||
globalTxManager.wal.Write(record)
|
||||
|
||||
// Очищаем текущую транзакцию
|
||||
currentTx.Store(nil)
|
||||
globalTxManager.activeTransactions.Delete(tx.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AbortCurrentTransaction откатывает текущую транзакцию
|
||||
func AbortCurrentTransaction() error {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return fmt.Errorf("no active transaction")
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
tx.State.Store(int32(TransactionAborted))
|
||||
|
||||
// Записываем откат в WAL
|
||||
record := &TransactionRecord{
|
||||
ID: tx.ID,
|
||||
State: TransactionAborted,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Operations: tx.Operations,
|
||||
}
|
||||
globalTxManager.wal.Write(record)
|
||||
|
||||
// Очищаем текущую транзакцию
|
||||
currentTx.Store(nil)
|
||||
globalTxManager.activeTransactions.Delete(tx.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasActiveTransaction проверяет наличие активной транзакции
|
||||
func HasActiveTransaction() bool {
|
||||
return currentTx.Load() != nil
|
||||
}
|
||||
|
||||
// FindInTransaction ищет документ в контексте транзакции
|
||||
func FindInTransaction(coll *Collection, id string) (*Document, error) {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return coll.Find(id)
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
|
||||
// Сначала ищем в операциях транзакции
|
||||
for i := len(tx.Operations) - 1; i >= 0; i-- {
|
||||
op := tx.Operations[i]
|
||||
if op.DocumentID == id {
|
||||
if op.Type == "delete" {
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
// Создаём документ из данных операции
|
||||
doc := NewDocumentWithID(op.DocumentID)
|
||||
for k, v := range op.Data {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
doc.Version = op.Version
|
||||
return doc, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем в основном хранилище
|
||||
return coll.Find(id)
|
||||
}
|
||||
|
||||
// applyOperation применяет операцию к хранилищу
|
||||
func applyOperation(op Operation) error {
|
||||
// В реальной реализации здесь будет применение операции к соответствующей коллекции
|
||||
// С использованием MVCC для версионирования
|
||||
|
||||
switch op.Type {
|
||||
case "insert":
|
||||
// Проверяем версию документа (MVCC)
|
||||
doc := NewDocumentWithID(op.DocumentID)
|
||||
for k, v := range op.Data {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
// Здесь должна быть вставка в коллекцию
|
||||
return nil
|
||||
case "update":
|
||||
// Обновление с проверкой версии
|
||||
return nil
|
||||
case "delete":
|
||||
// Удаление
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recoverFromWAL восстанавливает состояние из WAL после сбоя
|
||||
func (tm *TransactionManager) recoverFromWAL() {
|
||||
// В реальной реализации здесь будет чтение WAL и восстановление
|
||||
// незавершённых транзакций
|
||||
}
|
||||
|
||||
// GetTransaction возвращает транзакцию по ID
|
||||
func GetTransaction(id TransactionID) (*Transaction, bool) {
|
||||
if val, ok := globalTxManager.activeTransactions.Load(id); ok {
|
||||
return val.(*Transaction), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// MVCCSnapshot создаёт снапшот текущего состояния для MVCC
|
||||
func MVCCSnapshot() uint64 {
|
||||
return uint64(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// CreateDocumentVersion создаёт новую версию документа для MVCC
|
||||
func CreateDocumentVersion(doc *Document, txID TransactionID) *DocumentVersion {
|
||||
return &DocumentVersion{
|
||||
Document: doc.Clone(),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
TxID: txID,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user