Umsetzung der Ausbaustufen 2.1, 3.6, 3.8, 3.9, 4.1, 4.2, 4.4, 4.6, 5.1–5.4 (Details in AUSBAUSTUFEN.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
// Package workspace verwaltet die offenen Workspace-Dateien eines laufenden
|
||
// Prozesses.
|
||
//
|
||
// Jede Abteilung hat ihre eigene SQLite-Datei (siehe AUSBAUSTUFEN.md 4.4). Ein
|
||
// Manager öffnet sie bei Bedarf und hält sie offen, statt bei jedem Request
|
||
// neu zu öffnen — das Öffnen (inklusive Migrationsprüfung) kostet spürbar mehr
|
||
// als ein Map-Zugriff.
|
||
package workspace
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sync"
|
||
|
||
"munin/internal/search"
|
||
"munin/internal/store"
|
||
)
|
||
|
||
// Workspace bündelt, was zu einer offenen Workspace-Datei gehört: die
|
||
// Datenbank und ihren Suchindex. Beide sind an dieselbe Datei gebunden und
|
||
// werden deshalb gemeinsam verwaltet — ein Registry, der aus Versehen an der
|
||
// falschen Datenbank hängt, wäre ein stiller Fehler.
|
||
type Workspace struct {
|
||
ID string
|
||
Store *store.Store
|
||
Search *search.Registry
|
||
}
|
||
|
||
// Manager öffnet Workspace-Dateien bei Bedarf und hält sie offen.
|
||
//
|
||
// Bei 10–40 Workspaces (siehe AUSBAUSTUFEN.md 4.4) ist das unproblematisch —
|
||
// Verdrängung selten gebrauchter Dateien würde erst bei einer deutlich
|
||
// höheren Zahl nötig, siehe AUSBAUSTUFEN.md 1.2.
|
||
type Manager struct {
|
||
dir string // Ordner mit einer Datei je Workspace, z.B. "<DATA_DIR>/workspaces"
|
||
|
||
mu sync.Mutex
|
||
open map[string]*Workspace
|
||
}
|
||
|
||
// NewManager legt einen Manager an. dir muss bereits existieren — das
|
||
// übernimmt main.go beim Start.
|
||
func NewManager(dir string) *Manager {
|
||
return &Manager{dir: dir, open: make(map[string]*Workspace)}
|
||
}
|
||
|
||
// Get liefert den Workspace zu einer ID und öffnet ihn beim ersten Zugriff.
|
||
func (m *Manager) Get(workspaceID string) (*Workspace, error) {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
if w, ok := m.open[workspaceID]; ok {
|
||
return w, nil
|
||
}
|
||
|
||
s, err := store.Open(filepath.Join(m.dir, workspaceID+".db"))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("Workspace %s konnte nicht geöffnet werden: %w", workspaceID, err)
|
||
}
|
||
|
||
w := &Workspace{ID: workspaceID, Store: s, Search: search.NewRegistry(s)}
|
||
m.open[workspaceID] = w
|
||
return w, nil
|
||
}
|
||
|
||
// Delete schließt die Workspace-Datei, falls offen, und entfernt sie
|
||
// unwiederbringlich von der Platte — samt WAL/SHM-Nebendateien, die SQLite im
|
||
// laufenden Betrieb danebenlegt.
|
||
//
|
||
// Ruft nur die Datei ab — die Kontroll-Datenbank (workspaces, key_index) muss
|
||
// getrennt bereinigt werden, siehe ControlStore.DeleteWorkspace. Reihenfolge
|
||
// ist wichtig: Erst dort löschen, dann hier — sonst verweist die
|
||
// Kontroll-Datenbank kurzzeitig auf eine Datei, die es nicht mehr gibt.
|
||
func (m *Manager) Delete(workspaceID string) error {
|
||
m.mu.Lock()
|
||
if w, ok := m.open[workspaceID]; ok {
|
||
w.Store.Close()
|
||
delete(m.open, workspaceID)
|
||
}
|
||
m.mu.Unlock()
|
||
|
||
pfad := filepath.Join(m.dir, workspaceID+".db")
|
||
for _, endung := range []string{"", "-wal", "-shm"} {
|
||
if err := os.Remove(pfad + endung); err != nil && !os.IsNotExist(err) {
|
||
return fmt.Errorf("Workspace-Datei konnte nicht gelöscht werden: %w", err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Close schließt alle offenen Workspace-Dateien.
|
||
func (m *Manager) Close() error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
var erster error
|
||
for id, w := range m.open {
|
||
if err := w.Store.Close(); err != nil && erster == nil {
|
||
erster = err
|
||
}
|
||
delete(m.open, id)
|
||
}
|
||
return erster
|
||
}
|