Skip to content

Commit 495b3a7

Browse files
Merge pull request #1771 from entireio/fix/1770-git-refs-remote-list-discovery
feat(checkpoint): discover refs-native checkpoints on a second device via List remote enumeration
2 parents 0bfed39 + 93ab0d5 commit 495b3a7

18 files changed

Lines changed: 1035 additions & 21 deletions

api/checkpoint/metadata.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ type CheckpointInfo struct {
293293
// Imported is true when this checkpoint was imported from pre-existing
294294
// agent history (Kind == "imported"): read-only and commit-less.
295295
Imported bool
296+
297+
// ListedStub is true for names-only remote-discovery List entries that still
298+
// need hydration (or have not yet failed a hydration attempt). It is cleared
299+
// after a successful hydrate and also after a failed attempt (fail-once), so
300+
// callers do not re-fetch forever. A local ref whose root metadata was
301+
// unreadable has the same zero SessionID/SessionCount shape but ListedStub
302+
// false — do not treat field zero-ness alone as stub-ness.
303+
ListedStub bool `json:"-"`
296304
}
297305

298306
// SessionContent contains the actual content for a session.

cmd/entire/cli/checkpoint/fetching_tree.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ type BlobFetchFunc func(ctx context.Context, hashes []plumbing.Hash) error
2323
// package cannot resolve the remote target itself, so the CLI layer injects it.
2424
type RefFetchFunc func(ctx context.Context, ref plumbing.ReferenceName) error
2525

26+
// RemoteRefListFunc enumerates the per-checkpoint refs present on the configured
27+
// checkpoint remote (names only, via `ls-remote refs/entire/checkpoints/*` — no
28+
// object transfer), returning their full ref names. The git-refs store uses it
29+
// in List to discover checkpoints written on another machine that have no local
30+
// ref yet; each discovered checkpoint is then hydrated lazily on read via
31+
// RefFetchFunc. The checkpoint package cannot resolve the remote target itself,
32+
// so the CLI layer injects it.
33+
//
34+
// Scope is stricter than the on-demand read fetch: with no checkpoint_remote
35+
// configured the lister returns (nil, nil) and List stays local-only. The
36+
// on-demand fetch (FetchURL) falls back to origin in that case. When a
37+
// checkpoint_remote is configured the lister queries the resolved checkpoint
38+
// URL (which can still fall through to origin in FetchURL edge cases).
39+
type RemoteRefListFunc func(ctx context.Context) ([]plumbing.ReferenceName, error)
40+
2641
// FetchingTree wraps a git tree to automatically fetch missing blobs on demand.
2742
// After a treeless fetch (--filter=blob:none), tree objects are available locally
2843
// but blob objects are not. Each File() call checks whether the target blob

cmd/entire/cli/checkpoint/id/id.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"fmt"
1010
"regexp"
11+
"time"
1112

1213
ulid "github.com/oklog/ulid/v2"
1314
)
@@ -224,6 +225,24 @@ func (id CheckpointID) IsEmpty() bool {
224225
return id == EmptyCheckpointID
225226
}
226227

228+
// Time returns the creation time encoded in this ID and whether one is
229+
// available. A ULID embeds a millisecond Unix timestamp in its leading
230+
// characters, so the time is recoverable from the ID alone — no store read
231+
// required. This is what lets remote-ref discovery (which learns only ref names
232+
// via ls-remote) present and sort a not-yet-hydrated checkpoint by its real
233+
// creation time. Legacy 12-hex IDs carry no timestamp, so this returns
234+
// (zero, false) for them.
235+
func (id CheckpointID) Time() (time.Time, bool) {
236+
if id.Kind() != KindULID {
237+
return time.Time{}, false
238+
}
239+
u, err := ulid.ParseStrict(string(id))
240+
if err != nil {
241+
return time.Time{}, false
242+
}
243+
return ulid.Time(u.Time()), true
244+
}
245+
227246
// Path returns the sharded path for this checkpoint ID on entire/checkpoints/v1.
228247
// Uses first 2 characters as shard (256 buckets), remaining as folder name.
229248
// Example: "a3b2c4d5e6f7" -> "a3/b2c4d5e6f7"

cmd/entire/cli/checkpoint/id/id_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
11
package id
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"testing"
7+
"time"
8+
9+
ulid "github.com/oklog/ulid/v2"
610
)
711

