Skip to content

Commit a7da6d3

Browse files
committed
[+] add etcd<->postgres sync
1 parent 541d71e commit a7da6d3

File tree

8 files changed

+233
-248
lines changed

8 files changed

+233
-248
lines changed

cmd/etcd_fdw/main.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"context"
77
"fmt"
88
"os"
9+
"os/signal"
10+
"syscall"
911

1012
"github.com/jessevdk/go-flags"
1113
"github.com/sirupsen/logrus"
@@ -128,7 +130,19 @@ func main() {
128130
logrus.WithError(err).Fatal("Failed to setup logging")
129131
}
130132

131-
ctx := context.Background()
133+
// Setup graceful shutdown
134+
ctx, cancel := context.WithCancel(context.Background())
135+
defer cancel()
136+
137+
// Setup signal handling for graceful shutdown
138+
sigChan := make(chan os.Signal, 1)
139+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
140+
141+
go func() {
142+
sig := <-sigChan
143+
logrus.WithField("signal", sig).Info("Received shutdown signal, initiating graceful shutdown...")
144+
cancel()
145+
}()
132146

133147
// Connect to PostgreSQL
134148
var pgPool db.PgxPoolIface
@@ -153,7 +167,9 @@ func main() {
153167

154168
// Create and start sync service
155169
syncService := sync.NewService(pgPool, etcdClient, prefix, config.DryRun)
156-
if err := syncService.Start(ctx); err != nil {
170+
if err := syncService.Start(ctx); err != nil && ctx.Err() == nil {
157171
logrus.WithError(err).Fatal("Synchronization failed")
158172
}
173+
174+
logrus.Info("Graceful shutdown completed")
159175
}

internal/db/postgres.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,11 @@ func GetPendingWALEntries(ctx context.Context, pool PgxIface) ([]WALEntry, error
220220
return entries, nil
221221
}
222222

223-
// DeleteWALEntry removes a processed WAL entry
224-
func DeleteWALEntry(ctx context.Context, pool PgxIface, key string, timestamp string) error {
225-
query := `DELETE FROM etcd_wal WHERE key = $1 AND ts = $2`
223+
// UpdateWALEntry removes a processed WAL entry
224+
func UpdateWALEntry(ctx context.Context, pool PgxIface, key string, timestamp string, revision int64) error {
225+
query := `UPDATE etcd_wal SET revision = $3 WHERE key = $1 AND ts = $2`
226226

227-
_, err := pool.Exec(ctx, query, key, timestamp)
227+
_, err := pool.Exec(ctx, query, key, timestamp, revision)
228228
if err != nil {
229229
return fmt.Errorf("failed to delete WAL entry: %w", err)
230230
}

internal/db/postgres_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestInsertWALEntry(t *testing.T) {
7272

7373
// Set up mock expectations
7474
mock.ExpectExec("INSERT INTO etcd_wal").
75-
WithArgs(key, "value", 1).
75+
WithArgs(key, &value, &revision).
7676
WillReturnResult(pgxmock.NewResult("INSERT", 1))
7777

7878
// Test the function
@@ -127,12 +127,12 @@ func TestDeleteWALEntry(t *testing.T) {
127127
ctx := context.Background()
128128

129129
// Set up mock expectations
130-
mock.ExpectExec("DELETE FROM etcd_wal WHERE key").
131-
WithArgs("test/key", "2023-01-01T00:00:00Z").
132-
WillReturnResult(pgxmock.NewResult("DELETE", 1))
130+
mock.ExpectExec("UPDATE etcd_wal").
131+
WithArgs("test/key", "2023-01-01T00:00:00Z", int64(-1)).
132+
WillReturnResult(pgxmock.NewResult("UPDATE", 1))
133133

134134
// Test the function
135-
err = DeleteWALEntry(ctx, mock, "test/key", "2023-01-01T00:00:00Z")
135+
err = UpdateWALEntry(ctx, mock, "test/key", "2023-01-01T00:00:00Z", -1)
136136
require.NoError(t, err)
137137

138138
// Verify all expectations were met

internal/sync/resolver.go

Lines changed: 0 additions & 143 deletions
This file was deleted.

internal/sync/sync.go

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ package sync
33

44
import (
55
"context"
6+
"encoding/json"
67
"fmt"
78
"time"
89

10+
"github.com/jackc/pgx/v5/pgconn"
911
"github.com/sirupsen/logrus"
1012
clientv3 "go.etcd.io/etcd/client/v3"
1113

1214
"github.com/cybertec-postgresql/etcd_fdw/internal/db"
1315
"github.com/cybertec-postgresql/etcd_fdw/internal/etcd"
1416
)
1517

18+
const InvalidRevision = -1
19+
1620
// Service orchestrates bidirectional synchronization between etcd and PostgreSQL
1721
type Service struct {
1822
pgPool db.PgxPoolIface
@@ -224,25 +228,81 @@ func (s *Service) syncPostgreSQLToEtcd(ctx context.Context) error {
224228
}
225229

226230
if err := s.processPostgreSQLNotification(ctx, notification); err != nil {
227-
logrus.WithError(err).WithField("payload", "unknown").Error("Failed to process PostgreSQL notification")
231+
logrus.WithError(err).WithField("payload", notification.Payload).Error("Failed to process PostgreSQL notification")
228232
// Continue processing other notifications rather than failing entirely
229233
}
230234
}
231235
}
232236
}
233237

234238
// processPostgreSQLNotification processes a PostgreSQL NOTIFY and syncs to etcd
235-
func (s *Service) processPostgreSQLNotification(ctx context.Context, notification interface{}) error {
236-
// In a real implementation, we would parse the JSON payload to get WAL entry details
237-
// For now, we'll log that we received the notification
238-
logrus.WithField("notification", notification).Info("Received PostgreSQL notification")
239-
240-
// TODO: Parse the notification and sync the change to etcd
241-
// This would involve:
242-
// 1. Parse notification JSON to get key, value, revision
243-
// 2. Apply conflict resolution logic
244-
// 3. Put/Delete to etcd
245-
// 4. Mark WAL entry as processed
239+
func (s *Service) processPostgreSQLNotification(ctx context.Context, notification *pgconn.Notification) error {
240+
// Parse the JSON payload from the notification
241+
var walNotification struct {
242+
Key string `json:"key"`
243+
Ts string `json:"ts"`
244+
Value *string `json:"value"`
245+
Revision *int64 `json:"revision"`
246+
Operation string `json:"operation"`
247+
}
246248

247-
return nil
249+
if err := json.Unmarshal([]byte(notification.Payload), &walNotification); err != nil {
250+
return fmt.Errorf("failed to parse notification payload: %w", err)
251+
}
252+
253+
logrus.WithFields(logrus.Fields{
254+
"key": walNotification.Key,
255+
"ts": walNotification.Ts,
256+
"operation": walNotification.Operation,
257+
}).Info("Processing PostgreSQL notification")
258+
259+
// Apply conflict resolution: check if etcd has a newer version
260+
etcdKV, err := s.etcdClient.Get(ctx, walNotification.Key)
261+
if err != nil {
262+
return fmt.Errorf("failed to get key from etcd for conflict resolution: %w", err)
263+
}
264+
265+
// Conflict resolution: etcd wins (if etcd has newer revision, skip this change)
266+
if etcdKV != nil && walNotification.Revision != nil && etcdKV.Revision > *walNotification.Revision {
267+
logrus.WithFields(logrus.Fields{
268+
"key": walNotification.Key,
269+
"etcd_revision": etcdKV.Revision,
270+
"local_revision": *walNotification.Revision,
271+
}).Warn("Conflict detected: etcd has newer revision, skipping local change")
272+
273+
// Mark WAL entry as failed (conflict resolved - etcd wins)
274+
return db.UpdateWALEntry(ctx, s.pgPool, walNotification.Key, walNotification.Ts, InvalidRevision)
275+
}
276+
277+
// Apply the change to etcd
278+
var newRevision int64
279+
switch walNotification.Operation {
280+
case "CREATE", "UPDATE":
281+
if walNotification.Value != nil {
282+
resp, err := s.etcdClient.Put(ctx, walNotification.Key, *walNotification.Value)
283+
if err != nil {
284+
// Mark WAL entry as failed
285+
db.UpdateWALEntry(ctx, s.pgPool, walNotification.Key, walNotification.Ts, InvalidRevision)
286+
return fmt.Errorf("failed to put key to etcd: %w", err)
287+
}
288+
newRevision = resp.Header.Revision
289+
logrus.WithField("key", walNotification.Key).Info("Synced PostgreSQL change to etcd (PUT)")
290+
}
291+
case "DELETE":
292+
resp, err := s.etcdClient.Delete(ctx, walNotification.Key)
293+
if err != nil {
294+
// Mark WAL entry as failed
295+
db.UpdateWALEntry(ctx, s.pgPool, walNotification.Key, walNotification.Ts, InvalidRevision)
296+
return fmt.Errorf("failed to delete key from etcd: %w", err)
297+
}
298+
newRevision = resp.Header.Revision
299+
logrus.WithField("key", walNotification.Key).Info("Synced PostgreSQL change to etcd (DELETE)")
300+
default:
301+
// Mark WAL entry as failed due to unknown operation
302+
db.UpdateWALEntry(ctx, s.pgPool, walNotification.Key, walNotification.Ts, InvalidRevision)
303+
return fmt.Errorf("unknown operation type: %s", walNotification.Operation)
304+
}
305+
306+
// Mark WAL entry as successfully processed with the new etcd revision
307+
return db.UpdateWALEntry(ctx, s.pgPool, walNotification.Key, walNotification.Ts, newRevision)
248308
}

0 commit comments

Comments
 (0)