Skip to content

Commit 385e91a

Browse files
committed
feat: make Connect method idempotent across multiple extensions
1 parent 4734f23 commit 385e91a

15 files changed

Lines changed: 99 additions & 37 deletions

extensions/grpc/server.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ func (s *grpcServer) RegisterService(desc *grpc.ServiceDesc, impl interface{}) e
5252
return nil
5353
}
5454

55+
// Start is idempotent — calling it on an already-running server is a no-op.
5556
func (s *grpcServer) Start(ctx context.Context, addr string) error {
5657
// Check if already running
5758
s.mu.RLock()
5859
if s.running {
5960
s.mu.RUnlock()
60-
return ErrAlreadyStarted
61+
return nil
6162
}
6263
s.mu.RUnlock()
6364

@@ -70,7 +71,7 @@ func (s *grpcServer) Start(ctx context.Context, addr string) error {
7071

7172
// Double-check running status after acquiring write lock
7273
if s.running {
73-
return ErrAlreadyStarted
74+
return nil
7475
}
7576

7677
// Create server with options

extensions/mqtt/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (c *mqttClient) Connect(ctx context.Context) error {
4646
defer c.mu.Unlock()
4747

4848
if c.client != nil && c.client.IsConnected() {
49-
return fmt.Errorf("already connected")
49+
return nil
5050
}
5151

5252
// Create client options

extensions/mqtt/extension_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@ package mqtt
22

33
import (
44
"context"
5+
"net"
56
"testing"
7+
"time"
68

79
"github.com/xraph/confy"
810
"github.com/xraph/forge"
911
"github.com/xraph/forge/internal/logger"
1012
)
1113

14+
// requireBroker skips the test if no MQTT broker is reachable at localhost:1883.
15+
func requireBroker(t *testing.T) {
16+
t.Helper()
17+
conn, err := net.DialTimeout("tcp", "localhost:1883", 500*time.Millisecond)
18+
if err != nil {
19+
t.Skip("skipping: MQTT broker not available at localhost:1883")
20+
}
21+
conn.Close()
22+
}
23+
1224
func TestNewExtension(t *testing.T) {
1325
ext := NewExtension()
1426
if ext == nil {
@@ -64,6 +76,8 @@ func TestNewExtensionWithConfig(t *testing.T) {
6476
}
6577

6678
func TestExtensionRegister(t *testing.T) {
79+
requireBroker(t)
80+
6781
testApp := createTestApp(t)
6882
ext := NewExtension(
6983
WithBroker("tcp://localhost:1883"),
@@ -112,6 +126,8 @@ func TestExtensionRegisterRequireConfigFails(t *testing.T) {
112126
}
113127

114128
func TestExtensionClient(t *testing.T) {
129+
requireBroker(t)
130+
115131
testApp := createTestApp(t)
116132
ext := NewExtension(
117133
WithBroker("tcp://localhost:1883"),

extensions/queue/backends_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@ package queue
22

33
import (
44
"context"
5+
"net"
56
"testing"
7+
"time"
68

79
"github.com/xraph/forge"
810
"github.com/xraph/forge/errors"
911
"github.com/xraph/forge/internal/logger"
1012
)
1113

14+
// requireService skips the test if the given TCP address is not reachable.
15+
func requireService(t *testing.T, addr, name string) {
16+
t.Helper()
17+
conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
18+
if err != nil {
19+
t.Skipf("skipping: %s not available at %s", name, addr)
20+
}
21+
conn.Close()
22+
}
23+
1224
// Test all backend constructors and basic error paths
1325

1426
func TestRedisQueue_Constructor(t *testing.T) {
@@ -363,6 +375,8 @@ func TestNATSQueue_NotConnectedErrors(t *testing.T) {
363375
// Test extension with different drivers
364376

365377
func TestExtension_RedisDriver(t *testing.T) {
378+
requireService(t, "localhost:6379", "Redis")
379+
366380
app := forge.New(
367381
forge.WithAppName("test-app"),
368382
forge.WithAppVersion("1.0.0"),
@@ -392,6 +406,8 @@ func TestExtension_RedisDriver(t *testing.T) {
392406
}
393407

394408
func TestExtension_RabbitMQDriver(t *testing.T) {
409+
requireService(t, "localhost:5672", "RabbitMQ")
410+
395411
app := forge.New(
396412
forge.WithAppName("test-app"),
397413
forge.WithAppVersion("1.0.0"),
@@ -421,6 +437,8 @@ func TestExtension_RabbitMQDriver(t *testing.T) {
421437
}
422438

423439
func TestExtension_NATSDriver(t *testing.T) {
440+
requireService(t, "localhost:4222", "NATS")
441+
424442
app := forge.New(
425443
forge.WithAppName("test-app"),
426444
forge.WithAppVersion("1.0.0"),

extensions/queue/coverage_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ func TestInMemoryQueue_DoubleConnect(t *testing.T) {
2727
t.Fatalf("Connect() error = %v", err)
2828
}
2929

30-
// Second connect should return error
30+
// Connect is idempotent — second call should succeed
3131
err = q.Connect(ctx)
32-
if !errors.Is(err, ErrAlreadyConnected) {
33-
t.Errorf("Connect() second time error = %v, want ErrAlreadyConnected", err)
32+
if err != nil {
33+
t.Errorf("Connect() second time error = %v, want nil", err)
3434
}
3535
}
3636

extensions/queue/inmemory.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,13 @@ func NewInMemoryQueue(config Config, logger forge.Logger, metrics forge.Metrics)
5151
}
5252
}
5353

54+
// Connect is idempotent — calling it on an already-connected instance is a no-op.
5455
func (q *InMemoryQueue) Connect(ctx context.Context) error {
5556
q.mu.Lock()
5657
defer q.mu.Unlock()
5758

5859
if q.connected {
59-
return ErrAlreadyConnected
60+
return nil
6061
}
6162

6263
q.connected = true

extensions/queue/inmemory_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ func TestInMemoryQueue_Connect(t *testing.T) {
2828
t.Fatalf("failed to connect: %v", err)
2929
}
3030

31-
// Try connecting again
31+
// Connect is idempotent — second call should succeed
3232
err = q.Connect(ctx)
33-
if !errors.Is(err, ErrAlreadyConnected) {
34-
t.Errorf("expected ErrAlreadyConnected, got %v", err)
33+
if err != nil {
34+
t.Errorf("expected nil on idempotent connect, got %v", err)
3535
}
3636
}
3737

extensions/queue/nats.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (q *NATSQueue) Connect(ctx context.Context) error {
5555
defer q.mu.Unlock()
5656

5757
if q.connected {
58-
return ErrAlreadyConnected
58+
return nil
5959
}
6060

6161
// Build connection URL

extensions/queue/nats_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ func TestNATSQueue_ConnectDisconnect(t *testing.T) {
8080
t.Skipf("Cannot connect to NATS: %v", err)
8181
}
8282

83-
// Test double connect
83+
// Connect is idempotent — second call should succeed
8484
err = queue.Connect(ctx)
85-
if !errors.Is(err, ErrAlreadyConnected) {
86-
t.Errorf("Connect() second time should return ErrAlreadyConnected, got %v", err)
85+
if err != nil {
86+
t.Errorf("Connect() second time should be idempotent, got %v", err)
8787
}
8888

8989
// Test ping

extensions/queue/rabbitmq.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (q *RabbitMQQueue) Connect(ctx context.Context) error {
5353
defer q.mu.Unlock()
5454

5555
if q.connected {
56-
return ErrAlreadyConnected
56+
return nil
5757
}
5858

5959
// Build connection URL

0 commit comments

Comments
 (0)