독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
167 lines
3.8 KiB
Go
167 lines
3.8 KiB
Go
package agentconfig
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const DefaultRuntimeConfigPollInterval = 250 * time.Millisecond
|
|
|
|
// RuntimeConfigWatcher polls both source documents and atomically publishes
|
|
// only valid changed snapshots. Invalid edits are reported while the last
|
|
// valid snapshot remains current.
|
|
type RuntimeConfigWatcher struct {
|
|
repoGlobalPath string
|
|
userLocalPath string
|
|
pollInterval time.Duration
|
|
|
|
mu sync.RWMutex
|
|
current RuntimeSnapshot
|
|
updates chan RuntimeSnapshot
|
|
errors chan error
|
|
done chan struct{}
|
|
cancel context.CancelFunc
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
// WatchRuntimeConfig starts a watcher using the default poll interval.
|
|
func WatchRuntimeConfig(
|
|
ctx context.Context,
|
|
repoGlobalPath string,
|
|
userLocalPath string,
|
|
) (*RuntimeConfigWatcher, error) {
|
|
return NewRuntimeConfigWatcher(ctx, repoGlobalPath, userLocalPath, DefaultRuntimeConfigPollInterval)
|
|
}
|
|
|
|
// NewRuntimeConfigWatcher loads the initial snapshot before starting its
|
|
// polling goroutine. A non-positive interval is rejected.
|
|
func NewRuntimeConfigWatcher(
|
|
ctx context.Context,
|
|
repoGlobalPath string,
|
|
userLocalPath string,
|
|
pollInterval time.Duration,
|
|
) (*RuntimeConfigWatcher, error) {
|
|
if ctx == nil {
|
|
return nil, fmt.Errorf("agentconfig: watcher context is required")
|
|
}
|
|
if pollInterval <= 0 {
|
|
return nil, fmt.Errorf("agentconfig: watcher poll interval must be positive")
|
|
}
|
|
initial, err := LoadRuntimeConfig(repoGlobalPath, userLocalPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("agentconfig: watcher initial load: %w", err)
|
|
}
|
|
watchContext, cancel := context.WithCancel(ctx)
|
|
watcher := &RuntimeConfigWatcher{
|
|
repoGlobalPath: repoGlobalPath,
|
|
userLocalPath: userLocalPath,
|
|
pollInterval: pollInterval,
|
|
current: initial,
|
|
updates: make(chan RuntimeSnapshot, 1),
|
|
errors: make(chan error, 1),
|
|
done: make(chan struct{}),
|
|
cancel: cancel,
|
|
}
|
|
go watcher.run(watchContext)
|
|
return watcher, nil
|
|
}
|
|
|
|
// Snapshot returns the immutable snapshot pinned for a new invocation.
|
|
func (w *RuntimeConfigWatcher) Snapshot() RuntimeSnapshot {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
return w.current
|
|
}
|
|
|
|
// Updates reports valid revisions after the initial snapshot. The channel is
|
|
// bounded and coalesces unread values to the latest valid revision.
|
|
func (w *RuntimeConfigWatcher) Updates() <-chan RuntimeSnapshot {
|
|
return w.updates
|
|
}
|
|
|
|
// Errors reports invalid reloads without changing Snapshot.
|
|
func (w *RuntimeConfigWatcher) Errors() <-chan error {
|
|
return w.errors
|
|
}
|
|
|
|
// Done closes after the watcher stops.
|
|
func (w *RuntimeConfigWatcher) Done() <-chan struct{} {
|
|
return w.done
|
|
}
|
|
|
|
// Close stops the watcher and waits for its channels to close.
|
|
func (w *RuntimeConfigWatcher) Close() {
|
|
w.closeOnce.Do(w.cancel)
|
|
<-w.done
|
|
}
|
|
|
|
func (w *RuntimeConfigWatcher) run(ctx context.Context) {
|
|
defer close(w.done)
|
|
defer close(w.errors)
|
|
defer close(w.updates)
|
|
|
|
ticker := time.NewTicker(w.pollInterval)
|
|
defer ticker.Stop()
|
|
|
|
lastError := ""
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
next, err := LoadRuntimeConfig(w.repoGlobalPath, w.userLocalPath)
|
|
if err != nil {
|
|
message := err.Error()
|
|
if message != lastError {
|
|
w.publishError(err)
|
|
lastError = message
|
|
}
|
|
continue
|
|
}
|
|
lastError = ""
|
|
|
|
w.mu.Lock()
|
|
if next.Revision() == w.current.Revision() {
|
|
w.mu.Unlock()
|
|
continue
|
|
}
|
|
w.current = next
|
|
w.mu.Unlock()
|
|
w.publishUpdate(next)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *RuntimeConfigWatcher) publishUpdate(snapshot RuntimeSnapshot) {
|
|
select {
|
|
case w.updates <- snapshot:
|
|
return
|
|
default:
|
|
}
|
|
select {
|
|
case <-w.updates:
|
|
default:
|
|
}
|
|
select {
|
|
case w.updates <- snapshot:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (w *RuntimeConfigWatcher) publishError(err error) {
|
|
select {
|
|
case w.errors <- err:
|
|
return
|
|
default:
|
|
}
|
|
select {
|
|
case <-w.errors:
|
|
default:
|
|
}
|
|
select {
|
|
case w.errors <- err:
|
|
default:
|
|
}
|
|
}
|