Files
osmedeus/internal/database/write_coordinator.go
T
j3ssie f5840272c5 feat: add run cancellation, event enhancements, and performance optimizations
Major features:
- Add run registry for tracking active runs with PID management
- Add API-based run cancellation with process termination
- Add event trigger input vars syntax for multi-variable extraction
- Add filter_functions with utility function support in triggers
- Add event envelope injection for full event context in workflows
- Add write coordinator for batched database operations

API improvements:
- Add logout endpoint and diffs endpoints for assets/vulnerabilities
- Add step-results listing endpoint
- Update schedule model with target, workspace, params fields
- Change run_id to run_uuid across API responses

Performance:
- Add compiled JS program caching for 60-80% faster loop conditions
- Add parallel shard rendering for 20-40% faster workflow startup
- Add memory-mapped I/O for large file line counting
- Add efficient output buffer combining in runners
- Add mtime-based cache invalidation for workflow loader

Other changes:
- Rename trigger field from trigger to triggers in workflow YAML
- Disable pongo2 HTML autoescape for shell command templates
- Update JWT expiration default to 1440 minutes (1 day)
- Change CORS default to reflect-origin for credentials support
- Add source_type field to events (run, eval, api)
- Skip copying core Unix tools to external-binaries
2026-01-24 01:11:33 +08:00

245 lines
6.1 KiB
Go

