// 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.Backend (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 { backend store.Backend // SQLite-Dateien oder PostgreSQL-Schemas mu sync.Mutex open map[string]*Workspace } // NewManager legt einen Manager an, der seine Workspaces über backend öffnet. func NewManager(backend store.Backend) *Manager { return &Manager{backend: backend, 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.backend.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.Backend.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.backend.DeleteWorkspace(workspaceID) } // Backend gibt den Speicher zurück, über den der Manager öffnet — für die // Systemseite (Beschreibung, Kapazität). func (m *Manager) Backend() store.Backend { return m.backend } // OpenWorkspaces liefert die gerade geöffneten Workspaces, nach ID sortiert. Eine // Momentaufnahme: Währenddessen kann ein weiterer geöffnet werden. func (m *Manager) OpenWorkspaces() []*Workspace { m.mu.Lock() defer m.mu.Unlock() list := make([]*Workspace, 0, len(m.open)) for _, w := range m.open { list = append(list, w) } sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID }) return list } // Location nennt, wo die Daten eines Workspace liegen — Schema oder Datei (siehe // store.Backend.Ort). Öffnet dafür nichts. func (m *Manager) Location(workspaceID string) string { return m.backend.Location(workspaceID) } // Close schließt alle offenen Workspace-Dateien. func (m *Manager) Close() error { m.mu.Lock() defer m.mu.Unlock() var first error for id, w := range m.open { if err := w.Store.Close(); err != nil && first == nil { first = err } delete(m.open, id) } return first }