812
// A representative ULID (Crockford base32, 26 chars) used across tests.
913
const sampleULID = "01KVBJCWYA4YW6J5M9GP655HZN"
1014

15+
func TestCheckpointID_Time(t *testing.T) {
16+
t.Parallel()
17+
18+
// A ULID minted from a known instant recovers that instant (millisecond
19+
// precision), so remote-ref discovery can sort/display a checkpoint by its
20+
// real creation time from the ref name alone — no store read.
21+
want := time.UnixMilli(1700000000000).UTC()
22+
// Deterministic entropy (zeros); Time() only reads the timestamp prefix.
23+
minted := ulid.MustNew(ulid.Timestamp(want), bytes.NewReader(make([]byte, 16)))
24+
got, ok := CheckpointID(minted.String()).Time()
25+
if !ok {
26+
t.Fatalf("Time() ok = false for a valid ULID %q", minted)
27+
}
28+
if !got.Equal(want) {
29+
t.Errorf("Time() = %v, want %v", got, want)
30+
}
31+
32+
// The canonical sample ULID also yields a non-zero time.
33+
if ts, ok := CheckpointID(sampleULID).Time(); !ok || ts.IsZero() {
34+
t.Errorf("Time() for sample ULID = (%v, %v), want a non-zero time", ts, ok)
35+
}
36+
37+
// A legacy hex ID carries no timestamp.
38+
if _, ok := CheckpointID("a1b2c3d4e5f6").Time(); ok {
39+
t.Errorf("Time() ok = true for a legacy hex ID; want false")
40+
}
41+
42+
// Non-ID / empty strings report no time.
43+
if _, ok := CheckpointID("").Time(); ok {
44+
t.Errorf("Time() ok = true for empty ID; want false")
45+
}
46+
if _, ok := CheckpointID("not-an-id").Time(); ok {
47+
t.Errorf("Time() ok = true for a non-ID string; want false")
48+
}
49+
}
50+
1151
func TestGenerateULID(t *testing.T) {
1252
t.Parallel()
1353
a, err := GenerateULID()

cmd/entire/cli/checkpoint/open.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ type OpenOptions struct {
2424
// reads local-only; ignored by the git-branch backend.
2525
RefFetcher RefFetchFunc
2626

27+
// RemoteRefLister is the CLI-level checkpoint-ref enumerator, used by the
28+
// git-refs backend's List to discover checkpoints present on the checkpoint
29+
// remote but not yet local (see RemoteRefListFunc). It only fires on a
30+
// context marked by WithRemoteListDiscovery. nil (or an unmarked context)
31+
// leaves List local-only; ignored by the git-branch backend.
32+
RemoteRefLister RemoteRefListFunc
33+
2734
// Refs overrides the default committed-ref topology. A non-nil value wins,
2835
// e.g. attach pins reads to Primary via PrimaryAsRead().
2936
Refs *PersistentRefs
@@ -62,7 +69,7 @@ type Stores struct {
6269
// default git-branch backend with no mirrors, preserving default behavior.
6370
func Open(ctx context.Context, repo *git.Repository, opts OpenOptions) (*Stores, error) {
6471
refs := resolveOpenRefs(ctx, opts)
65-
env := OpenEnv{Repo: repo, BlobFetcher: opts.BlobFetcher, RefFetcher: opts.RefFetcher, Refs: refs}
72+
env := OpenEnv{Repo: repo, BlobFetcher: opts.BlobFetcher, RefFetcher: opts.RefFetcher, RemoteRefLister: opts.RemoteRefLister, Refs: refs}
6673

6774
cfg, err := settings.LoadCheckpointsConfig(ctx)
6875
if err != nil {

cmd/entire/cli/checkpoint/refs_naming.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const CheckpointRefPrefix = "refs/entire/checkpoints/"
1717

1818
// RefName returns the per-checkpoint git ref for a checkpoint ID:
1919
// refs/entire/checkpoints/<shard>/<id>, where <shard> is id.ShardFor() (the
20-
// first two chars for legacy hex IDs, the last two for ULIDs). The full ID is
21-
// always the leaf, so the ref round-trips through ParseRef.
20+
// last two characters of the ID for both legacy hex and ULID formats). The full
21+
// ID is always the leaf, so the ref round-trips through ParseRef.
2222
//
2323
// It errors on an empty or unrecognized checkpoint ID rather than returning a
2424
// malformed ref (e.g. "refs/entire/checkpoints//"), so callers at trust

0 commit comments

Comments
 (0)