feat(testing): add full integration test suite
Some checks failed
Integration Tests / integration-test (push) Failing after -23m59s
Some checks failed
Integration Tests / integration-test (push) Failing after -23m59s
This commit introduces a comprehensive integration test suite for the pgsql-broker. The test suite includes: - A Docker/Podman environment for running a PostgreSQL database, managed via a . - Integration tests that cover the broker's lifecycle, including job creation, execution, and instance management. - A GitHub Actions workflow to automate the execution of all tests on push and pull requests. - A dedicated test configuration file () and helper test files. refactor(worker): fix job processing transaction - The worker's job processing now uses a single transaction to fetch and run a job, resolving a race condition where jobs were not in the 'running' state when being executed. - The broker's database instance registration is now more robust, handling cases where another instance is already active. The Makefile has been significantly updated to orchestrate the entire test flow, including setting up the database, starting/stopping the broker, and running unit and integration tests separately.
This commit is contained in:
@@ -2,6 +2,7 @@ package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql" // Import sql package
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -133,22 +134,68 @@ func (i *DatabaseInstance) Stop() error {
|
||||
func (i *DatabaseInstance) registerInstance() error {
|
||||
var retval int
|
||||
var errmsg string
|
||||
var instanceID int64
|
||||
var nullableInstanceID sql.NullInt64 // Change to nullable type
|
||||
|
||||
i.logger.Debug("registering instance", "name", i.Name, "hostname", i.Hostname, "pid", i.PID, "version", i.Version, "queue_count", i.dbConfig.QueueCount)
|
||||
err := i.db.QueryRow(i.ctx,
|
||||
"SELECT p_retval, p_errmsg, p_instance_id FROM broker_register_instance($1, $2, $3, $4, $5)",
|
||||
i.Name, i.Hostname, i.PID, i.Version, i.dbConfig.QueueCount,
|
||||
).Scan(&retval, &errmsg, &instanceID)
|
||||
).Scan(&retval, &errmsg, &nullableInstanceID)
|
||||
|
||||
if err != nil {
|
||||
i.logger.Error("query error during instance registration", "error", err)
|
||||
return fmt.Errorf("query error: %w", err)
|
||||
}
|
||||
|
||||
if retval > 0 {
|
||||
if retval == 3 {
|
||||
i.logger.Warn("another broker instance is already active, attempting to retrieve ID", "error", errmsg)
|
||||
// Try to retrieve the ID of the active instance
|
||||
var activeID int64
|
||||
err := i.db.QueryRow(i.ctx,
|
||||
"SELECT id_broker_queueinstance FROM broker_queueinstance WHERE name = $1 AND hostname = $2 AND status = 'active' ORDER BY started_at DESC LIMIT 1",
|
||||
i.Name, i.Hostname,
|
||||
).Scan(&activeID)
|
||||
if err != nil {
|
||||
i.logger.Error("failed to retrieve ID of active instance", "error", err)
|
||||
return fmt.Errorf("failed to retrieve ID of active instance: %w", err)
|
||||
}
|
||||
i.ID = activeID
|
||||
i.logger.Info("retrieved active instance ID", "id", i.ID)
|
||||
return nil
|
||||
} else if retval > 0 {
|
||||
i.logger.Error("broker_register_instance error", "retval", retval, "errmsg", errmsg)
|
||||
return fmt.Errorf("broker_register_instance error: %s", errmsg)
|
||||
}
|
||||
|
||||
i.ID = instanceID
|
||||
// If successfully registered, nullableInstanceID.Valid will be true
|
||||
if nullableInstanceID.Valid {
|
||||
i.ID = nullableInstanceID.Int64
|
||||
i.logger.Info("registered new instance", "id", i.ID)
|
||||
|
||||
// Debug logging: Retrieve all entries from broker_queueinstance
|
||||
rows, err := i.db.Query(i.ctx, "SELECT id_broker_queueinstance, name, hostname, status FROM broker_queueinstance")
|
||||
if err != nil {
|
||||
i.logger.Error("debug query failed", "error", err)
|
||||
} else {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var name, hostname, status string
|
||||
if err := rows.Scan(&id, &name, &hostname, &status); err != nil {
|
||||
i.logger.Error("debug scan failed", "error", err)
|
||||
break
|
||||
}
|
||||
i.logger.Debug("broker_queueinstance entry", "id", id, "name", name, "hostname", hostname, "status", status)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This case should ideally not happen if retval is 0 (success)
|
||||
// but if it does, it means p_instance_id was NULL despite success.
|
||||
// This would be an unexpected scenario.
|
||||
i.logger.Error("broker_register_instance returned success but no instance ID", "retval", retval, "errmsg", errmsg)
|
||||
return fmt.Errorf("broker_register_instance returned success but no instance ID")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -323,4 +370,4 @@ func (i *DatabaseInstance) GetStats() map[string]interface{} {
|
||||
stats["queues"] = queueStats
|
||||
|
||||
return stats
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
\echo 'Installing PostgreSQL Broker tables...'
|
||||
|
||||
\i 01_broker_queueinstance.sql
|
||||
\i 03_broker_schedule.sql
|
||||
\i 02_broker_jobs.sql
|
||||
\i 02_broker_schedule.sql
|
||||
\i 03_broker_jobs.sql
|
||||
|
||||
\echo 'PostgreSQL Broker tables installed successfully!'
|
||||
\echo 'PostgreSQL Broker tables installed successfully!'
|
||||
@@ -2,6 +2,7 @@ package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql" // Import sql package
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -145,50 +146,58 @@ func (w *Worker) processLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// processJobs processes jobs from the queue
|
||||
// processJobs processes jobs from the queue within a transaction
|
||||
func (w *Worker) processJobs(ctx context.Context, specificJob *models.Job) {
|
||||
defer w.recoverPanic()
|
||||
|
||||
for i := 0; i < w.fetchSize; i++ {
|
||||
|
||||
tx, err := w.db.Begin(ctx) // Start transaction
|
||||
if err != nil {
|
||||
w.logger.Error("failed to begin transaction", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
var jobID int64
|
||||
|
||||
if specificJob != nil && specificJob.ID > 0 {
|
||||
jobID = specificJob.ID
|
||||
specificJob = nil // Only process once
|
||||
} else {
|
||||
// Fetch next job from database
|
||||
var err error
|
||||
jobID, err = w.fetchNextJob(ctx)
|
||||
jobID, err = w.fetchNextJobTx(ctx, tx) // Use transaction
|
||||
if err != nil {
|
||||
tx.Rollback() // Rollback on fetch error
|
||||
w.logger.Error("failed to fetch job", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if jobID <= 0 {
|
||||
// No more jobs
|
||||
return
|
||||
tx.Rollback() // No job found, rollback
|
||||
return // No more jobs
|
||||
}
|
||||
|
||||
// Run the job
|
||||
if err := w.runJob(ctx, jobID); err != nil {
|
||||
if err := w.runJobTx(ctx, tx, jobID); err != nil { // Use transaction
|
||||
tx.Rollback() // Rollback on job execution error
|
||||
w.logger.Error("failed to run job", "job_id", jobID, "error", err)
|
||||
} else {
|
||||
tx.Commit() // Commit if job successful
|
||||
w.jobsHandled++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetchNextJob fetches the next job from the queue
|
||||
func (w *Worker) fetchNextJob(ctx context.Context) (int64, error) {
|
||||
// fetchNextJobTx fetches the next job from the queue within a transaction
|
||||
func (w *Worker) fetchNextJobTx(ctx context.Context, tx adapter.DBTransaction) (int64, error) {
|
||||
var retval int
|
||||
var errmsg string
|
||||
var jobID int64
|
||||
var nullableJobID sql.NullInt64
|
||||
|
||||
err := w.db.QueryRow(ctx,
|
||||
err := tx.QueryRow(ctx,
|
||||
"SELECT p_retval, p_errmsg, p_job_id FROM broker_get($1, $2)",
|
||||
w.QueueNumber, w.InstanceID,
|
||||
).Scan(&retval, &errmsg, &jobID)
|
||||
).Scan(&retval, &errmsg, &nullableJobID)
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("query error: %w", err)
|
||||
@@ -198,17 +207,21 @@ func (w *Worker) fetchNextJob(ctx context.Context) (int64, error) {
|
||||
return 0, fmt.Errorf("broker_get error: %s", errmsg)
|
||||
}
|
||||
|
||||
return jobID, nil
|
||||
if !nullableJobID.Valid {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return nullableJobID.Int64, nil
|
||||
}
|
||||
|
||||
// runJob executes a job
|
||||
func (w *Worker) runJob(ctx context.Context, jobID int64) error {
|
||||
// runJobTx executes a job within a transaction
|
||||
func (w *Worker) runJobTx(ctx context.Context, tx adapter.DBTransaction, jobID int64) error {
|
||||
w.logger.Debug("running job", "job_id", jobID)
|
||||
|
||||
var retval int
|
||||
var errmsg string
|
||||
|
||||
err := w.db.QueryRow(ctx,
|
||||
err := tx.QueryRow(ctx,
|
||||
"SELECT p_retval, p_errmsg FROM broker_run($1)",
|
||||
jobID,
|
||||
).Scan(&retval, &errmsg)
|
||||
|
||||
Reference in New Issue
Block a user