Skip to content

refactor(store): stage 3 of the InstanceInfo split - versioned session records with transparent legacy upgrade#1025

Merged
acouvreur merged 1 commit into
mainfrom
refactor/session-record
Jul 11, 2026
Merged

refactor(store): stage 3 of the InstanceInfo split - versioned session records with transparent legacy upgrade#1025
acouvreur merged 1 commit into
mainfrom
refactor/session-record

Conversation

@acouvreur

Copy link
Copy Markdown
Member

Finding

Stage 3 of the InstanceInfo split (stage 1: #1023, stage 2: #1024). This one targets the persistence facet: both stores serialized the raw domain InstanceInfo as the session entry, so the storage schema was "whatever the domain struct happens to marshal as". Consequences:

  • Every field added to InstanceInfo silently became persisted state.
  • There was no version to gate schema evolution and no way to recognize records written by an older release - a future shape change would have silently mis-decoded live sessions.
  • Stale label configuration is frozen into sessions (the instanceRequest path even warns about invalid stored running-hours values).

A constraint discovered during design (worth knowing)

The stored record is not just bookkeeping: on the ready path, instanceRequest serves the stored InstanceInfo back to callers without any provider call - that is Sablier's documented warm-session performance model. So the record cannot be slimmed down to "just session fields" without either changing API responses or re-inspecting per request. This stage therefore delivers the versioned envelope + migration machinery; actually removing label config from the record is a later stage that will now be an explicit v2 schema bump instead of a silent break.

What this does

  • Sessions persist as an explicit SessionRecord envelope - {"v":1,"instance":{...}} - defined next to the Store interface.
  • SessionRecord.UnmarshalJSON transparently upgrades pre-versioning payloads (bare InstanceInfo documents): live sessions in valkey and state.json snapshots survive the Sablier upgrade instead of being dropped or mis-read.
  • Foreign keys sharing the valkey keyspace keep the previous tolerance (decode to an empty-name record, skipped by the existing name check).
  • The Store interface is unchanged (Get/Put still speak InstanceInfo), so core, providers, API and wire are untouched - zero behavior change outside the persisted bytes.

Migration evidence

  • TestSessionRecordLegacyUpgrade - a bare pre-versioning payload decodes to a current-version record with every field intact.
  • TestInMemorySnapshotIsVersioned - new snapshots carry "v":1; a legacy-shaped snapshot loads and upgrades.
  • TestValKeySessionRecordMigration (testcontainers) - a legacy payload written directly into valkey is readable through both Get and Range (the metrics path), and new entries carry the version.
  • The sabliercmd storage fixtures already used the legacy shape, so the existing state-file load tests now exercise the upgrade end to end - unchanged and passing.

Full pkg/sablier + store suites (including valkey via testcontainers) and golangci-lint pass.

Docs

No user-facing impact: no config, labels, API, or generated docs change (the record is persistence-internal and does not appear in the OpenAPI schema).

Both stores serialized the raw domain InstanceInfo as the persisted session
entry, making the storage schema "whatever the domain struct happens to
marshal as": every field added to InstanceInfo silently became persisted
state with no version to gate evolution and no way to recognize records
written by an older release.

Sessions are now wrapped in a SessionRecord envelope ({"v":1,"instance":
{...}}) owned next to the Store interface. Its unmarshaler transparently
upgrades pre-versioning payloads (bare InstanceInfo documents), so live
sessions in valkey and state.json snapshots survive the upgrade instead of
being dropped. Foreign keys sharing the valkey keyspace keep the previous
tolerance: they decode to a record with an empty instance name and are
skipped by the existing name check.

The Store interface is unchanged (Get/Put still speak InstanceInfo), so the
core and providers are untouched; this is stage 3 of the InstanceInfo split
(stage 1: #1023, stage 2: #1024) and the prerequisite for slimming what
sessions actually persist, which will be a schema-version bump instead of a
silent break.

Migration coverage: record round-trip and legacy-upgrade unit tests, an
inmemory legacy-snapshot load test, and valkey testcontainers tests reading
a legacy payload through Get and Range and asserting new entries carry the
version. The sabliercmd storage fixtures already use the legacy shape, so
the state-file load tests now exercise the upgrade end to end.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed! | 794 tests in 102.321s

View HTML Test Report

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ Diff between sablier and sablier                                                      │
├─────────┬─────────────────────────────────────────────┬──────────┬──────────┬─────────┤
│ PERCENT │ NAME                                        │ OLD SIZE │ NEW SIZE │ DIFF    │
├─────────┼─────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ +0.28%  │ go.opentelemetry.io/otel                    │ 1.1 MB   │ 1.1 MB   │ +3.0 kB │
│ +0.27%  │ github.com/sablierapp/sablier               │ 654 kB   │ 656 kB   │ +1.7 kB │
│ +0.54%  │ go.yaml.in/yaml/v3                          │ 312 kB   │ 314 kB   │ +1.7 kB │
│ +0.04%  │ runtime                                     │ 3.3 MB   │ 3.3 MB   │ +1.2 kB │
│ +0.18%  │ go.yaml.in/yaml/v2                          │ 275 kB   │ 275 kB   │ +504 B  │
│ +0.16%  │ golang.org/x/crypto                         │ 64 kB    │ 64 kB    │ +101 B  │
│ +0.02%  │ encoding                                    │ 418 kB   │ 418 kB   │ +74 B   │
│ +0.16%  │ vendor/golang.org/x/net/http2/hpack         │ 35 kB    │ 35 kB    │ +54 B   │
│ +0.09%  │ k8s.io/utils                                │ 32 kB    │ 32 kB    │ +29 B   │
│ +0.46%  │ vendor/golang.org/x/sys/cpu                 │ 6.4 kB   │ 6.4 kB   │ +29 B   │
│ +0.01%  │ log                                         │ 126 kB   │ 126 kB   │ +16 B   │
│ +0.00%  │ crypto                                      │ 1.9 MB   │ 1.9 MB   │ +13 B   │
│ +0.00%  │ k8s.io/apimachinery                         │ 1.8 MB   │ 1.8 MB   │ +11 B   │
│ +0.01%  │ github.com/spf13/viper                      │ 73 kB    │ 73 kB    │ +7 B    │
│ +0.00%  │ github.com/spf13/cobra                      │ 239 kB   │ 239 kB   │ +1 B    │
│ +0.00%  │ github.com/emicklei/go-restful/v3           │ 134 kB   │ 134 kB   │ +1 B    │
│ -0.00%  │ github.com/quic-go/quic-go                  │ 1.4 MB   │ 1.4 MB   │ -1 B    │
│ -0.01%  │ github.com/pmezard/go-difflib               │ 17 kB    │ 17 kB    │ -1 B    │
│ -0.00%  │ go.mongodb.org/mongo-driver/v2              │ 674 kB   │ 674 kB   │ -2 B    │
│ -0.00%  │ github.com/google/gnostic-models            │ 1.6 MB   │ 1.6 MB   │ -2 B    │
│ -0.00%  │ github.com/luthermonson/go-proxmox          │ 333 kB   │ 333 kB   │ -3 B    │
│ -0.00%  │ k8s.io/api                                  │ 17 MB    │ 17 MB    │ -8 B    │
│ -0.02%  │ golang.org/x/sys                            │ 46 kB    │ 46 kB    │ -10 B   │
│ -0.31%  │ internal/cpu                                │ 6.1 kB   │ 6.1 kB   │ -19 B   │
│ -0.00%  │ k8s.io/client-go                            │ 14 MB    │ 14 MB    │ -24 B   │
│ -0.11%  │ vendor/golang.org/x/net/idna                │ 22 kB    │ 22 kB    │ -25 B   │
│ -0.00%  │ golang.org/x/net                            │ 902 kB   │ 902 kB   │ -29 B   │
│ -0.04%  │ k8s.io/klog/v2                              │ 124 kB   │ 124 kB   │ -45 B   │
│ -0.05%  │ go.opentelemetry.io/auto/sdk                │ 89 kB    │ 89 kB    │ -47 B   │
│ -0.02%  │ github.com/gin-gonic/gin                    │ 337 kB   │ 337 kB   │ -56 B   │
│ -0.04%  │ sigs.k8s.io/json                            │ 173 kB   │ 173 kB   │ -74 B   │
│ -0.14%  │ vendor/golang.org/x/crypto/chacha20poly1305 │ 71 kB    │ 71 kB    │ -101 B  │
│ -17.89% │ go.shape                                    │ 4.8 kB   │ 3.9 kB   │ -855 B  │
│ -0.72%  │ gopkg.in/yaml.v3                            │ 306 kB   │ 304 kB   │ -2.2 kB │
├─────────┼─────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ +0.02%  │ .rodata                                     │ 2.6 MB   │ 2.6 MB   │ +559 B  │
├─────────┼─────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ +0.01%  │ sablier                                     │ 62 MB    │ 62 MB    │ +8.2 kB │
│         │ sablier                                     │          │          │         │
└─────────┴─────────────────────────────────────────────┴──────────┴──────────┴─────────┘

@acouvreur acouvreur merged commit 57b9fe4 into main Jul 11, 2026
8 checks passed
@acouvreur acouvreur deleted the refactor/session-record branch July 11, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant