Skip to content

Commit 08fc8f6

Browse files
committed
feat: kv wildcard list
1 parent 2777bcd commit 08fc8f6

6 files changed

Lines changed: 107 additions & 6 deletions

File tree

internal/components/sessions/sessions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ type SessionStore interface {
1616
// Returns ErrNotFound when absent.
1717
GetSession(id, dockerdId string) (*models.Session, error)
1818

19+
// GetSessionById retrieves a session by id regardless of dockerd namespace.
20+
// Returns ErrNotFound when absent.
21+
GetSessionById(id string) (*models.Session, error)
22+
1923
// ListSessions returns the raw bytes of every stored session across all daemons.
2024
ListSessions() ([]*models.Session, error)
2125

internal/components/sessions/sessions_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,33 @@ func TestSessionStore_Get_WrongDockerdId(t *testing.T) {
7676
}
7777
}
7878

79+
// The following test covers id-only retrieval across daemon namespaces.
80+
func TestSessionStore_GetById(t *testing.T) {
81+
s := newTestSessionStore()
82+
83+
_ = s.SaveSession(&models.Session{Id: "s1", DockerDId: "d1", Spec: models.Spec{Image: "a"}})
84+
_ = s.SaveSession(&models.Session{Id: "s2", DockerDId: "d2", Spec: models.Spec{Image: "b"}})
85+
86+
got, err := s.GetSessionById("s2")
87+
if err != nil {
88+
t.Fatalf("GetSessionById: %v", err)
89+
}
90+
91+
if got.Id != "s2" || got.DockerDId != "d2" {
92+
t.Errorf("GetSessionById: got (%s,%s), want (s2,d2)", got.Id, got.DockerDId)
93+
}
94+
}
95+
96+
// The following test covers id-only retrieval for unknown ids.
97+
func TestSessionStore_GetById_NotFound(t *testing.T) {
98+
s := newTestSessionStore()
99+
100+
_, err := s.GetSessionById("missing")
101+
if !errors.Is(err, xerrors.StorageErrNotFound) {
102+
t.Errorf("GetSessionById missing: got %v, want xerrors.StorageErrNotFound", err)
103+
}
104+
}
105+
79106
// The following test covers the overwrite behavior of SaveSession,
80107
// ensuring that saving a session with the same id and dockerdId updates the existing entry rather than creating a duplicate.
81108
func TestSessionStore_Save_Overwrite(t *testing.T) {

internal/components/sessions/store.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/amirhnajafiz/bedrock-api/internal/storage"
77
"github.com/amirhnajafiz/bedrock-api/pkg/models"
8+
"github.com/amirhnajafiz/bedrock-api/pkg/xerrors"
89
)
910

1011
// sessionPrefix namespaces all session keys inside the shared KVStorage backend.
@@ -52,6 +53,25 @@ func (s *sessionStore) GetSession(id, dockerdId string) (*models.Session, error)
5253
return session, nil
5354
}
5455

56+
// GetSessionById retrieves the session by scanning all daemon namespaces.
57+
func (s *sessionStore) GetSessionById(id string) (*models.Session, error) {
58+
bytes, err := s.backend.List(sessionPrefix + "*/" + id)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
if len(bytes) == 0 {
64+
return nil, xerrors.StorageErrNotFound
65+
}
66+
67+
var session *models.Session
68+
if err := json.Unmarshal(bytes[0], &session); err != nil {
69+
return nil, err
70+
}
71+
72+
return session, nil
73+
}
74+
5575
// ListSessions returns the raw bytes of every stored session across all daemons.
5676
func (s *sessionStore) ListSessions() ([]*models.Session, error) {
5777
bytes, err := s.backend.List(sessionPrefix)

internal/storage/gocache/backend.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"errors"
1111
"fmt"
12+
"path"
1213
"strings"
1314
"time"
1415

@@ -63,18 +64,33 @@ func (b *Backend) Delete(key string) error {
6364
return b.cache.Delete(context.Background(), key)
6465
}
6566

66-
// List returns the values of all entries whose keys start with prefix.
67-
// An empty prefix returns every value currently in the store.
67+
// List returns the values of all entries whose keys match wildcard.
68+
// If wildcard contains no glob tokens, it is treated as a key prefix.
69+
// If wildcard contains glob tokens ('*', '?', '['), path.Match is used.
70+
// An empty wildcard returns every value currently in the store.
6871
// The returned slice is a snapshot; mutations to it do not affect the cache.
6972
// eko/gocache does not expose a scan API, so this method accesses the
7073
// underlying go-cache client directly via Items().
71-
func (b *Backend) List(prefix string) ([][]byte, error) {
74+
func (b *Backend) List(wildcard string) ([][]byte, error) {
7275
items := b.client.Items()
7376
var result [][]byte
77+
hasGlob := strings.ContainsAny(wildcard, "*?[")
78+
if hasGlob {
79+
if _, err := path.Match(wildcard, ""); err != nil {
80+
return nil, fmt.Errorf("gocache: invalid wildcard %q: %w", wildcard, err)
81+
}
82+
}
7483

7584
for k, item := range items {
76-
if !strings.HasPrefix(k, prefix) {
77-
continue
85+
if wildcard != "" {
86+
if hasGlob {
87+
matched, _ := path.Match(wildcard, k)
88+
if !matched {
89+
continue
90+
}
91+
} else if !strings.HasPrefix(k, wildcard) {
92+
continue
93+
}
7894
}
7995

8096
data, ok := item.Object.([]byte)

internal/storage/gocache/backend_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,36 @@ func TestBackend_List_NoMatch(t *testing.T) {
134134
t.Errorf("List no-match: got %d entries, want 0", len(result))
135135
}
136136
}
137+
138+
func TestBackend_List_WithWildcard(t *testing.T) {
139+
b := newTestBackend()
140+
141+
_ = b.Set("sessions/d1/s1", []byte("d1s1"))
142+
_ = b.Set("sessions/d2/s1", []byte("d2s1"))
143+
_ = b.Set("sessions/d2/s2", []byte("d2s2"))
144+
145+
result, err := b.List("sessions/*/s1")
146+
if err != nil {
147+
t.Fatalf("List wildcard: unexpected error: %v", err)
148+
}
149+
150+
if len(result) != 2 {
151+
t.Errorf("List sessions/*/s1: got %d entries, want 2", len(result))
152+
}
153+
154+
want := map[string]bool{"d1s1": true, "d2s1": true}
155+
for _, v := range result {
156+
if !want[string(v)] {
157+
t.Errorf("List wildcard returned unexpected value %q", v)
158+
}
159+
}
160+
}
161+
162+
func TestBackend_List_InvalidWildcard(t *testing.T) {
163+
b := newTestBackend()
164+
165+
_, err := b.List("sessions/[*/s1")
166+
if err == nil {
167+
t.Errorf("List invalid wildcard: expected error, got nil")
168+
}
169+
}

internal/storage/storage.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ type KVStorage interface {
2020
Get(key string) ([]byte, error)
2121
// Delete removes the entry for key. It is a no-op when the key does not exist.
2222
Delete(key string) error
23-
// List retrieves the values for a matching wildcard with the key.
23+
// List retrieves the values for keys matching wildcard.
24+
// When wildcard has no glob tokens, implementations may treat it as a prefix.
2425
List(wildcard string) ([][]byte, error)
2526
}
2627

0 commit comments

Comments
 (0)