feat: implement control history and mutation APIs

This commit is contained in:
2026-09-06 10:18:37 +00:00
parent ef43e9592c
commit 8df56263fb
11 changed files with 1515 additions and 3 deletions
+156
View File
@@ -0,0 +1,156 @@
package store
import (
"bytes"
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/klauspost/compress/zstd"
"github.com/rvbox/rvbox/internal/domain"
)
// EventView is a validated persisted command event. Payload is the stored
// event bytes (normally deterministic protobuf bytes); callers that need the
// uncompressed form should use DecodeEventPayload.
type EventView struct {
IssueUUID domain.UUID
EventSeq uint64
ObservedUnixNano int64
ReceiptUnixNano int64
EventType uint16
Stream uint16
Compression uint16
RawLength uint64
Payload []byte
ImmutableSHA256 [32]byte
}
// ReadCommandEvents returns at most limit events after afterEventSeq in strict
// sequence order. Segment references are revalidated at read time; a malformed
// or replaced segment is an error rather than silently returning partial data.
func (store *Store) ReadCommandEvents(ctx context.Context, issue domain.UUID, afterEventSeq uint64, limit uint32) ([]EventView, error) {
if issue == (domain.UUID{}) || limit == 0 || limit > 1000 {
return nil, errors.New("invalid command event read")
}
database, err := store.openDatabase()
if err != nil {
return nil, err
}
rows, err := database.QueryContext(ctx, `SELECT
ce.event_seq, ce.observed_at, ce.server_receipt_time, ce.event_type,
ce.compression, ce.raw_bytes, ce.payload, ce.segment_ordinal,
ce.segment_record_offset, ce.segment_record_length, os.path,
ce.immutable_sha256
FROM command_events ce LEFT JOIN output_segments os
ON os.issue_uuid = ce.issue_uuid AND os.ordinal = ce.segment_ordinal
WHERE ce.issue_uuid = ? AND ce.event_seq > ? ORDER BY ce.event_seq LIMIT ?`, issue[:], afterEventSeq, limit)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]EventView, 0, limit)
for rows.Next() {
var event EventView
var inline []byte
var ordinal, offset, length sql.NullInt64
var path sql.NullString
var digest []byte
if err := rows.Scan(&event.EventSeq, &event.ObservedUnixNano, &event.ReceiptUnixNano, &event.EventType, &event.Compression, &event.RawLength, &inline, &ordinal, &offset, &length, &path, &digest); err != nil {
return nil, err
}
if len(digest) != 32 || event.EventSeq == 0 || event.EventType == 0 {
return nil, ErrInvalidSegmentRecord
}
copy(event.IssueUUID[:], issue[:])
copy(event.ImmutableSHA256[:], digest)
switch {
case inline != nil && !ordinal.Valid && !offset.Valid && !length.Valid && !path.Valid:
event.Payload = append([]byte(nil), inline...)
case inline == nil && ordinal.Valid && offset.Valid && length.Valid && path.Valid:
if ordinal.Int64 < 0 || offset.Int64 < 0 || length.Int64 <= 0 || ordinal.Int64 > int64(^uint32(0)) || uint64(length.Int64) > uint64(^uint(0)>>1) {
return nil, ErrInvalidSegmentRecord
}
payload, stream, readErr := store.readEventSegment(path.String, issue, event.EventSeq, uint32(ordinal.Int64), uint64(offset.Int64), uint64(length.Int64))
if readErr != nil {
return nil, readErr
}
event.Payload = payload
event.Stream = stream
default:
return nil, ErrInvalidSegmentRecord
}
if uint64(len(event.Payload)) > DefaultSegmentLimit || event.Compression < 1 || event.Compression > 2 || event.RawLength > DefaultSegmentLimit {
return nil, ErrInvalidSegmentRecord
}
result = append(result, event)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
func (store *Store) readEventSegment(name string, issue domain.UUID, sequence uint64, ordinal uint32, offset, length uint64) ([]byte, uint16, error) {
if filepath.Base(name) != name {
return nil, 0, ErrUnsafeSegmentReference
}
owner, parsedOrdinal, err := parseSegmentName(name)
if err != nil || owner != [16]byte(issue) || parsedOrdinal != ordinal {
return nil, 0, ErrUnsafeSegmentReference
}
path := filepath.Join(store.segmentDirectory(), name)
info, err := os.Lstat(path)
if err != nil {
return nil, 0, err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm()&0o077 != 0 {
return nil, 0, ErrUnsafeSegmentReference
}
if length > uint64(^uint(0)>>1) || offset > uint64(^uint(0)>>1) || offset+length < offset || offset+length > uint64(info.Size()) {
return nil, 0, ErrCommittedRangeMissing
}
file, err := os.Open(path)
if err != nil {
return nil, 0, err
}
defer file.Close()
reader := io.NewSectionReader(file, int64(offset), int64(length))
record, encodedLength, err := DecodeSegmentRecord(reader, DefaultSegmentLimits())
if err != nil {
return nil, 0, fmt.Errorf("decode command event segment: %w", err)
}
if encodedLength != length || record.OwnerUUID != [16]byte(issue) || record.Sequence != sequence || record.Kind != PayloadKindCommandEvent {
return nil, 0, ErrInvalidSegmentRecord
}
return append([]byte(nil), record.Payload...), record.Stream, nil
}
// DecodeEventPayload validates and boundedly decompresses a stored event
// payload. It is intentionally generic so output/follow handlers share the
// same decompression ceiling.
func DecodeEventPayload(event EventView) ([]byte, error) {
if event.Compression == 1 {
if event.RawLength != uint64(len(event.Payload)) {
return nil, ErrInvalidSegmentRecord
}
return append([]byte(nil), event.Payload...), nil
}
if event.Compression != 2 || event.RawLength > DefaultSegmentLimit {
return nil, ErrInvalidSegmentRecord
}
decoder, err := zstd.NewReader(bytes.NewReader(event.Payload), zstd.WithDecoderConcurrency(1), zstd.WithDecoderMaxMemory(DefaultSegmentLimit+1))
if err != nil {
return nil, ErrInvalidSegmentRecord
}
defer decoder.Close()
decoded, err := io.ReadAll(io.LimitReader(decoder, int64(event.RawLength)+1))
if err != nil || uint64(len(decoded)) != event.RawLength {
return nil, ErrInvalidSegmentRecord
}
return decoded, nil
}
+52
View File
@@ -0,0 +1,52 @@
package store
import (
"context"
"crypto/sha256"
"path/filepath"
"testing"
"time"
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
"github.com/rvbox/rvbox/internal/domain"
)
func TestReadCommandEventsValidatesSegmentAndPreservesOrder_HP_EVENT_04(t *testing.T) {
ctx := context.Background()
opened, err := Open(ctx, Options{DataDir: filepath.Join(t.TempDir(), "state"), BusyTimeout: time.Second})
if err != nil {
t.Fatal(err)
}
defer opened.Close()
if _, err := opened.RegisterClientSession(ctx, ClientRegistration{ClientID: "win-client", Platform: 3, Architecture: "amd64", DaemonVersion: "test", DaemonCWD: `C:\`, SupportedShells: []byte{1}, ClientInstanceID: [16]byte{1}, SessionID: [16]byte{2}, ConnectedAt: time.Now()}); err != nil {
t.Fatal(err)
}
issue, _ := domain.ParseUUIDv7("019c46f1-1d02-7000-8000-0000000000b1")
now := time.Now().UTC()
if _, err := opened.QueueCommand(ctx, QueueCommandInput{IssueUUID: issue, ClientID: "win-client", IssueTime: now, ReceiptTime: now, ImmutableSHA256: sha256.Sum256([]byte("request")), ExecutionSpec: []byte("spec")}); err != nil {
t.Fatal(err)
}
if _, err := opened.ClaimNextDispatch(ctx, "win-client", 1, now); err != nil {
t.Fatal(err)
}
if _, err := opened.RecordCommandAcceptance(ctx, issue, "win-client", 1, 1, true, now); err != nil {
t.Fatal(err)
}
for sequence, lifecycle := range []rvboxv1.CommandLifecycle{rvboxv1.CommandLifecycle_COMMAND_RUNNING, rvboxv1.CommandLifecycle_COMMAND_SUCCEEDED} {
event := []byte{byte(sequence + 1), 'e', 'v', 't'}
if _, err := opened.AppendCommandEvent(ctx, EventAppend{IssueUUID: [16]byte(issue), ClientID: "win-client", SessionGeneration: 1, EventSeq: uint64(sequence + 1), ObservedUnixNano: now.UnixNano(), ReceiptUnixNano: now.Add(time.Duration(sequence) * time.Millisecond).UnixNano(), EventType: 4, Compression: 1, RawLength: uint64(len(event)), Payload: event, ImmutableSHA256: sha256.Sum256(event), Lifecycle: &lifecycle, LifecycleRevision: 1}); err != nil {
t.Fatal(err)
}
}
events, err := opened.ReadCommandEvents(ctx, issue, 0, 10)
if err != nil || len(events) != 2 || events[0].EventSeq != 1 || events[1].EventSeq != 2 || string(events[1].Payload) != "\x02evt" {
t.Fatalf("read events = %#v, %v", events, err)
}
decoded, err := DecodeEventPayload(events[0])
if err != nil || string(decoded) != "\x01evt" {
t.Fatalf("decoded event payload = %q, %v", decoded, err)
}
if _, err := opened.ReadCommandEvents(ctx, issue, 0, 0); err == nil {
t.Fatal("zero event limit accepted")
}
}
+118
View File
@@ -0,0 +1,118 @@
package store
import (
"context"
"database/sql"
"errors"
"time"
)
type IncidentView struct {
IncidentUUID [16]byte
DetectedAt time.Time
ResolvedAt *time.Time
State IncidentState
Kind IncidentKind
Scope IncidentScope
ClientID string
IssueUUID *[16]byte
Summary string
DataLoss bool
AutomaticallyRepairable bool
}
type IncidentPage struct {
IncludeResolved bool
Limit uint32
SnapshotBoundary int64
AfterDetectedAt int64
AfterUUID [16]byte
HasAfter bool
}
func (store *Store) ListIncidentViews(ctx context.Context, page IncidentPage) ([]IncidentView, bool, error) {
if page.Limit == 0 || page.Limit > 1000 || page.SnapshotBoundary <= 0 {
return nil, false, errors.New("invalid incident page")
}
database, err := store.openDatabase()
if err != nil {
return nil, false, err
}
query := `SELECT incident_uuid, detected_at, resolved_at, state, kind, scope, client_id, issue_uuid, summary, data_loss, automatically_repairable FROM storage_incidents WHERE detected_at <= ?`
args := []any{page.SnapshotBoundary}
if !page.IncludeResolved {
query += ` AND state = 1`
}
if page.HasAfter {
query += ` AND (detected_at < ? OR (detected_at = ? AND incident_uuid < ?))`
args = append(args, page.AfterDetectedAt, page.AfterDetectedAt, page.AfterUUID[:])
}
query += ` ORDER BY detected_at DESC, incident_uuid DESC LIMIT ?`
args = append(args, page.Limit+1)
rows, err := database.QueryContext(ctx, query, args...)
if err != nil {
return nil, false, err
}
defer rows.Close()
result := make([]IncidentView, 0, page.Limit)
for rows.Next() {
view, scanErr := scanIncidentView(rows)
if scanErr != nil {
return nil, false, scanErr
}
if uint32(len(result)) < page.Limit {
result = append(result, view)
} else {
return result, true, rows.Err()
}
}
if err := rows.Err(); err != nil {
return nil, false, err
}
return result, false, nil
}
func (store *Store) GetIncidentView(ctx context.Context, incidentUUID [16]byte) (IncidentView, error) {
if incidentUUID == [16]byte{} {
return IncidentView{}, ErrIncidentNotFound
}
database, err := store.openDatabase()
if err != nil {
return IncidentView{}, err
}
view, err := scanIncidentView(database.QueryRowContext(ctx, `SELECT incident_uuid, detected_at, resolved_at, state, kind, scope, client_id, issue_uuid, summary, data_loss, automatically_repairable FROM storage_incidents WHERE incident_uuid = ?`, incidentUUID[:]))
if errors.Is(err, sql.ErrNoRows) {
return IncidentView{}, ErrIncidentNotFound
}
return view, err
}
func scanIncidentView(scanner interface{ Scan(...any) error }) (IncidentView, error) {
var view IncidentView
var encoded, issue []byte
var detected int64
var resolved sql.NullInt64
var state, kind uint32
var client sql.NullString
var dataLoss, repairable int
if err := scanner.Scan(&encoded, &detected, &resolved, &state, &kind, &view.Scope, &client, &issue, &view.Summary, &dataLoss, &repairable); err != nil {
return view, err
}
if len(encoded) != 16 || (issue != nil && len(issue) != 16) || state < uint32(IncidentOpen) || state > uint32(IncidentAcknowledged) || kind < uint32(IncidentUncommittedTail) || kind > uint32(IncidentDiskExhaustion) {
return view, ErrInvalidSegmentRecord
}
copy(view.IncidentUUID[:], encoded)
view.DetectedAt = time.Unix(0, detected).UTC()
view.ResolvedAt = nullableTime(resolved)
view.State, view.Kind = IncidentState(state), IncidentKind(kind)
if client.Valid {
view.ClientID = client.String
}
view.DataLoss, view.AutomaticallyRepairable = dataLoss == 1, repairable == 1
if issue != nil {
value := [16]byte{}
copy(value[:], issue)
view.IssueUUID = &value
}
return view, nil
}
+98
View File
@@ -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
}
+163
View File
@@ -0,0 +1,163 @@
package store
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/binary"
"errors"
"math"
"time"
"github.com/rvbox/rvbox/internal/domain"
)
var (
ErrCommandTerminal = errors.New("command is already terminal")
ErrStdinConflict = errors.New("stdin request conflicts with an earlier request")
)
type StdinWriteInput struct {
IssueUUID domain.UUID
ClientID string
RequestUUID domain.UUID
Data []byte
AppendNewline bool
Close bool
ImmutableHash [32]byte
OccurredAt time.Time
}
type StdinWriteResult struct {
WriteSeq uint64
Duplicate bool
}
// AppendStdin durably records one ordered stdin intent. It does not claim
// delivery to a process; an agent acknowledgement is a later protocol event.
// Reusing RequestUUID with the same immutable hash returns the original write
// sequence without adding a second row.
func (store *Store) AppendStdin(ctx context.Context, input StdinWriteInput) (StdinWriteResult, error) {
return store.appendStdin(ctx, input)
}
func (store *Store) CloseStdin(ctx context.Context, input StdinWriteInput) (StdinWriteResult, error) {
input.Close = true
return store.appendStdin(ctx, input)
}
func (store *Store) appendStdin(ctx context.Context, input StdinWriteInput) (StdinWriteResult, error) {
if input.IssueUUID == (domain.UUID{}) || input.ClientID == "" || input.RequestUUID == (domain.UUID{}) || input.OccurredAt.IsZero() || input.ImmutableHash == [32]byte{} || (!input.Close && len(input.Data) == 0) || (input.Close && len(input.Data) != 0) || len(input.Data) > 64<<10 {
return StdinWriteResult{}, errors.New("invalid stdin write")
}
stored, err := compressCommandSpec(input.Data)
if err != nil {
return StdinWriteResult{}, err
}
charge, err := EstimateCharge(ChargeInput{EncodedBytes: uint64(len(stored)), SQLiteRows: 1, IndexEntries: 1})
if err != nil {
return StdinWriteResult{}, err
}
method := "append_stdin"
if input.Close {
method = "close_stdin"
}
target := input.IssueUUID.String()
store.writeMu.Lock()
defer store.writeMu.Unlock()
database, err := store.openDatabase()
if err != nil {
return StdinWriteResult{}, err
}
if existing, found, lookupErr := lookupControlMutation(ctx, database, input.RequestUUID, method, target, input.ImmutableHash); lookupErr != nil {
return StdinWriteResult{}, lookupErr
} else if found {
if len(existing) != 8 {
return StdinWriteResult{}, ErrInvalidSegmentRecord
}
return StdinWriteResult{WriteSeq: binary.BigEndian.Uint64(existing), Duplicate: true}, nil
}
var lifecycle uint32
var commandCharged, closeout, clientCharged, serverCharged uint64
err = database.QueryRowContext(ctx, `SELECT commands.lifecycle, commands.charged_bytes, commands.closeout_remaining_bytes,
clients.charged_bytes, storage_counters.command_charged_bytes
FROM commands JOIN clients ON clients.client_id = commands.client_id
JOIN storage_counters ON storage_counters.singleton = 1
WHERE commands.issue_uuid = ? AND commands.client_id = ?`, input.IssueUUID[:], input.ClientID).Scan(&lifecycle, &commandCharged, &closeout, &clientCharged, &serverCharged)
if errors.Is(err, sql.ErrNoRows) {
return StdinWriteResult{}, ErrCommandNotFound
}
if err != nil {
return StdinWriteResult{}, err
}
if lifecycle < 1 || lifecycle > 4 {
return StdinWriteResult{}, ErrCommandTerminal
}
freeBytes, err := store.freeSpaceProbe.AvailableBytes(store.dataDir)
if err != nil {
return StdinWriteResult{}, err
}
reservation, err := CheckReservation(store.quotaLimits, ReservationState{CommandTotalCharged: commandCharged, CloseoutRemaining: closeout, ClientTotalCharged: clientCharged, ServerTotalCharged: serverCharged, FilesystemFreeBytes: freeBytes}, ReservationRequest{ChargedBytes: charge, PhysicalBytes: uint64(len(stored))})
if err != nil {
return StdinWriteResult{}, err
}
var writeSeq uint64
if err := database.QueryRowContext(ctx, `SELECT COALESCE(MAX(write_seq), 0) + 1 FROM stdin_writes WHERE issue_uuid = ?`, input.IssueUUID[:]).Scan(&writeSeq); err != nil {
return StdinWriteResult{}, err
}
if writeSeq == 0 || writeSeq > math.MaxInt64 {
return StdinWriteResult{}, errors.New("stdin write sequence exhausted")
}
digest := sha256.Sum256(input.Data)
appendNewline := 0
if input.AppendNewline {
appendNewline = 1
}
closeIntent := 0
if input.Close {
closeIntent = 1
}
tx, err := database.BeginTx(ctx, nil)
if err != nil {
return StdinWriteResult{}, err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, `INSERT INTO stdin_writes (issue_uuid, write_seq, payload, raw_bytes, stored_bytes, compression, sha256, append_newline, close_intent, acknowledged) VALUES (?, ?, ?, ?, ?, 2, ?, ?, ?, 0)`, input.IssueUUID[:], writeSeq, stored, len(input.Data), len(stored), digest[:], appendNewline, closeIntent)
if err == nil {
_, err = tx.ExecContext(ctx, `UPDATE commands SET charged_bytes = ?, closeout_remaining_bytes = ? WHERE issue_uuid = ? AND charged_bytes = ?`, reservation.CommandTotalCharged, reservation.CloseoutRemaining, input.IssueUUID[:], commandCharged)
}
if err == nil {
_, err = tx.ExecContext(ctx, `UPDATE clients SET charged_bytes = ? WHERE client_id = ? AND charged_bytes = ?`, reservation.ClientTotalCharged, input.ClientID, clientCharged)
}
if err == nil {
_, err = tx.ExecContext(ctx, `UPDATE storage_counters SET command_charged_bytes = ? WHERE singleton = 1 AND command_charged_bytes = ?`, reservation.ServerTotalCharged, serverCharged)
}
result := make([]byte, 8)
if err == nil {
binary.BigEndian.PutUint64(result, writeSeq)
_, err = tx.ExecContext(ctx, `INSERT INTO control_mutations (request_uuid, method, owner_kind, owner_id, target, immutable_sha256, assigned_write_seq, result, created_at) VALUES (?, ?, 'command', ?, ?, ?, ?, ?, ?)`, input.RequestUUID[:], method, input.IssueUUID.String(), target, input.ImmutableHash[:], writeSeq, result, input.OccurredAt.UTC().UnixNano())
}
if err != nil {
return StdinWriteResult{}, err
}
if err := tx.Commit(); err != nil {
return StdinWriteResult{}, err
}
return StdinWriteResult{WriteSeq: writeSeq}, nil
}
func lookupControlMutation(ctx context.Context, database *sql.DB, requestUUID domain.UUID, method, target string, hash [32]byte) ([]byte, bool, error) {
var storedMethod, storedTarget string
var storedHash, result []byte
err := database.QueryRowContext(ctx, `SELECT method, target, immutable_sha256, result FROM control_mutations WHERE request_uuid = ?`, requestUUID[:]).Scan(&storedMethod, &storedTarget, &storedHash, &result)
if errors.Is(err, sql.ErrNoRows) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
if storedMethod != method || storedTarget != target || len(storedHash) != 32 || string(storedHash) != string(hash[:]) {
return nil, false, ErrMutationConflict
}
return append([]byte(nil), result...), true, nil
}