feat(PROJ-21/23): Pro-Tenant Xapian-Index + Tenant-LDAP Backend
PROJ-21 Phase 4:
- internal/index/tenant_manager.go: TenantIndexManager mit lazy-loading Pool
- internal/index/tenant_worker.go: TenantIndexWorker leitet Submit an richtigen Index
- Jeder Mandant bekommt eigenes Xapian-Verzeichnis (tenant-<id>/)
- handleSearch nutzt direkt Tenant-Index statt nachgelagertem Post-Filter
- runBackfill re-indexiert pro Mandant beim Start
PROJ-23 / PROJ-16 Phase B:
- internal/ldapconfig/tenant_store.go: TenantStore mit AES-256-GCM für tenant_ldap
- internal/api/ldap_tenants.go: 8 neue Handler (GET/PUT/DELETE/test für
/api/tenant/ldap und /api/admin/tenants/{id}/ldap)
- internal/auth/auth.go: Login-Fallback prüft tenant_ldap nach globalem LDAP
(Domain-Extraktion → tenant_ldap config → UpsertLDAPUser mit tenant_id)
- internal/api/server.go: SetTenantLDAP(), neue Routen registriert
- internal/tenantstore/store.go: GetByDomain() Interface für auth-Package
- cmd/archivmail/main.go: TenantLDAPStore + TenantIndexManager verdrahtet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ type MailDocument struct {
|
||||
HasAttachment bool
|
||||
Date time.Time
|
||||
Size int64
|
||||
TenantID *int64 // nil = global / superadmin context
|
||||
}
|
||||
|
||||
// SearchRequest specifies search parameters.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TenantIndexManager manages a pool of Xapian indexes, one per tenant.
|
||||
// Tenant 0 / nil maps to the global index used by superadmin and as a fallback.
|
||||
type TenantIndexManager struct {
|
||||
basePath string
|
||||
batchSize int
|
||||
backend string
|
||||
mu sync.RWMutex
|
||||
pool map[int64]Indexer // tenant_id -> Indexer
|
||||
global Indexer // tenant_id == 0 / nil (superadmin, fallback)
|
||||
}
|
||||
|
||||
// NewTenantIndexManager creates a new TenantIndexManager.
|
||||
// The global index lives at basePath directly; per-tenant indexes at basePath/tenant-<id>/.
|
||||
func NewTenantIndexManager(basePath string, batchSize int, backend string) (*TenantIndexManager, error) {
|
||||
// Ensure the base path directory exists.
|
||||
if err := os.MkdirAll(basePath, 0o750); err != nil {
|
||||
return nil, fmt.Errorf("tenant index manager: mkdir base: %w", err)
|
||||
}
|
||||
|
||||
global, err := New(basePath, batchSize, backend)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tenant index manager: open global: %w", err)
|
||||
}
|
||||
|
||||
return &TenantIndexManager{
|
||||
basePath: basePath,
|
||||
batchSize: batchSize,
|
||||
backend: backend,
|
||||
pool: make(map[int64]Indexer),
|
||||
global: global,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ForTenant returns the Indexer for the given tenant.
|
||||
// nil or pointer to 0 returns the global index.
|
||||
// Other tenant IDs lazily open a per-tenant index at basePath/tenant-<id>/.
|
||||
func (m *TenantIndexManager) ForTenant(tenantID *int64) Indexer {
|
||||
if tenantID == nil || *tenantID == 0 {
|
||||
return m.global
|
||||
}
|
||||
id := *tenantID
|
||||
|
||||
// Fast path: read lock.
|
||||
m.mu.RLock()
|
||||
if idx, ok := m.pool[id]; ok {
|
||||
m.mu.RUnlock()
|
||||
return idx
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Slow path: write lock, create if not exists.
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Double check after acquiring write lock.
|
||||
if idx, ok := m.pool[id]; ok {
|
||||
return idx
|
||||
}
|
||||
|
||||
dir := filepath.Join(m.basePath, fmt.Sprintf("tenant-%d", id))
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
// Return global as fallback on error.
|
||||
return m.global
|
||||
}
|
||||
|
||||
idx, err := New(dir, m.batchSize, m.backend)
|
||||
if err != nil {
|
||||
// Return global as fallback on error.
|
||||
return m.global
|
||||
}
|
||||
|
||||
m.pool[id] = idx
|
||||
return idx
|
||||
}
|
||||
|
||||
// Global returns the global (non-tenant) index.
|
||||
func (m *TenantIndexManager) Global() Indexer {
|
||||
return m.global
|
||||
}
|
||||
|
||||
// Close closes all indexes (global + per-tenant).
|
||||
func (m *TenantIndexManager) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var firstErr error
|
||||
for id, idx := range m.pool {
|
||||
if err := idx.Close(); err != nil && firstErr == nil {
|
||||
firstErr = fmt.Errorf("close tenant-%d index: %w", id, err)
|
||||
}
|
||||
delete(m.pool, id)
|
||||
}
|
||||
|
||||
if m.global != nil {
|
||||
if err := m.global.Close(); err != nil && firstErr == nil {
|
||||
firstErr = fmt.Errorf("close global index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return firstErr
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TenantIndexWorker processes MailDocument indexing requests asynchronously,
|
||||
// routing each document to the correct per-tenant Xapian index via TenantIndexManager.
|
||||
type TenantIndexWorker struct {
|
||||
mgr *TenantIndexManager
|
||||
queue chan MailDocument
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewTenantWorker creates a new TenantIndexWorker with the given queue capacity.
|
||||
func NewTenantWorker(mgr *TenantIndexManager, queueSize int, logger *slog.Logger) *TenantIndexWorker {
|
||||
if queueSize <= 0 {
|
||||
queueSize = 1000
|
||||
}
|
||||
return &TenantIndexWorker{
|
||||
mgr: mgr,
|
||||
queue: make(chan MailDocument, queueSize),
|
||||
done: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Submit enqueues a document for background indexing. If the queue is full,
|
||||
// the document is dropped and a warning is logged.
|
||||
func (w *TenantIndexWorker) Submit(doc MailDocument) {
|
||||
select {
|
||||
case w.queue <- doc:
|
||||
// queued
|
||||
default:
|
||||
w.logger.Warn("tenant index worker: queue full, dropping document", "id", doc.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Start launches the background goroutine that processes the queue.
|
||||
func (w *TenantIndexWorker) Start() {
|
||||
w.wg.Add(1)
|
||||
go func() {
|
||||
defer w.wg.Done()
|
||||
w.logger.Info("tenant index worker: started", "queue_size", cap(w.queue))
|
||||
for {
|
||||
select {
|
||||
case doc, ok := <-w.queue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.indexDoc(doc)
|
||||
case <-w.done:
|
||||
// Drain remaining items in the queue before exiting.
|
||||
for {
|
||||
select {
|
||||
case doc, ok := <-w.queue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.indexDoc(doc)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop signals the worker to drain remaining items and stop.
|
||||
func (w *TenantIndexWorker) Stop() {
|
||||
close(w.done)
|
||||
w.wg.Wait()
|
||||
w.logger.Info("tenant index worker: stopped")
|
||||
}
|
||||
|
||||
// QueueLen returns the current number of items waiting in the queue.
|
||||
func (w *TenantIndexWorker) QueueLen() int {
|
||||
return len(w.queue)
|
||||
}
|
||||
|
||||
func (w *TenantIndexWorker) indexDoc(doc MailDocument) {
|
||||
idx := w.mgr.ForTenant(doc.TenantID)
|
||||
if err := idx.IndexSync(doc); err != nil {
|
||||
w.logger.Error("tenant index worker: index failed", "id", doc.ID, "tenant_id", doc.TenantID, "err", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user