feat: complete Windows client control and recovery paths
This commit is contained in:
@@ -267,14 +267,25 @@ func (store *Store) AssignSendWindow(ctx context.Context, issueUUID domain.UUID,
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var next uint64
|
||||
err = tx.QueryRowContext(ctx, `SELECT next_event_seq FROM commands WHERE issue_uuid = ?`, issueUUID[:]).Scan(&next)
|
||||
var next, lastAck, assignedBytes uint64
|
||||
err = tx.QueryRowContext(ctx, `SELECT next_event_seq, last_server_ack,
|
||||
COALESCE((SELECT sum(length(payload)) FROM events
|
||||
WHERE issue_uuid = commands.issue_uuid AND event_seq IS NOT NULL AND event_seq > commands.last_server_ack), 0)
|
||||
FROM commands WHERE issue_uuid = ?`, issueUUID[:]).Scan(&next, &lastAck, &assignedBytes)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrUnknownCommand
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Assigned rows remain pinned until cumulative acknowledgement. Never
|
||||
// assign another row while that durable send window is full; otherwise a
|
||||
// burst of local events could grow the pinned set without bound even though
|
||||
// callers pass a per-command byte limit on every invocation.
|
||||
if assignedBytes >= maximumStoredBytes {
|
||||
return eventsBySequence(ctx, tx, issueUUID, 0)
|
||||
}
|
||||
maximumStoredBytes -= assignedBytes
|
||||
rows, err := tx.QueryContext(ctx, `SELECT local_ordinal, length(payload) FROM events WHERE issue_uuid = ? AND event_seq IS NULL ORDER BY local_ordinal LIMIT ?`, issueUUID[:], maximumEvents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -475,6 +486,14 @@ func (store *Store) RecoverLaunchUncertainty(ctx context.Context, now time.Time)
|
||||
// crash between a terminal marker and its public event from creating a state
|
||||
// that can be replayed as a second execution.
|
||||
func (store *Store) AppendLifecycle(ctx context.Context, issueUUID domain.UUID, phase uint32, revision uint64, detail string, observedAt time.Time) (Event, error) {
|
||||
return store.AppendLifecycleWithIdentity(ctx, issueUUID, phase, revision, detail, observedAt, nil)
|
||||
}
|
||||
|
||||
// AppendLifecycleWithIdentity is the lifecycle boundary used by the client
|
||||
// runtime when the Windows supervisor has captured an immutable selection
|
||||
// record. Keeping the legacy wrapper above preserves the small store API for
|
||||
// recovery and tests that have no platform identity to attach.
|
||||
func (store *Store) AppendLifecycleWithIdentity(ctx context.Context, issueUUID domain.UUID, phase uint32, revision uint64, detail string, observedAt time.Time, identity *rvboxv1.WindowsExecutionIdentity) (Event, error) {
|
||||
if !validUUID(issueUUID) || phase == 0 || phase > 11 || observedAt.IsZero() || revision == 0 {
|
||||
return Event{}, errors.New("invalid lifecycle event")
|
||||
}
|
||||
@@ -503,7 +522,7 @@ func (store *Store) AppendLifecycle(ctx context.Context, issueUUID domain.UUID,
|
||||
if !domain.CanTransition(rvboxv1.CommandLifecycle(current), rvboxv1.CommandLifecycle(phase)) {
|
||||
return Event{}, fmt.Errorf("invalid lifecycle transition %s -> %s", rvboxv1.CommandLifecycle(current), rvboxv1.CommandLifecycle(phase))
|
||||
}
|
||||
lifecycle := &rvboxv1.LifecycleChange{Lifecycle: rvboxv1.CommandLifecycle(phase), CommandRevision: revision, Detail: detail}
|
||||
lifecycle := &rvboxv1.LifecycleChange{Lifecycle: rvboxv1.CommandLifecycle(phase), CommandRevision: revision, Detail: detail, WindowsExecutionIdentity: identity}
|
||||
payload, err := proto.MarshalOptions{Deterministic: true}.Marshal(&rvboxv1.CommandEvent{IssueUuid: issueUUID.String(), ObservedAt: timestamppb.New(observedAt), Payload: &rvboxv1.CommandEvent_Lifecycle{Lifecycle: lifecycle}})
|
||||
if err != nil {
|
||||
return Event{}, err
|
||||
|
||||
Reference in New Issue
Block a user