connect_test.go

  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}