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>
| M | tinygit/ | 8 | ++-- |
| A | tinygit/ | 63 | ++++++++++++++++++++++++++++++ |
@@ -96,15 +96,15 @@ func (g *GitRelay) recoverJournals() error {
9696 keepJournal := false9797 if jr.Kind == 30618 {9898 current, found, err := g.store.Latest(context.Background(), 30618, owner, id)99- if err != nil || !found || current.ID != jr.EventID {99+ if err != nil {100+ return err101+ }102+ if !found || current.ID != jr.EventID {100103 if removeErr := os.Remove(filepath.Join(g.journalDir(), entry.Name())); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {101104 return removeErr102105 }103106 continue104107 }105- if err != nil {106- return err107- }108108 if g.stateObjectsPresent(context.Background(), r) {109109 err = g.writeState(r)110110 } else {@@ -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+}