Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
67cef25
feat(crdb): add experimental changelog-watch flag plumbing
ecordell Jul 1, 2026
daedbc9
feat(crdb): gated creation of experimental relationship_changelog table
ecordell Jul 1, 2026
12c08cd
feat(crdb): dual-write relationships into changelog table
ecordell Jul 1, 2026
deef3b6
fix(crdb): make changelog ordinal transaction-scoped to avoid PK coll…
ecordell Jul 1, 2026
d9310f4
feat(crdb): dual-write schema changes into changelog table
ecordell Jul 2, 2026
8fb5224
feat(crdb): poll-based changelog Watch at closed timestamp
ecordell Jul 2, 2026
d3d4b3b
fix(crdb): emit changelog rows at exactly the poll target revision
ecordell Jul 2, 2026
2bf3343
feat(crdb): dual-write bulk-loaded relationships into changelog
ecordell Jul 2, 2026
98c6a4b
feat(crdb): changefeed nudge for low-latency changelog watch
ecordell Jul 2, 2026
8ca0b10
feat(crdb): stale-cursor handling + docs for experimental changelog w…
ecordell Jul 2, 2026
c75665d
fix(crdb): chunk bulk changelog insert to stay under param limit
ecordell Jul 2, 2026
3645c27
fix(datastore): use caveat prefix when re-accounting caveat byte size
ecordell Jul 2, 2026
6154b43
docs(crdb): document EmitImmediatelyStrategy limitation of changelog …
ecordell Jul 2, 2026
ca15ca6
feat(crdb): carry transaction metadata in changelog and skip transact…
ecordell Jul 2, 2026
c227002
feat(crdb): emit changelog changes immediately, checkpoint at max-off…
ecordell Jul 2, 2026
5f485b9
test(crdb): deterministic immediate-mode cursor/dedup coverage for ch…
ecordell Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]
### Added
- Added an experimental CockroachDB `--datastore-experimental-crdb-changelog-watch` flag that serves the Watch API from a dual-written changelog table polled at a closed timestamp, keeping Watch advancing during bulk loads (at the cost of write latency).

### Changed
- Schema: reads inside write transactions now use a cheap hash-only lookup (`schema_revision`) to check the cache before loading the full schema blob, reducing DB round-trips on cache hits (https://github.com/authzed/spicedb/pull/3160)
- Updated the Prometheus buckets for `grpc_server_handling_seconds` and `spicedb_datastore_query_latency` to be able to correlate them (https://github.com/authzed/spicedb/pull/3188)
Expand Down
6 changes: 6 additions & 0 deletions docs/spicedb.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/datastore/common/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func (ch *Changes[R, K]) AddChangedDefinition(
case *core.CaveatDefinition:
delete(record.caveatsDeleted, t.Name)

if existing, ok := record.definitionsChanged[nsPrefix+t.Name]; ok {
if existing, ok := record.definitionsChanged[caveatPrefix+t.Name]; ok {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a latent bug to me

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱 yes, please send a different PR

if err := ch.adjustByteSize(existing, -1); err != nil {
return err
}
Expand Down
34 changes: 34 additions & 0 deletions internal/datastore/common/changes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"testing"

"github.com/ccoveille/go-safecast/v2"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"

Expand Down Expand Up @@ -828,6 +829,39 @@ func TestMaximumSizeReplacement(t *testing.T) {
require.Equal(t, int64(243), ch.currentByteSize)
}

// TestMaximumSizeCaveatReplacement is a regression test for a byte-accounting
// bug where re-changing the same caveat within one window looked up the
// prior entry under the namespace-prefixed key (nsPrefix+name) instead of the
// caveat-prefixed key it was actually stored under (caveatPrefix+name). That
// mismatch meant the prior entry's byte size was never decremented before the
// new one was added, so currentByteSize was over-counted on every repeated
// change to the same caveat. With the fix, replacing a caveat definition
// leaves currentByteSize unchanged (matching the analogous namespace
// behavior verified by TestMaximumSizeReplacement).
func TestMaximumSizeCaveatReplacement(t *testing.T) {
ctx := t.Context()

caveatDef := &core.CaveatDefinition{Name: "somecaveat"}
size := caveatDef.SizeVT()

ch := NewChanges(revisions.HLCKeyFunc, datastore.WatchRelationships|datastore.WatchSchema, safecast.MustConvert[uint64](size))
require.True(t, ch.IsEmpty())

rev0, err := revisions.HLCRevisionFromString("1")
require.NoError(t, err)

err = ch.AddChangedDefinition(ctx, rev0, caveatDef)
require.NoError(t, err)
require.EqualValues(t, size, ch.currentByteSize)

// Changing the same caveat again must not accumulate byte size: the
// prior entry should be found (under caveatPrefix, not nsPrefix) and its
// size decremented before the new size is added back in.
err = ch.AddChangedDefinition(ctx, rev0, caveatDef)
require.NoError(t, err)
require.EqualValues(t, size, ch.currentByteSize)
}

func TestCanonicalize(t *testing.T) {
testCases := []struct {
name string
Expand Down
24 changes: 24 additions & 0 deletions internal/datastore/crdb/caveat.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@
if _, err := rwt.tx.Exec(ctx, sql, args...); err != nil {
return fmt.Errorf(errWriteCaveat, err)
}

if rwt.changelogWatchEnabled {
ttl := rwt.changelogTTL()
for _, caveat := range caveats {
serialized, err := caveat.MarshalVT()
if err != nil {
return fmt.Errorf(errWriteCaveat, err)
}
if err := rwt.appendSchemaChangelog(ctx, "caveat", caveat.Name, serialized, rwt.nextChangelogOrdinal(), ttl); err != nil {
return err
}

Check warning on line 168 in internal/datastore/crdb/caveat.go

View check run for this annotation

Codecov / codecov/patch

internal/datastore/crdb/caveat.go#L160-L168

Added lines #L160 - L168 were not covered by tests
}
}

return nil
}

Expand All @@ -170,5 +184,15 @@
if _, err := rwt.tx.Exec(ctx, sql, args...); err != nil {
return fmt.Errorf(errDeleteCaveats, err)
}

if rwt.changelogWatchEnabled {
ttl := rwt.changelogTTL()
for _, name := range names {
if err := rwt.appendSchemaChangelog(ctx, "caveat", name, nil, rwt.nextChangelogOrdinal(), ttl); err != nil {
return err
}

Check warning on line 193 in internal/datastore/crdb/caveat.go

View check run for this annotation

Codecov / codecov/patch

internal/datastore/crdb/caveat.go#L189-L193

Added lines #L189 - L193 were not covered by tests
}
}

return nil
}
Loading
Loading