package store import ( "context" "crypto/subtle" "database/sql" "errors" "fmt" "io/fs" "math" "path/filepath" ) const ( FaultAfterSegmentSyncBeforeMetadata = "after_segment_sync_before_metadata" FaultAfterMetadataCommit = "after_metadata_commit" ) var ( ErrStoreClosed = errors.New("store is closed") ErrCommandNotFound = errors.New("command not found") ErrEventConflict = errors.New("event sequence already has different immutable content") ErrEventSequenceGap = errors.New("event sequence is not the next expected value") ) type FaultInjector interface { Checkpoint(name string) error } type FaultInjectorFunc func(name string) error func (function FaultInjectorFunc) Checkpoint(name string) error { return function(name) } type EventAppend struct { IssueUUID [16]byte EventSeq uint64 ObservedUnixNano int64 ReceiptUnixNano int64 EventType uint16 Stream uint16 Compression uint16 RawLength uint64 Payload []byte ImmutableSHA256 [32]byte } type EventAppendResult struct { ThroughEventSeq uint64 Duplicate bool } type activeSegment struct { segment *Segment ordinal uint32 committed uint64 } func (store *Store) AppendCommandEvent(ctx context.Context, event EventAppend) (EventAppendResult, error) { var result EventAppendResult if isZeroUUID(event.IssueUUID) || event.EventSeq == 0 || event.EventSeq > math.MaxInt64 || event.EventType == 0 || event.ReceiptUnixNano <= 0 || allZero(event.ImmutableSHA256[:]) { return result, ErrInvalidSegmentRecord } record := SegmentRecord{ OwnerUUID: event.IssueUUID, Sequence: event.EventSeq, ObservedUnixNano: event.ObservedUnixNano, ReceiptUnixNano: event.ReceiptUnixNano, Kind: PayloadKindCommandEvent, Stream: event.Stream, Compression: event.Compression, RawLength: event.RawLength, Payload: event.Payload, } encoded, err := EncodeSegmentRecord(record, DefaultSegmentLimits()) if err != nil { return result, err } store.writeMu.Lock() defer store.writeMu.Unlock() database, err := store.openDatabase() if err != nil { return result, err } var lastSequence uint64 err = database.QueryRowContext(ctx, `SELECT last_event_seq FROM commands WHERE issue_uuid = ?`, event.IssueUUID[:]).Scan(&lastSequence) if errors.Is(err, sql.ErrNoRows) { return result, ErrCommandNotFound } if err != nil { return result, err } if event.EventSeq <= lastSequence { var storedHash []byte if err := database.QueryRowContext(ctx, `SELECT immutable_sha256 FROM command_events WHERE issue_uuid = ? AND event_seq = ?`, event.IssueUUID[:], event.EventSeq).Scan(&storedHash); err != nil { return result, fmt.Errorf("load duplicate event: %w", err) } if len(storedHash) != len(event.ImmutableSHA256) || subtle.ConstantTimeCompare(storedHash, event.ImmutableSHA256[:]) != 1 { return result, ErrEventConflict } return EventAppendResult{ThroughEventSeq: lastSequence, Duplicate: true}, nil } if event.EventSeq != lastSequence+1 { return result, ErrEventSequenceGap } active, err := store.segmentForAppend(ctx, database, event.IssueUUID, uint64(len(encoded))) if err != nil { return result, err } recordOffset := active.segment.offset committedEnd, err := active.segment.Append(record) if err != nil { store.invalidateActiveSegment(event.IssueUUID) return result, err } if err := store.checkpoint(FaultAfterSegmentSyncBeforeMetadata); err != nil { store.invalidateActiveSegment(event.IssueUUID) return result, err } tx, err := database.BeginTx(ctx, nil) if err != nil { store.invalidateActiveSegment(event.IssueUUID) return result, err } streamMix := uint64(0) if event.Stream > 0 { streamMix = uint64(1) << (event.Stream - 1) } if active.committed == 0 { _, err = tx.ExecContext(ctx, `INSERT INTO output_segments ( issue_uuid, ordinal, path, committed_end_offset, min_event_seq, max_event_seq, stream_mix, compressed_bytes, raw_bytes, checksum, sealed, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?)`, event.IssueUUID[:], active.ordinal, active.segment.Name(), committedEnd, event.EventSeq, event.EventSeq, streamMix, len(event.Payload), event.RawLength, event.ReceiptUnixNano) } else { var update sql.Result update, err = tx.ExecContext(ctx, `UPDATE output_segments SET committed_end_offset = ?, max_event_seq = ?, stream_mix = stream_mix | ?, compressed_bytes = compressed_bytes + ?, raw_bytes = raw_bytes + ? WHERE issue_uuid = ? AND ordinal = ? AND committed_end_offset = ?`, committedEnd, event.EventSeq, streamMix, len(event.Payload), event.RawLength, event.IssueUUID[:], active.ordinal, active.committed) if err == nil { var affected int64 affected, err = update.RowsAffected() if err == nil && affected != 1 { err = errors.New("segment committed offset changed unexpectedly") } } } if err == nil { _, err = tx.ExecContext(ctx, `INSERT INTO command_events ( issue_uuid, event_seq, observed_at, server_receipt_time, event_type, compression, raw_bytes, stored_bytes, payload, segment_ordinal, segment_record_offset, segment_record_length, immutable_sha256 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?)`, event.IssueUUID[:], event.EventSeq, event.ObservedUnixNano, event.ReceiptUnixNano, event.EventType, event.Compression, event.RawLength, len(event.Payload), active.ordinal, recordOffset, uint64(len(encoded)), event.ImmutableSHA256[:]) } if err == nil { var update sql.Result update, err = tx.ExecContext(ctx, `UPDATE commands SET last_event_seq = ?, retained_compressed_bytes = retained_compressed_bytes + ? WHERE issue_uuid = ? AND last_event_seq = ?`, event.EventSeq, len(event.Payload), event.IssueUUID[:], lastSequence) if err == nil { var affected int64 affected, err = update.RowsAffected() if err == nil && affected != 1 { err = errors.New("command event sequence changed unexpectedly") } } } if err != nil { _ = tx.Rollback() store.invalidateActiveSegment(event.IssueUUID) return result, fmt.Errorf("commit event metadata: %w", err) } if err := tx.Commit(); err != nil { store.invalidateActiveSegment(event.IssueUUID) return result, fmt.Errorf("commit event metadata: %w", err) } active.committed = committedEnd result.ThroughEventSeq = event.EventSeq if err := store.checkpoint(FaultAfterMetadataCommit); err != nil { return result, err } return result, nil } func (store *Store) segmentForAppend(ctx context.Context, database *sql.DB, owner [16]byte, recordLength uint64) (*activeSegment, error) { if current := store.activeSegments[owner]; current != nil { if current.segment.offset == 0 || current.segment.offset+recordLength <= store.segmentTarget { return current, nil } if err := current.segment.Close(); err != nil { return nil, err } delete(store.activeSegments, owner) } var highest int64 if err := database.QueryRowContext(ctx, `SELECT COALESCE(MAX(ordinal), -1) FROM output_segments WHERE issue_uuid = ?`, owner[:]).Scan(&highest); err != nil { return nil, err } if highest >= math.MaxUint32 { return nil, errors.New("segment ordinal exhausted") } ordinal := uint32(highest + 1) for { segment, err := CreateSegment(store.segmentDirectory(), owner, ordinal, DefaultSegmentLimits()) if err == nil { active := &activeSegment{segment: segment, ordinal: ordinal} store.activeSegments[owner] = active return active, nil } if !errors.Is(err, fs.ErrExist) || ordinal == math.MaxUint32 { return nil, err } ordinal++ } } func (store *Store) invalidateActiveSegment(owner [16]byte) { if active := store.activeSegments[owner]; active != nil { _ = active.segment.Close() delete(store.activeSegments, owner) } } func (store *Store) openDatabase() (*sql.DB, error) { store.mu.Lock() defer store.mu.Unlock() if store.db == nil { return nil, ErrStoreClosed } return store.db, nil } func (store *Store) checkpoint(name string) error { if store.faultInjector == nil { return nil } return store.faultInjector.Checkpoint(name) } func (store *Store) segmentDirectory() string { return filepath.Join(store.dataDir, "segments") }