fix: resolve data races in store tests with atomic operations
Fix all data races detected by -race flag in parallel store tests. Problems fixed: 1. TestMain not propagating exit code - Was: m.Run(); return - Now: os.Exit(m.Run()) 2. Data races on global DSN variables - mysqlBaseDSN and postgresBaseDSN written in sync.Once - Read outside Once without synchronization - Race detector: write/read without happens-before relationship 3. Data races on container pointers - mysqlContainer, postgresContainer, testDockerNetwork - Same pattern: write in Once, read in cleanup Solution: Use atomic operations - atomic.Value for DSN strings (Store/Load) - atomic.Pointer for container pointers (Store/Load) - Provides proper memory synchronization - Race-free reads and writes Why sync.Once alone wasn't enough: - sync.Once guarantees function runs once - Does NOT provide memory barrier for variables written inside - Reads outside Once have no synchronization with writes - Race detector correctly flags this as violation Technical details: - atomic.Value.Store() provides release semantics - atomic.Value.Load() provides acquire semantics - Guarantees happens-before relationship per Go memory model - All 159 parallel tests can safely access globals Impact: - Tests now pass with -race flag - No performance degradation (atomic ops are fast) - Maintains parallel execution benefits (8-10x speedup) - Proper Go memory model compliance Related: Issues #2, #3 from race analysis
This commit is contained in:
parent
e082adf7b6
commit
db57f4456a
2 changed files with 34 additions and 31 deletions
|
|
@ -42,17 +42,17 @@ var (
|
||||||
wait.ForListeningPort("5230/tcp"),
|
wait.ForListeningPort("5230/tcp"),
|
||||||
).WithDeadline(180 * time.Second)
|
).WithDeadline(180 * time.Second)
|
||||||
|
|
||||||
mysqlContainer *mysql.MySQLContainer
|
mysqlContainer atomic.Pointer[mysql.MySQLContainer]
|
||||||
postgresContainer *postgres.PostgresContainer
|
postgresContainer atomic.Pointer[postgres.PostgresContainer]
|
||||||
mysqlOnce sync.Once
|
mysqlOnce sync.Once
|
||||||
postgresOnce sync.Once
|
postgresOnce sync.Once
|
||||||
mysqlBaseDSN string
|
mysqlBaseDSN atomic.Value // stores string
|
||||||
postgresBaseDSN string
|
postgresBaseDSN atomic.Value // stores string
|
||||||
dbCounter atomic.Int64
|
dbCounter atomic.Int64
|
||||||
dbCreationMutex sync.Mutex // Protects database creation operations
|
dbCreationMutex sync.Mutex // Protects database creation operations
|
||||||
|
|
||||||
// Network for container communication.
|
// Network for container communication.
|
||||||
testDockerNetwork *testcontainers.DockerNetwork
|
testDockerNetwork atomic.Pointer[testcontainers.DockerNetwork]
|
||||||
testNetworkOnce sync.Once
|
testNetworkOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -65,9 +65,9 @@ func getTestNetwork(ctx context.Context) (*testcontainers.DockerNetwork, error)
|
||||||
networkErr = err
|
networkErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
testDockerNetwork = nw
|
testDockerNetwork.Store(nw)
|
||||||
})
|
})
|
||||||
return testDockerNetwork, networkErr
|
return testDockerNetwork.Load(), networkErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMySQLDSN starts a MySQL container (if not already running) and creates a fresh database for this test.
|
// GetMySQLDSN starts a MySQL container (if not already running) and creates a fresh database for this test.
|
||||||
|
|
@ -99,7 +99,7 @@ func GetMySQLDSN(t *testing.T) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to start MySQL container: %v", err)
|
t.Fatalf("failed to start MySQL container: %v", err)
|
||||||
}
|
}
|
||||||
mysqlContainer = container
|
mysqlContainer.Store(container)
|
||||||
|
|
||||||
dsn, err := container.ConnectionString(ctx, "multiStatements=true")
|
dsn, err := container.ConnectionString(ctx, "multiStatements=true")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -110,10 +110,11 @@ func GetMySQLDSN(t *testing.T) string {
|
||||||
t.Fatalf("MySQL not ready for connections: %v", err)
|
t.Fatalf("MySQL not ready for connections: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mysqlBaseDSN = dsn
|
mysqlBaseDSN.Store(dsn)
|
||||||
})
|
})
|
||||||
|
|
||||||
if mysqlBaseDSN == "" {
|
dsn, ok := mysqlBaseDSN.Load().(string)
|
||||||
|
if !ok || dsn == "" {
|
||||||
t.Fatal("MySQL container failed to start in a previous test")
|
t.Fatal("MySQL container failed to start in a previous test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,7 +124,7 @@ func GetMySQLDSN(t *testing.T) string {
|
||||||
|
|
||||||
// Create a fresh database for this test
|
// Create a fresh database for this test
|
||||||
dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1))
|
dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1))
|
||||||
db, err := sql.Open("mysql", mysqlBaseDSN)
|
db, err := sql.Open("mysql", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to connect to MySQL: %v", err)
|
t.Fatalf("failed to connect to MySQL: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +135,7 @@ func GetMySQLDSN(t *testing.T) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return DSN pointing to the new database
|
// Return DSN pointing to the new database
|
||||||
return strings.Replace(mysqlBaseDSN, "/init_db?", "/"+dbName+"?", 1)
|
return strings.Replace(dsn, "/init_db?", "/"+dbName+"?", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForDB polls the database until it's ready or timeout is reached.
|
// waitForDB polls the database until it's ready or timeout is reached.
|
||||||
|
|
@ -195,7 +196,7 @@ func GetPostgresDSN(t *testing.T) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to start PostgreSQL container: %v", err)
|
t.Fatalf("failed to start PostgreSQL container: %v", err)
|
||||||
}
|
}
|
||||||
postgresContainer = container
|
postgresContainer.Store(container)
|
||||||
|
|
||||||
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
|
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -206,10 +207,11 @@ func GetPostgresDSN(t *testing.T) string {
|
||||||
t.Fatalf("PostgreSQL not ready for connections: %v", err)
|
t.Fatalf("PostgreSQL not ready for connections: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
postgresBaseDSN = dsn
|
postgresBaseDSN.Store(dsn)
|
||||||
})
|
})
|
||||||
|
|
||||||
if postgresBaseDSN == "" {
|
dsn, ok := postgresBaseDSN.Load().(string)
|
||||||
|
if !ok || dsn == "" {
|
||||||
t.Fatal("PostgreSQL container failed to start in a previous test")
|
t.Fatal("PostgreSQL container failed to start in a previous test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +221,7 @@ func GetPostgresDSN(t *testing.T) string {
|
||||||
|
|
||||||
// Create a fresh database for this test
|
// Create a fresh database for this test
|
||||||
dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1))
|
dbName := fmt.Sprintf("memos_test_%d", dbCounter.Add(1))
|
||||||
db, err := sql.Open("postgres", postgresBaseDSN)
|
db, err := sql.Open("postgres", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to connect to PostgreSQL: %v", err)
|
t.Fatalf("failed to connect to PostgreSQL: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +232,7 @@ func GetPostgresDSN(t *testing.T) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return DSN pointing to the new database
|
// Return DSN pointing to the new database
|
||||||
return strings.Replace(postgresBaseDSN, "/init_db?", "/"+dbName+"?", 1)
|
return strings.Replace(dsn, "/init_db?", "/"+dbName+"?", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDedicatedMySQLDSN starts a dedicated MySQL container for migration testing.
|
// GetDedicatedMySQLDSN starts a dedicated MySQL container for migration testing.
|
||||||
|
|
@ -336,33 +338,35 @@ func GetDedicatedPostgresDSN(t *testing.T) (dsn string, containerHost string, cl
|
||||||
// This is typically called from TestMain.
|
// This is typically called from TestMain.
|
||||||
func TerminateContainers() {
|
func TerminateContainers() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if mysqlContainer != nil {
|
if container := mysqlContainer.Load(); container != nil {
|
||||||
_ = mysqlContainer.Terminate(ctx)
|
_ = container.Terminate(ctx)
|
||||||
}
|
}
|
||||||
if postgresContainer != nil {
|
if container := postgresContainer.Load(); container != nil {
|
||||||
_ = postgresContainer.Terminate(ctx)
|
_ = container.Terminate(ctx)
|
||||||
}
|
}
|
||||||
if testDockerNetwork != nil {
|
if network := testDockerNetwork.Load(); network != nil {
|
||||||
_ = testDockerNetwork.Remove(ctx)
|
_ = network.Remove(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMySQLContainerHost returns the MySQL container hostname for use within the Docker network.
|
// GetMySQLContainerHost returns the MySQL container hostname for use within the Docker network.
|
||||||
func GetMySQLContainerHost() string {
|
func GetMySQLContainerHost() string {
|
||||||
if mysqlContainer == nil {
|
container := mysqlContainer.Load()
|
||||||
|
if container == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
name, _ := mysqlContainer.Name(context.Background())
|
name, _ := container.Name(context.Background())
|
||||||
// Remove leading slash from container name
|
// Remove leading slash from container name
|
||||||
return strings.TrimPrefix(name, "/")
|
return strings.TrimPrefix(name, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPostgresContainerHost returns the PostgreSQL container hostname for use within the Docker network.
|
// GetPostgresContainerHost returns the PostgreSQL container hostname for use within the Docker network.
|
||||||
func GetPostgresContainerHost() string {
|
func GetPostgresContainerHost() string {
|
||||||
if postgresContainer == nil {
|
container := postgresContainer.Load()
|
||||||
|
if container == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
name, _ := postgresContainer.Name(context.Background())
|
name, _ := container.Name(context.Background())
|
||||||
return strings.TrimPrefix(name, "/")
|
return strings.TrimPrefix(name, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -395,11 +399,11 @@ func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcon
|
||||||
case "mysql":
|
case "mysql":
|
||||||
env["MEMOS_DRIVER"] = "mysql"
|
env["MEMOS_DRIVER"] = "mysql"
|
||||||
env["MEMOS_DSN"] = cfg.DSN
|
env["MEMOS_DSN"] = cfg.DSN
|
||||||
opts = append(opts, network.WithNetwork(nil, testDockerNetwork))
|
opts = append(opts, network.WithNetwork(nil, testDockerNetwork.Load()))
|
||||||
case "postgres":
|
case "postgres":
|
||||||
env["MEMOS_DRIVER"] = "postgres"
|
env["MEMOS_DRIVER"] = "postgres"
|
||||||
env["MEMOS_DSN"] = cfg.DSN
|
env["MEMOS_DSN"] = cfg.DSN
|
||||||
opts = append(opts, network.WithNetwork(nil, testDockerNetwork))
|
opts = append(opts, network.WithNetwork(nil, testDockerNetwork.Load()))
|
||||||
default:
|
default:
|
||||||
return nil, errors.Errorf("unsupported driver: %s", cfg.Driver)
|
return nil, errors.Errorf("unsupported driver: %s", cfg.Driver)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ func TestMain(m *testing.M) {
|
||||||
// If DRIVER is set, run tests for that driver only
|
// If DRIVER is set, run tests for that driver only
|
||||||
if os.Getenv("DRIVER") != "" {
|
if os.Getenv("DRIVER") != "" {
|
||||||
defer TerminateContainers()
|
defer TerminateContainers()
|
||||||
m.Run()
|
os.Exit(m.Run())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No DRIVER set - run tests for all drivers sequentially
|
// No DRIVER set - run tests for all drivers sequentially
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue