clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

fix: retain Git recovery journals on storage errors

Dami committed aa10e5a
Return metadata read failures before deciding whether a journal is obsolete. Preserve the pending transition so recovery can retry once storage is available.

Reproduce a failed read, verify the journal survives, then complete recovery through the restored store.
commit
aa10e5ab59902dcc8e778c9a85f0f5889132541b
parent
aff3977dcd4ab365bd4e41ccf5a911d9fbb88e88
author
Dami <dami.etoile@gmail.com>
2 files changed, +67 -4
Mtinygit/journal.go8++--
Atinygit/journal_test.go63++++++++++++++++++++++++++++++
M tinygit/journal.go +4 -4
A tinygit/journal_test.go +63 -0
@@ -0,0 +1,63 @@
1+package tinygit2+3+import (4+ "context"5+ "errors"6+ "os"7+ "path/filepath"8+ "strings"9+ "testing"10+11+ "github.com/FelineStateMachine/tinyrelay/protocol/nostr"12+)13+14+type unavailableMetadataStore struct {15+ Store16+ err error17+}18+19+func (s unavailableMetadataStore) Latest(context.Context, int, string, string) (Event, bool, error) {20+ return Event{}, false, s.err21+}22+23+func TestJournalRecoveryPreservesIntentWhenStorageReadFails(t *testing.T) {24+ ctx := context.Background()25+ root := t.TempDir()26+ store, err := OpenStore(ctx, filepath.Join(root, "events.db"))27+ if err != nil {28+ t.Fatal(err)29+ }30+ t.Cleanup(func() { _ = store.Close() })31+ g, err := New(Config{Store: store, Root: filepath.Join(root, "git")})32+ if err != nil {33+ t.Fatal(err)34+ }35+ state := Event{Kind: 30618, CreatedAt: 1, Tags: [][]string{{"d", "demo"}, {"refs/heads/main", strings.Repeat("c", 40)}}}36+ if err := nostr.Sign(&state, strings.Repeat("0", 63)+"1"); err != nil {37+ t.Fatal(err)38+ }39+ if err := store.Save(ctx, state, 1); err != nil {40+ t.Fatal(err)41+ }42+ repo := Repository{Owner: state.PubKey, Identifier: "demo", Refs: map[string]string{"refs/heads/main": strings.Repeat("c", 40)}}43+ journal := journalRecord{Repository: key(repo.Owner, repo.Identifier), EventID: state.ID, Kind: 30618, Refs: repo.Refs}44+ if err := g.writeJournal(repo, journal); err != nil {45+ t.Fatal(err)46+ }47+ unavailable := errors.New("metadata temporarily unavailable")48+ g.store = unavailableMetadataStore{Store: store, err: unavailable}49+ if err := g.recoverJournals(); !errors.Is(err, unavailable) {50+ t.Errorf("recovery error=%v; want storage failure", err)51+ }52+ if _, err := os.Stat(g.journalPath(journal)); err != nil {53+ t.Fatalf("recovery discarded the journal after a read failure: %v", err)54+ }55+ g.store = store56+ if err := g.recoverJournals(); err != nil {57+ t.Fatal(err)58+ }59+ pending, err := os.ReadFile(filepath.Join(g.repoPath(repo), "tinyrelay.pending"))60+ if err != nil || !strings.Contains(string(pending), "refs/heads/main "+strings.Repeat("c", 40)) {61+ t.Fatalf("retry did not recover pending state: %q, %v", pending, err)62+ }63+}