feat: implement control history and mutation APIs
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
|
||||
"github.com/rvbox/rvbox/internal/domain"
|
||||
)
|
||||
|
||||
var ErrSignalDeliveryUnavailable = errors.New("signal delivery is unavailable for this command state")
|
||||
|
||||
type SignalInput struct {
|
||||
IssueUUID domain.UUID
|
||||
ClientID string
|
||||
RequestUUID domain.UUID
|
||||
Signal rvboxv1.SignalKind
|
||||
Hash [32]byte
|
||||
OccurredAt time.Time
|
||||
}
|
||||
|
||||
type SignalResult struct {
|
||||
CommandRevision uint64
|
||||
Duplicate bool
|
||||
Cancelled bool
|
||||
}
|
||||
|
||||
// SignalCommand applies the only locally-completable signal operation: a
|
||||
// queued command can be cancelled before dispatch. For dispatched/accepted/
|
||||
// running work the revisioned remote delivery path is intentionally not
|
||||
// claimed until a live session control queue is available.
|
||||
func (store *Store) SignalCommand(ctx context.Context, input SignalInput) (SignalResult, error) {
|
||||
if input.IssueUUID == (domain.UUID{}) || input.ClientID == "" || input.RequestUUID == (domain.UUID{}) || input.Signal < rvboxv1.SignalKind_SIGNAL_HUP || input.Signal > rvboxv1.SignalKind_SIGNAL_USR2 || input.Hash == [32]byte{} || input.OccurredAt.IsZero() {
|
||||
return SignalResult{}, errors.New("invalid signal request")
|
||||
}
|
||||
store.writeMu.Lock()
|
||||
defer store.writeMu.Unlock()
|
||||
database, err := store.openDatabase()
|
||||
if err != nil {
|
||||
return SignalResult{}, err
|
||||
}
|
||||
method := "signal_command"
|
||||
target := input.IssueUUID.String()
|
||||
if existing, found, lookupErr := lookupControlMutation(ctx, database, input.RequestUUID, method, target, input.Hash); lookupErr != nil {
|
||||
return SignalResult{}, lookupErr
|
||||
} else if found {
|
||||
if len(existing) != 8 {
|
||||
return SignalResult{}, ErrInvalidSegmentRecord
|
||||
}
|
||||
return SignalResult{CommandRevision: binary.BigEndian.Uint64(existing), Duplicate: true}, nil
|
||||
}
|
||||
tx, err := database.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return SignalResult{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var lifecycle uint32
|
||||
var revision uint64
|
||||
err = tx.QueryRowContext(ctx, `SELECT lifecycle, revision FROM commands WHERE issue_uuid = ? AND client_id = ?`, input.IssueUUID[:], input.ClientID).Scan(&lifecycle, &revision)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return SignalResult{}, ErrCommandNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return SignalResult{}, err
|
||||
}
|
||||
if lifecycle != uint32(rvboxv1.CommandLifecycle_COMMAND_QUEUED) {
|
||||
return SignalResult{}, ErrSignalDeliveryUnavailable
|
||||
}
|
||||
if revision == ^uint64(0) {
|
||||
return SignalResult{}, errors.New("command revision exhausted")
|
||||
}
|
||||
nextRevision := revision + 1
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE commands SET lifecycle = ?, terminal_time = ?, revision = ? WHERE issue_uuid = ? AND client_id = ? AND lifecycle = ? AND revision = ?`, rvboxv1.CommandLifecycle_COMMAND_CANCELLED, input.OccurredAt.UTC().UnixNano(), nextRevision, input.IssueUUID[:], input.ClientID, rvboxv1.CommandLifecycle_COMMAND_QUEUED, revision); err != nil {
|
||||
return SignalResult{}, err
|
||||
}
|
||||
result := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(result, nextRevision)
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO control_mutations (request_uuid, method, owner_kind, owner_id, target, immutable_sha256, assigned_revision, result, created_at) VALUES (?, ?, 'command', ?, ?, ?, ?, ?, ?)`, input.RequestUUID[:], method, input.IssueUUID.String(), target, input.Hash[:], nextRevision, result, input.OccurredAt.UTC().UnixNano())
|
||||
if err != nil {
|
||||
return SignalResult{}, err
|
||||
}
|
||||
// Keep a compact audit digest; command text/stdin is never copied to the
|
||||
// audit payload.
|
||||
auditPayload := []byte{byte(input.Signal)}
|
||||
auditHash := sha256.Sum256(auditPayload)
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO audit_events (occurred_at, source, action, outcome, compression, payload, raw_bytes, stored_bytes, sha256) VALUES (?, 'control', ?, 'success', 1, ?, ?, ?, ?)`, input.OccurredAt.UTC().UnixNano(), method, auditPayload, len(auditPayload), len(auditPayload), auditHash[:])
|
||||
if err != nil {
|
||||
return SignalResult{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return SignalResult{}, err
|
||||
}
|
||||
return SignalResult{CommandRevision: nextRevision, Cancelled: true}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user