Skip to content

Commit 2782a9d

Browse files
corylanouclaude
andcommitted
feat(directory-watcher): add validation and test improvements
- Add validation: return error when no databases found in directory without watch enabled - Split EmptyDirectory test to validate both watch enabled/disabled scenarios - Add test for recursive mode detecting nested databases - Fix race condition in Store.Close() by cloning dbs slice while holding lock 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 5c6ad60 commit 2782a9d

File tree

3 files changed

+69
-2
lines changed

3 files changed

+69
-2
lines changed

cmd/litestream/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,10 @@ func NewDBsFromDirectoryConfig(dbc *DBConfig) ([]*litestream.DB, error) {
594594
return nil, fmt.Errorf("failed to scan directory %s: %w", dirPath, err)
595595
}
596596

597+
if len(dbPaths) == 0 && !dbc.Watch {
598+
return nil, fmt.Errorf("no SQLite databases found in directory %s with pattern %s", dirPath, dbc.Pattern)
599+
}
600+
597601
// Create DB instances for each found database
598602
var dbs []*litestream.DB
599603
for _, dbPath := range dbPaths {

cmd/litestream/main_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1809,7 +1809,7 @@ func TestNewDBsFromDirectoryConfig_ReplicasArray(t *testing.T) {
18091809
}
18101810
}
18111811

1812-
func TestNewDBsFromDirectoryConfig_EmptyDirectory(t *testing.T) {
1812+
func TestNewDBsFromDirectoryConfig_EmptyDirectoryRequiresDatabases(t *testing.T) {
18131813
tmpDir := t.TempDir()
18141814
replicaDir := filepath.Join(tmpDir, "replica")
18151815

@@ -1819,6 +1819,22 @@ func TestNewDBsFromDirectoryConfig_EmptyDirectory(t *testing.T) {
18191819
Replica: &main.ReplicaConfig{Type: "file", Path: replicaDir},
18201820
}
18211821

1822+
if _, err := main.NewDBsFromDirectoryConfig(config); err == nil {
1823+
t.Fatalf("expected error for empty directory when watch disabled")
1824+
}
1825+
}
1826+
1827+
func TestNewDBsFromDirectoryConfig_EmptyDirectoryWithWatch(t *testing.T) {
1828+
tmpDir := t.TempDir()
1829+
replicaDir := filepath.Join(tmpDir, "replica")
1830+
1831+
config := &main.DBConfig{
1832+
Dir: tmpDir,
1833+
Pattern: "*.db",
1834+
Watch: true,
1835+
Replica: &main.ReplicaConfig{Type: "file", Path: replicaDir},
1836+
}
1837+
18221838
dbs, err := main.NewDBsFromDirectoryConfig(config)
18231839
if err != nil {
18241840
t.Fatalf("unexpected error: %v", err)
@@ -1883,6 +1899,49 @@ func TestDirectoryMonitor_DetectsDatabaseLifecycle(t *testing.T) {
18831899
}
18841900
}
18851901

1902+
func TestDirectoryMonitor_RecursiveDetectsNestedDatabases(t *testing.T) {
1903+
ctx := context.Background()
1904+
rootDir := t.TempDir()
1905+
replicaDir := filepath.Join(t.TempDir(), "replicas")
1906+
1907+
config := &main.DBConfig{
1908+
Dir: rootDir,
1909+
Pattern: "*.db",
1910+
Recursive: true,
1911+
Watch: true,
1912+
Replica: &main.ReplicaConfig{Type: "file", Path: replicaDir},
1913+
}
1914+
1915+
storeConfig := main.DefaultConfig()
1916+
store := litestream.NewStore(nil, storeConfig.CompactionLevels())
1917+
store.CompactionMonitorEnabled = false
1918+
if err := store.Open(ctx); err != nil {
1919+
t.Fatalf("unexpected error opening store: %v", err)
1920+
}
1921+
defer func() {
1922+
if err := store.Close(context.Background()); err != nil {
1923+
t.Fatalf("unexpected error closing store: %v", err)
1924+
}
1925+
}()
1926+
1927+
monitor, err := main.NewDirectoryMonitor(ctx, store, config, nil)
1928+
if err != nil {
1929+
t.Fatalf("failed to initialize directory monitor: %v", err)
1930+
}
1931+
defer monitor.Close()
1932+
1933+
deepDir := filepath.Join(rootDir, "tenant", "nested", "deeper")
1934+
if err := os.MkdirAll(deepDir, 0755); err != nil {
1935+
t.Fatalf("failed to create nested directories: %v", err)
1936+
}
1937+
deepDB := filepath.Join(deepDir, "deep.db")
1938+
createSQLiteDB(t, deepDB)
1939+
1940+
if !waitForCondition(5*time.Second, func() bool { return hasDBPath(store.DBs(), deepDB) }) {
1941+
t.Fatalf("expected nested database %s to be detected", deepDB)
1942+
}
1943+
}
1944+
18861945
// createSQLiteDB creates a minimal SQLite database file for testing
18871946
func createSQLiteDB(t *testing.T, path string) {
18881947
t.Helper()

store.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ func (s *Store) Open(ctx context.Context) error {
135135
}
136136

137137
func (s *Store) Close(ctx context.Context) (err error) {
138-
for _, db := range s.dbs {
138+
s.mu.Lock()
139+
dbs := slices.Clone(s.dbs)
140+
s.mu.Unlock()
141+
142+
for _, db := range dbs {
139143
if e := db.Close(ctx); e != nil && err == nil {
140144
err = e
141145
}

0 commit comments

Comments
 (0)