// Package workspace verwaltet die offenen Workspaces eines laufenden // Prozesses. // // Jede Abteilung hat ihren eigenen, abgeschotteten Bereich (siehe // AUSBAUSTUFEN.md 4.4): eine SQLite-Datei oder ein PostgreSQL-Schema — welches // von beiden, entscheidet der store.Speicher (AUSBAUSTUFEN.md 4.10). Ein // Manager öffnet die Bereiche 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" "sort" "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 { speicher store.Speicher // SQLite-Dateien oder PostgreSQL-Schemas mu sync.Mutex open map[string]*Workspace } // NewManager legt einen Manager an, der seine Workspaces über speicher öffnet. func NewManager(speicher store.Speicher) *Manager { return &Manager{speicher: speicher, 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 := m.speicher.OpenWorkspace(workspaceID) 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 den Workspace, falls offen, und entfernt seinen Bereich // unwiederbringlich — die Datei samt Nebendateien bzw. das Schema samt // Tabellen (siehe store.Speicher.DeleteWorkspace). // // Räumt nur den Bereich 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 einen Bereich, den 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() return m.speicher.DeleteWorkspace(workspaceID) } // Speicher gibt den Speicher zurück, über den der Manager öffnet — für die // Systemseite (Beschreibung, Kapazität). func (m *Manager) Speicher() store.Speicher { return m.speicher } // Offen liefert die gerade geöffneten Workspaces, nach ID sortiert. Eine // Momentaufnahme: Währenddessen kann ein weiterer geöffnet werden. func (m *Manager) Offen() []*Workspace { m.mu.Lock() defer m.mu.Unlock() liste := make([]*Workspace, 0, len(m.open)) for _, w := range m.open { liste = append(liste, w) } sort.Slice(liste, func(i, j int) bool { return liste[i].ID < liste[j].ID }) return liste } // Ort nennt, wo die Daten eines Workspace liegen — Schema oder Datei (siehe // store.Speicher.Ort). Öffnet dafür nichts. func (m *Manager) Ort(workspaceID string) string { return m.speicher.Ort(workspaceID) } // 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 }