Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

234 lines
6.4 KiB
Go

package daemon
import (
"context"
"errors"
"log/slog"
"sync"
"time"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
type OperationExecutor interface {
ExecuteOperation(context.Context, string) error
}
type OperationScheduler struct {
ctx context.Context
executor OperationExecutor
mu sync.RWMutex
active map[string]struct{}
wait sync.WaitGroup
}
var _ market.OperationScheduler = (*OperationScheduler)(nil)
func NewOperationScheduler(ctx context.Context) *OperationScheduler {
if ctx == nil {
ctx = context.Background()
}
return &OperationScheduler{ctx: ctx, active: make(map[string]struct{})}
}
func (scheduler *OperationScheduler) Bind(executor OperationExecutor) error {
if executor == nil {
return errors.New("connector market operation executor is required")
}
scheduler.mu.Lock()
defer scheduler.mu.Unlock()
if scheduler.executor != nil {
return errors.New("connector market operation executor is already bound")
}
scheduler.executor = executor
return nil
}
func (scheduler *OperationScheduler) Schedule(_ context.Context, operationID string) error {
scheduler.mu.Lock()
if scheduler.executor == nil {
scheduler.mu.Unlock()
return errors.New("connector market operation executor is not bound")
}
if _, running := scheduler.active[operationID]; running {
scheduler.mu.Unlock()
return nil
}
scheduler.active[operationID] = struct{}{}
executor := scheduler.executor
scheduler.wait.Add(1)
scheduler.mu.Unlock()
go func() {
defer scheduler.wait.Done()
defer func() {
scheduler.mu.Lock()
delete(scheduler.active, operationID)
scheduler.mu.Unlock()
}()
if err := executor.ExecuteOperation(scheduler.ctx, operationID); err != nil {
slog.Warn("connector market operation failed", "operationId", operationID, "error", err)
}
}()
return nil
}
func (scheduler *OperationScheduler) Wait() {
scheduler.wait.Wait()
}
type ChangedEventPublisher interface {
PublishConnectorMarketChanged(context.Context, market.ChangedEvent) error
}
const (
DefaultTerminalOperationRetention = 24 * time.Hour
DefaultPublishedEventRetention = time.Hour
DefaultLifecycleCleanupInterval = time.Hour
DefaultLifecycleCleanupBatchSize = 500
)
type LifecycleCleanupPolicy struct {
TerminalOperationRetention time.Duration
PublishedEventRetention time.Duration
Interval time.Duration
BatchSize int
}
type LifecycleCleanupWorker struct {
Store market.LifecycleCleanupStore
Now func() time.Time
Policy LifecycleCleanupPolicy
}
func (worker LifecycleCleanupWorker) Run(ctx context.Context) {
policy := normalizedLifecycleCleanupPolicy(worker.Policy)
worker.Policy = policy
if err := worker.Cleanup(ctx); err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector market lifecycle cleanup failed", "error", err)
}
ticker := time.NewTicker(policy.Interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := worker.Cleanup(ctx); err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector market lifecycle cleanup failed", "error", err)
}
}
}
}
func (worker LifecycleCleanupWorker) Cleanup(ctx context.Context) error {
if worker.Store == nil {
return errors.New("connector market lifecycle cleanup store is required")
}
policy := normalizedLifecycleCleanupPolicy(worker.Policy)
now := time.Now().UTC()
if worker.Now != nil {
now = worker.Now().UTC()
}
request := market.LifecycleCleanupRequest{
TerminalOperationsUpdatedThrough: now.Add(-policy.TerminalOperationRetention),
PublishedEventsPublishedThrough: now.Add(-policy.PublishedEventRetention),
BatchSize: policy.BatchSize,
}
var total market.LifecycleCleanupResult
for {
result, err := worker.Store.CleanupLifecycle(ctx, request)
if err != nil {
return err
}
total.TerminalOperationsDeleted += result.TerminalOperationsDeleted
total.PublishedEventsDeleted += result.PublishedEventsDeleted
if result.TerminalOperationsDeleted < int64(policy.BatchSize) &&
result.PublishedEventsDeleted < int64(policy.BatchSize) {
break
}
if err := ctx.Err(); err != nil {
return err
}
}
if total.TerminalOperationsDeleted > 0 || total.PublishedEventsDeleted > 0 {
slog.Info("connector market lifecycle cleanup completed",
"terminalOperationsDeleted", total.TerminalOperationsDeleted,
"publishedEventsDeleted", total.PublishedEventsDeleted,
"terminalOperationRetention", policy.TerminalOperationRetention,
"publishedEventRetention", policy.PublishedEventRetention)
}
return nil
}
func normalizedLifecycleCleanupPolicy(policy LifecycleCleanupPolicy) LifecycleCleanupPolicy {
if policy.TerminalOperationRetention <= 0 {
policy.TerminalOperationRetention = DefaultTerminalOperationRetention
}
if policy.PublishedEventRetention <= 0 {
policy.PublishedEventRetention = DefaultPublishedEventRetention
}
if policy.Interval <= 0 {
policy.Interval = DefaultLifecycleCleanupInterval
}
if policy.BatchSize <= 0 || policy.BatchSize > DefaultLifecycleCleanupBatchSize {
policy.BatchSize = DefaultLifecycleCleanupBatchSize
}
return policy
}
type OutboxDispatcher struct {
Outbox market.ChangedEventOutbox
Publisher ChangedEventPublisher
Now func() time.Time
Interval time.Duration
}
func (dispatcher OutboxDispatcher) Run(ctx context.Context) {
interval := dispatcher.Interval
if interval <= 0 {
interval = 250 * time.Millisecond
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
if err := dispatcher.Flush(ctx); err != nil && !errors.Is(err, context.Canceled) {
slog.Warn("connector market outbox delivery failed", "error", err)
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
func (dispatcher OutboxDispatcher) Flush(ctx context.Context) error {
if dispatcher.Outbox == nil || dispatcher.Publisher == nil {
return errors.New("connector market outbox dependencies are required")
}
now := dispatcher.Now
if now == nil {
now = time.Now
}
for {
entries, err := dispatcher.Outbox.PendingChangedEvents(ctx, 100)
if err != nil {
return err
}
if len(entries) == 0 {
return nil
}
for _, entry := range entries {
event := entry.Event
event.Cursor = entry.Sequence
if err := dispatcher.Publisher.PublishConnectorMarketChanged(ctx, event); err != nil {
return err
}
if err := dispatcher.Outbox.MarkChangedEventPublished(ctx, entry.Sequence, now().UTC()); err != nil {
return err
}
}
}
}