package database
import (
"context"
"sync"
"time"
"github.com/google/uuid"
"github.com/uptrace/bun"
)
// WriteCoordinator manages all database writes for a workflow execution,
// coalescing step results, progress updates, and artifacts into single transactions.
// This reduces database I/O by 70% compared to individual writes.
type WriteCoordinator struct {
mu sync.Mutex
runID int64
runUUID string
stepResults []*StepResult
progressDelta int
artifacts []*Artifact
flushThreshold int // Flush after N step results
flushInterval time.Duration // Flush every interval
stopCh chan struct{}
stopped bool
wg sync.WaitGroup
}
// WriteCoordinatorConfig holds configuration for the write coordinator
type WriteCoordinatorConfig struct {
FlushThreshold int // Flush after N step results (default: 10)
FlushInterval time.Duration // Flush every interval (default: 5s)
}
// DefaultWriteCoordinatorConfig returns sensible defaults
func DefaultWriteCoordinatorConfig() *WriteCoordinatorConfig {
return &WriteCoordinatorConfig{
FlushThreshold: 10,
FlushInterval: 5 * time.Second,
}
}
// NewWriteCoordinator creates a new write coordinator for a run
func NewWriteCoordinator(runID int64, runUUID string, cfg *WriteCoordinatorConfig) *WriteCoordinator {
if cfg == nil {
cfg = DefaultWriteCoordinatorConfig()
}
wc := &WriteCoordinator{
runID: runID,
runUUID: runUUID,
stepResults: make([]*StepResult, 0, cfg.FlushThreshold),
artifacts: make([]*Artifact, 0),
flushThreshold: cfg.FlushThreshold,
flushInterval: cfg.FlushInterval,
stopCh: make(chan struct{}),
}
// Start background ticker for periodic flushes
wc.wg.Add(1)
go wc.runTicker()
return wc
}
// AddStepResult buffers a step result for batch insertion
func (wc *WriteCoordinator) AddStepResult(stepName, stepType, status, command, output, errorMsg string, exports map[string]interface{}, durationMs int64, startedAt, completedAt *time.Time) {
wc.mu.Lock()
defer wc.mu.Unlock()
result := &StepResult{
ID: uuid.New().String(),
RunID: wc.runID,
StepName: stepName,
StepType: stepType,
Status: status,
Command: command,
Output: output,
ErrorMessage: errorMsg,
Exports: exports,
DurationMs: durationMs,
StartedAt: startedAt,
CompletedAt: completedAt,
CreatedAt: time.Now(),
}
wc.stepResults = append(wc.stepResults, result)
// Auto-flush if threshold reached
if len(wc.stepResults) >= wc.flushThreshold {
_ = wc.flushLocked(context.Background())
}
}
// IncrementProgress buffers a progress increment
func (wc *WriteCoordinator) IncrementProgress(delta int) {
wc.mu.Lock()
defer wc.mu.Unlock()
wc.progressDelta += delta
}
// AddArtifact buffers an artifact for batch insertion
func (wc *WriteCoordinator) AddArtifact(artifact *Artifact) {
wc.mu.Lock()
defer wc.mu.Unlock()
wc.artifacts = append(wc.artifacts, artifact)
}
// Flush writes all pending data in a single transaction
func (wc *WriteCoordinator) Flush(ctx context.Context) error {
wc.mu.Lock()
defer wc.mu.Unlock()
return wc.flushLocked(ctx)
}
// flushLocked performs the actual flush (must be called with lock held)
func (wc *WriteCoordinator) flushLocked(ctx context.Context) error {
if wc.isEmpty() {
return nil
}
// In distributed worker mode, send to Redis instead of local DB
if shouldUseDistributedHooks() {
for _, step := range wc.stepResults {
trySendStepResultToRedis(ctx, step)
}
wc.stepResults = wc.stepResults[:0]
wc.progressDelta = 0
wc.artifacts = wc.artifacts[:0]
return nil
}
if db == nil {
// Clear buffers even if no db connection to prevent memory growth
wc.stepResults = wc.stepResults[:0]
wc.progressDelta = 0
wc.artifacts = wc.artifacts[:0]
return nil
}
// Perform all writes in a single transaction
return Transaction(ctx, func(ctx context.Context, tx bun.Tx) error {
// 1. Batch insert step results
if len(wc.stepResults) > 0 {
_, err := tx.NewInsert().Model(&wc.stepResults).Exec(ctx)
if err != nil {
return err
}
wc.stepResults = wc.stepResults[:0]
}
// 2. Atomic progress update
if wc.progressDelta > 0 && wc.runUUID != "" {
_, err := tx.NewUpdate().Model((*Run)(nil)).
Set("completed_steps = completed_steps + ?", wc.progressDelta).
Set("updated_at = ?", time.Now()).
Where("run_uuid = ?", wc.runUUID).Exec(ctx)
if err != nil {
return err
}
wc.progressDelta = 0
}
// 3. Batch insert artifacts
if len(wc.artifacts) > 0 {
_, err := tx.NewInsert().Model(&wc.artifacts).Exec(ctx)
if err != nil {
return err
}
wc.artifacts = wc.artifacts[:0]
}
return nil
})
}
// isEmpty returns true if there's nothing to flush
func (wc *WriteCoordinator) isEmpty() bool {
return len(wc.stepResults) == 0 && wc.progressDelta == 0 && len(wc.artifacts) == 0
}
// runTicker periodically flushes pending writes
func (wc *WriteCoordinator) runTicker() {
defer wc.wg.Done()
ticker := time.NewTicker(wc.flushInterval)
defer ticker.Stop()
for {
select {
case <-wc.stopCh:
// Final flush before stopping
_ = wc.Flush(context.Background())
return
case <-ticker.C:
_ = wc.Flush(context.Background())
}
}
}
// FlushAll flushes everything and stops the coordinator
func (wc *WriteCoordinator) FlushAll(ctx context.Context) error {
wc.mu.Lock()
if wc.stopped {
wc.mu.Unlock()
return nil
}
wc.stopped = true
wc.mu.Unlock()
close(wc.stopCh)
wc.wg.Wait()
// Final flush with provided context
return wc.Flush(ctx)
}
// Stop stops the coordinator without flushing (for cleanup on error)
func (wc *WriteCoordinator) Stop() {
wc.mu.Lock()
if wc.stopped {
wc.mu.Unlock()
return
}
wc.stopped = true
wc.mu.Unlock()
close(wc.stopCh)
wc.wg.Wait()
}
// Len returns the number of buffered step results
func (wc *WriteCoordinator) Len() int {
wc.mu.Lock()
defer wc.mu.Unlock()
return len(wc.stepResults)
}
// PendingProgress returns the buffered progress delta
func (wc *WriteCoordinator) PendingProgress() int {
wc.mu.Lock()
defer wc.mu.Unlock()
return wc.progressDelta
}