1package db
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "testing"
8
9 "github.com/stretchr/testify/require"
10)
11
12func TestConnect_SharesConnectionForSameDataDir(t *testing.T) {
13 t.Cleanup(ResetPool)
14
15 dataDir := t.TempDir()
16
17 conn1, err := Connect(context.Background(), dataDir)
18 require.NoError(t, err)
19
20 conn2, err := Connect(context.Background(), dataDir)
21 require.NoError(t, err)
22
23 require.Same(t, conn1, conn2, "should return the same *sql.DB for the same data dir")
24
25 // Releasing once should not close the connection.
26 require.NoError(t, Release(dataDir))
27 require.NoError(t, conn1.PingContext(context.Background()), "connection should still be usable after partial release")
28
29 // Releasing again should close it.
30 require.NoError(t, Release(dataDir))
31 require.Error(t, conn1.PingContext(context.Background()), "connection should be closed after final release")
32}
33
34func TestConnect_SeparateConnectionsForDifferentDataDirs(t *testing.T) {
35 t.Cleanup(ResetPool)
36
37 dir1 := t.TempDir()
38 dir2 := t.TempDir()
39
40 conn1, err := Connect(context.Background(), dir1)
41 require.NoError(t, err)
42
43 conn2, err := Connect(context.Background(), dir2)
44 require.NoError(t, err)
45
46 require.NotSame(t, conn1, conn2, "different data dirs should get different connections")
47
48 require.NoError(t, Release(dir1))
49 require.NoError(t, Release(dir2))
50}
51
52func TestRelease_NoopForUnknownDataDir(t *testing.T) {
53 t.Cleanup(ResetPool)
54
55 require.NoError(t, Release("/nonexistent/path"), "releasing unknown data dir should not error")
56}
57
58// TestConnect_FailsWhenDataDirLocked simulates a second crush process by
59// taking the data-dir lock directly via the OS primitive on a separate
60// file descriptor and then asserting that Connect surfaces a clean
61// ErrDataDirLocked instead of opening the database under contention.
62func TestConnect_FailsWhenDataDirLocked(t *testing.T) {
63 t.Cleanup(ResetPool)
64
65 dataDir := t.TempDir()
66 lockPath := filepath.Join(dataDir, dataDirLockFile)
67
68 release, err := tryFileLock(lockPath)
69 require.NoError(t, err, "expected to take the data-dir lock for the first time")
70 t.Cleanup(release)
71
72 _, err = Connect(context.Background(), dataDir, WithDataDirLock(true))
73 require.Error(t, err, "Connect must refuse to open a locked data dir")
74 require.ErrorIs(t, err, ErrDataDirLocked)
75}
76
77// TestConnect_SucceedsAfterContenderReleases ensures the lock is purely
78// advisory and that a clean release lets the next Connect proceed.
79func TestConnect_SucceedsAfterContenderReleases(t *testing.T) {
80 t.Cleanup(ResetPool)
81
82 dataDir := t.TempDir()
83 lockPath := filepath.Join(dataDir, dataDirLockFile)
84
85 release, err := tryFileLock(lockPath)
86 require.NoError(t, err)
87
88 _, err = Connect(context.Background(), dataDir, WithDataDirLock(true))
89 require.ErrorIs(t, err, ErrDataDirLocked)
90
91 release()
92
93 conn, err := Connect(context.Background(), dataDir, WithDataDirLock(true))
94 require.NoError(t, err, "Connect should succeed once the contender releases the lock")
95 require.NoError(t, conn.PingContext(context.Background()))
96 require.NoError(t, Release(dataDir))
97}
98
99// TestConnect_LockReleasedOnFinalRelease confirms that closing the last
100// reference to a pool entry also drops the OS lock, so subsequent
101// processes can take the data dir.
102func TestConnect_LockReleasedOnFinalRelease(t *testing.T) {
103 t.Cleanup(ResetPool)
104
105 dataDir := t.TempDir()
106 lockPath := filepath.Join(dataDir, dataDirLockFile)
107
108 conn, err := Connect(context.Background(), dataDir, WithDataDirLock(true))
109 require.NoError(t, err)
110 require.NoError(t, conn.PingContext(context.Background()))
111
112 // Holding the in-process entry must keep the OS lock held so a
113 // "second process" (simulated by a fresh tryFileLock call) is
114 // rejected.
115 _, lockErr := tryFileLock(lockPath)
116 require.Error(t, lockErr)
117 require.True(t, errors.Is(lockErr, errLockContended), "expected contended lock while pool entry is live")
118
119 require.NoError(t, Release(dataDir))
120
121 // After the final release the lock is free again.
122 release, err := tryFileLock(lockPath)
123 require.NoError(t, err, "expected lock to be released after final Release")
124 release()
125}
126
127// TestConnect_SharedPoolDoesNotReacquireLock makes sure that subsequent
128// in-process Connect calls reuse the existing OS lock through refcount,
129// not by re-acquiring it. The simplest observable signal of correctness
130// is that the second Connect does not error and the lock is still held
131// after a single Release.
132func TestConnect_SharedPoolDoesNotReacquireLock(t *testing.T) {
133 t.Cleanup(ResetPool)
134
135 dataDir := t.TempDir()
136 lockPath := filepath.Join(dataDir, dataDirLockFile)
137
138 _, err := Connect(context.Background(), dataDir, WithDataDirLock(true))
139 require.NoError(t, err)
140
141 _, err = Connect(context.Background(), dataDir, WithDataDirLock(true))
142 require.NoError(t, err)
143
144 // Drop one reference; lock must still be held.
145 require.NoError(t, Release(dataDir))
146 _, lockErr := tryFileLock(lockPath)
147 require.ErrorIs(t, lockErr, errLockContended)
148
149 require.NoError(t, Release(dataDir))
150}
151
152// TestConnect_SkipLockEnvBypassesAcquisition exercises the escape
153// hatch used by users on filesystems where flock is unreliable.
154func TestConnect_SkipLockEnvBypassesAcquisition(t *testing.T) {
155 t.Cleanup(ResetPool)
156
157 dataDir := t.TempDir()
158 lockPath := filepath.Join(dataDir, dataDirLockFile)
159
160 release, err := tryFileLock(lockPath)
161 require.NoError(t, err)
162 t.Cleanup(release)
163
164 t.Setenv("CRUSH_SKIP_DATADIR_LOCK", "1")
165
166 conn, err := Connect(context.Background(), dataDir, WithDataDirLock(true))
167 require.NoError(t, err, "skip-lock env should bypass contention")
168 require.NoError(t, conn.PingContext(context.Background()))
169 require.NoError(t, Release(dataDir))
170}
171
172// TestConnect_DefaultIgnoresContendedLock confirms that without
173// WithDataDirLock(true) the lock file is irrelevant: a contender can
174// hold tryFileLock and Connect still succeeds. This pins the
175// local-mode default to its pre-lock behavior.
176func TestConnect_DefaultIgnoresContendedLock(t *testing.T) {
177 t.Cleanup(ResetPool)
178
179 dataDir := t.TempDir()
180 lockPath := filepath.Join(dataDir, dataDirLockFile)
181
182 release, err := tryFileLock(lockPath)
183 require.NoError(t, err, "expected to take the data-dir lock for the first time")
184 t.Cleanup(release)
185
186 conn, err := Connect(context.Background(), dataDir)
187 require.NoError(t, err, "default Connect must not take the lock and must succeed under contention")
188 require.NoError(t, conn.PingContext(context.Background()))
189 require.NoError(t, Release(dataDir))
190}
191
192// TestConnect_ServerPathFailsWhenDataDirLocked is the server's
193// workspace-bootstrap analogue of TestConnect_FailsWhenDataDirLocked:
194// passing WithDataDirLock(true) must surface ErrDataDirLocked when a
195// contender already holds the lock.
196func TestConnect_ServerPathFailsWhenDataDirLocked(t *testing.T) {
197 t.Cleanup(ResetPool)
198
199 dataDir := t.TempDir()
200 lockPath := filepath.Join(dataDir, dataDirLockFile)
201
202 release, err := tryFileLock(lockPath)
203 require.NoError(t, err, "expected to take the data-dir lock for the first time")
204 t.Cleanup(release)
205
206 _, err = Connect(context.Background(), dataDir, WithDataDirLock(true))
207 require.Error(t, err, "server-path Connect must refuse to open a locked data dir")
208 require.ErrorIs(t, err, ErrDataDirLocked)
209}