- OpenAI request rebuilder with tool validation and provider tunnel - Edge config runtime refresh for stream evidence gate - Filter observation contract and runtime with sink/correlation - Stream gate dispatcher, release sink, and vertical slice - Recovery coordinator for evidence tail - Parallel evaluation and commit boundary - E2E test script for OpenAI vLLM - Archive completed task groups to archive/2026/07
310 lines
10 KiB
Go
310 lines
10 KiB
Go
package streamgate
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
)
|
|
|
|
// ErrReleaserSerializerConflict is returned when an operation conflicts
|
|
// with the current serializer state (e.g., concurrent epoch calls).
|
|
var ErrReleaserSerializerConflict = errors.New("streamgate: releaser serializer conflict")
|
|
|
|
// ErrReleaserEpochConsumed is returned when an epoch has already been fully
|
|
// confirmed.
|
|
var ErrReleaserEpochConsumed = errors.New("streamgate: releaser epoch consumed")
|
|
|
|
// ErrReleaserNoEpoch is returned when the given epoch ID does not exist.
|
|
var ErrReleaserNoEpoch = errors.New("streamgate: releaser epoch not found")
|
|
|
|
// ErrReleaserReplaceNotPermitted is returned when replace is called but the
|
|
// boundary or tail state does not permit it.
|
|
var ErrReleaserReplaceNotPermitted = errors.New("streamgate: replace not permitted")
|
|
|
|
// AttemptCanceller is the host seam for cancelling a provider attempt. The
|
|
// stream releaser calls this for idle and overflow recovery; the actual
|
|
// implementation lives in the transport adapter.
|
|
type AttemptCanceller interface {
|
|
CancelAttempt(ctx context.Context, attemptID string) error
|
|
}
|
|
|
|
// StreamReleaser serializes EvidenceTail prepare/confirm with CommitBoundary
|
|
// release/terminal operations. It ensures zero/partial/full sink progress is
|
|
// confirmed correctly, exactly-once terminal semantics, and coordinated
|
|
// replacement.
|
|
type StreamReleaser struct {
|
|
mu sync.Mutex
|
|
busy bool
|
|
tail *EvidenceTail
|
|
boundary *CommitBoundary
|
|
canceller AttemptCanceller
|
|
completedEpochs map[uint64]bool
|
|
}
|
|
|
|
// NewStreamReleaser creates a new StreamReleaser. Both tail and boundary
|
|
// must be non-nil. The canceller may be nil if FailPending is not expected
|
|
// to be called.
|
|
func NewStreamReleaser(tail *EvidenceTail, boundary *CommitBoundary, canceller AttemptCanceller) (*StreamReleaser, error) {
|
|
if tail == nil {
|
|
return nil, errors.New("streamgate: stream releaser tail is required")
|
|
}
|
|
if boundary == nil {
|
|
return nil, errors.New("streamgate: stream releaser boundary is required")
|
|
}
|
|
return &StreamReleaser{
|
|
tail: tail,
|
|
boundary: boundary,
|
|
canceller: canceller,
|
|
completedEpochs: make(map[uint64]bool),
|
|
}, nil
|
|
}
|
|
|
|
func (r *StreamReleaser) isCompletedEpoch(epochID uint64) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.completedEpochs != nil && r.completedEpochs[epochID]
|
|
}
|
|
|
|
func (r *StreamReleaser) markCompletedEpoch(epochID uint64) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.completedEpochs == nil {
|
|
r.completedEpochs = make(map[uint64]bool)
|
|
}
|
|
r.completedEpochs[epochID] = true
|
|
}
|
|
|
|
func (r *StreamReleaser) clearCompletedEpochs() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.completedEpochs = make(map[uint64]bool)
|
|
}
|
|
|
|
func (r *StreamReleaser) tryAcquireSerializer() error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.busy {
|
|
return ErrReleaserSerializerConflict
|
|
}
|
|
r.busy = true
|
|
return nil
|
|
}
|
|
|
|
func (r *StreamReleaser) releaseSerializer() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.busy = false
|
|
}
|
|
|
|
func (r *StreamReleaser) releaseAndConfirm(ctx context.Context, attemptID string, epochID uint64) (ReleaseProgress, error) {
|
|
prepared, err := r.tail.PrepareRelease(epochID)
|
|
if err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
|
|
releaseEvents := prepared.ReleaseEvents()
|
|
var (
|
|
prog ReleaseProgress
|
|
relErr error
|
|
)
|
|
if len(releaseEvents) > 0 {
|
|
prog, relErr = r.boundary.ReleaseSafe(ctx, attemptID, releaseEvents)
|
|
}
|
|
|
|
confirmErr := r.tail.ConfirmRelease(
|
|
prepared.Token(),
|
|
ReleaseConfirmation{ReleasedEvents: prog.ReleasedEvents()},
|
|
)
|
|
|
|
jointErr := errors.Join(relErr, confirmErr)
|
|
if jointErr != nil {
|
|
return prog, jointErr
|
|
}
|
|
|
|
r.markCompletedEpoch(epochID)
|
|
return prog, nil
|
|
}
|
|
|
|
// ReleaseEpoch serializes a full release cycle: prepare from tail, release
|
|
// through boundary, and confirm sink progress back to tail.
|
|
func (r *StreamReleaser) ReleaseEpoch(ctx context.Context, attemptID string, epochID uint64) (ReleaseProgress, error) {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
return r.releaseAndConfirm(ctx, attemptID, epochID)
|
|
}
|
|
|
|
// ReleasePassThroughEvent releases an unbuffered event directly through the boundary under serializer lock.
|
|
func (r *StreamReleaser) ReleasePassThroughEvent(ctx context.Context, attemptID string, ev NormalizedEvent) (ReleaseProgress, error) {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
relEv, err := normalizedToReleaseEvent(ev)
|
|
if err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
|
|
return r.boundary.ReleaseSafe(ctx, attemptID, []ReleaseEvent{relEv})
|
|
}
|
|
|
|
// releaseReplacementEvents releases an ordered replacement payload in place of
|
|
// the pending content captured by the given epoch. The tail validates the
|
|
// payload against that epoch before any tail or sink mutation, so an unknown
|
|
// epoch, a foreign channel, or a non-releasable kind fails closed with the
|
|
// stream untouched. Prepare, the single ordered boundary release, and confirm
|
|
// all run inside one serializer reservation: the original payload can never
|
|
// reach the sink, and confirm reflects exactly the zero/partial/full progress
|
|
// the boundary reported back into the committed look-behind and cursor. Confirm
|
|
// runs even when the release failed, so a partially accepted replacement never
|
|
// leaves the tail ahead of or behind downstream reality.
|
|
func (r *StreamReleaser) releaseReplacementEvents(ctx context.Context, attemptID string, epochID uint64, events []NormalizedEvent) (ReleaseProgress, error) {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
prepared, err := r.tail.prepareReplacement(epochID, events)
|
|
if err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
|
|
prog, relErr := r.boundary.ReleaseSafe(ctx, attemptID, prepared.ReleaseEvents())
|
|
|
|
confirmErr := r.tail.confirmReplacement(
|
|
prepared.Token(),
|
|
ReleaseConfirmation{ReleasedEvents: prog.ReleasedEvents()},
|
|
)
|
|
if confirmErr == nil {
|
|
// The target epoch is settled: its original content is consumed and can
|
|
// never be released, so a later terminal must not re-release it.
|
|
r.markCompletedEpoch(epochID)
|
|
}
|
|
|
|
return prog, errors.Join(relErr, confirmErr)
|
|
}
|
|
|
|
// CommitTerminalWithoutEpoch commits a terminal result without requiring epoch tail prepare/confirm.
|
|
func (r *StreamReleaser) CommitTerminalWithoutEpoch(ctx context.Context, attemptID string, result TerminalResult) error {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
if !result.Success() {
|
|
_, err := r.failPendingLocked(ctx, attemptID, result)
|
|
return err
|
|
}
|
|
return r.boundary.CommitTerminal(ctx, attemptID, result)
|
|
}
|
|
|
|
// ReleaseTerminalEpoch commits a terminal result for a buffered epoch.
|
|
func (r *StreamReleaser) ReleaseTerminalEpoch(ctx context.Context, attemptID string, epochID uint64, result TerminalResult) (ReleaseProgress, error) {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
if !result.Success() {
|
|
return r.failPendingLocked(ctx, attemptID, result)
|
|
}
|
|
|
|
if r.isCompletedEpoch(epochID) {
|
|
return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, result)
|
|
}
|
|
|
|
prog, err := r.releaseAndConfirm(ctx, attemptID, epochID)
|
|
if err != nil {
|
|
return prog, err
|
|
}
|
|
|
|
return prog, r.boundary.CommitTerminal(ctx, attemptID, result)
|
|
}
|
|
|
|
// ReplaceUncommittedAttempt coordinates replacing the current attempt.
|
|
func (r *StreamReleaser) ReplaceUncommittedAttempt(ctx context.Context, oldAttemptID, newAttemptID string) error {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
if err := r.boundary.ReplaceUncommittedAttempt(oldAttemptID, newAttemptID); err != nil {
|
|
return err
|
|
}
|
|
|
|
r.tail.ResetForReplace()
|
|
r.clearCompletedEpochs()
|
|
return nil
|
|
}
|
|
|
|
// ContinueOpenAttempt coordinates transferring attempt identity when continuing an open stream.
|
|
func (r *StreamReleaser) ContinueOpenAttempt(ctx context.Context, oldAttemptID, newAttemptID string) error {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
return r.boundary.ContinueOpenAttempt(oldAttemptID, newAttemptID)
|
|
}
|
|
|
|
// FailPending discards all pending tail state, calls the canceller, and
|
|
// commits an error terminal through the boundary.
|
|
func (r *StreamReleaser) FailPending(ctx context.Context, attemptID string, result TerminalResult) (ReleaseProgress, error) {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return ReleaseProgress{}, err
|
|
}
|
|
defer r.releaseSerializer()
|
|
return r.failPendingLocked(ctx, attemptID, result)
|
|
}
|
|
|
|
// DiscardPendingAndCommitTerminal discards pending tail state without calling attempt canceller
|
|
// (when previous attempt was already aborted/closed) and commits terminal through the boundary.
|
|
func (r *StreamReleaser) DiscardPendingAndCommitTerminal(ctx context.Context, attemptID string, result TerminalResult) error {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return err
|
|
}
|
|
defer r.releaseSerializer()
|
|
|
|
r.tail.DiscardPendingForTerminal()
|
|
return r.boundary.CommitTerminal(ctx, attemptID, result)
|
|
}
|
|
|
|
func (r *StreamReleaser) failPendingLocked(ctx context.Context, attemptID string, result TerminalResult) (ReleaseProgress, error) {
|
|
r.tail.DiscardPendingForTerminal()
|
|
|
|
if r.canceller != nil {
|
|
if err := r.canceller.CancelAttempt(ctx, attemptID); err != nil {
|
|
if result.Error() {
|
|
causes := result.FailureCauses()
|
|
newCause, cerr := NewFailureCause("releaser", "attempt_cancel_failed", "", "", "")
|
|
if cerr == nil {
|
|
causes, _ = causes.Append(newCause)
|
|
}
|
|
newResult, rerr := NewErrorTerminalResult(
|
|
result.Channel(),
|
|
*result.ExternalDesc(),
|
|
causes,
|
|
result.OccurredAt(),
|
|
)
|
|
if rerr == nil {
|
|
return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, newResult)
|
|
}
|
|
}
|
|
return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, result)
|
|
}
|
|
}
|
|
|
|
return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, result)
|
|
}
|
|
|
|
// PrepareForReplace prepares a release for the given epoch without executing.
|
|
func (r *StreamReleaser) PrepareForReplace(ctx context.Context, attemptID string, epochID uint64) (PreparedRelease, error) {
|
|
if err := r.tryAcquireSerializer(); err != nil {
|
|
return PreparedRelease{}, err
|
|
}
|
|
defer r.releaseSerializer()
|
|
return r.tail.PrepareRelease(epochID)
|
|
}
|