Skip to content

Commit e3e4658

Browse files
Update.
1 parent 7309b74 commit e3e4658

14 files changed

Lines changed: 785 additions & 480 deletions

include/pgraft_core.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ typedef struct
8080
int port;
8181
WORKER_STATUS status;
8282

83+
/*
84+
* Protects the command/status/apply circular buffers and their
85+
* head/tail/count indices below. Client backends (producers) and the
86+
* background worker (consumer) mutate these concurrently, so every
87+
* head/tail/count update must be done while holding this spinlock.
88+
*/
89+
slock_t mutex;
90+
91+
/* Monotonic id source used to uniquely identify queued commands
92+
* (replaces the old 1-second-granularity time(NULL) key, which collided
93+
* for commands queued within the same second). */
94+
int64_t next_command_id;
95+
8396
/* Fixed-size circular buffer for commands */
8497
pgraft_command_t commands[MAX_COMMANDS];
8598
int command_head; /* Index of next command to process */

src/pgraft.c

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,16 @@ pgraft_main(Datum main_arg)
452452
{
453453
/* Create JSON data for Raft replication using json-c */
454454
if (pgraft_json_create_kv_operation(PGRAFT_KV_PUT, cmd.kv_key, cmd.kv_value, cmd.kv_client_id, json_data, sizeof(json_data)) != 0) {
455-
elog(ERROR, "pgraft: failed to create JSON for KV PUT operation");
456-
continue;
455+
/* Fail just this command; do NOT elog(ERROR) here,
456+
* which would abort (and restart) the whole worker. */
457+
cmd.status = COMMAND_STATUS_FAILED;
458+
snprintf(cmd.error_message, sizeof(cmd.error_message),
459+
"Failed to create JSON for KV PUT operation");
460+
elog(WARNING, "pgraft: %s", cmd.error_message);
461+
pgraft_update_command_status(cmd.timestamp, cmd.status, cmd.error_message);
462+
break;
457463
}
458-
464+
459465
elog(LOG, "pgraft: calling pgraft_go_append_log with data=%s", json_data);
460466

461467
/* Replicate through Raft */
@@ -498,10 +504,16 @@ pgraft_main(Datum main_arg)
498504
{
499505
/* Create JSON data for Raft replication using json-c */
500506
if (pgraft_json_create_kv_operation(PGRAFT_KV_DELETE, cmd.kv_key, NULL, cmd.kv_client_id, json_data, sizeof(json_data)) != 0) {
501-
elog(ERROR, "pgraft: failed to create JSON for KV DELETE operation");
502-
continue;
507+
/* Fail just this command; do NOT elog(ERROR) here,
508+
* which would abort (and restart) the whole worker. */
509+
cmd.status = COMMAND_STATUS_FAILED;
510+
snprintf(cmd.error_message, sizeof(cmd.error_message),
511+
"Failed to create JSON for KV DELETE operation");
512+
elog(WARNING, "pgraft: %s", cmd.error_message);
513+
pgraft_update_command_status(cmd.timestamp, cmd.status, cmd.error_message);
514+
break;
503515
}
504-
516+
505517
elog(LOG, "pgraft: calling pgraft_go_append_log with data=%s", json_data);
506518

507519
/* Replicate through Raft */
@@ -550,6 +562,45 @@ pgraft_main(Datum main_arg)
550562
}
551563
}
552564

565+
/*
566+
* Apply committed Raft entries that the Go layer has replicated to
567+
* this node. applyEntry() in the Go library enqueues every committed
568+
* EntryNormal via pgraft_enqueue_for_apply_from_go(); here we drain
569+
* that queue and apply each entry locally. Each apply is wrapped in
570+
* PG_TRY so that a single malformed entry cannot abort (and thereby
571+
* restart) the whole background worker.
572+
*
573+
* NOTE: this worker holds BGWORKER_SHMEM_ACCESS only (no database
574+
* connection), so only shared-memory key/value operations are applied
575+
* here. SQL/DDL replication would additionally require
576+
* BackgroundWorkerInitializeConnection() and a transaction.
577+
*/
578+
{
579+
pgraft_apply_entry_t apply_entry;
580+
MemoryContext apply_loop_ctx = CurrentMemoryContext;
581+
582+
while (pgraft_dequeue_apply_entry(&apply_entry))
583+
{
584+
PG_TRY();
585+
{
586+
pgraft_apply_entry_to_postgres(apply_entry.raft_index,
587+
apply_entry.data,
588+
apply_entry.data_len);
589+
}
590+
PG_CATCH();
591+
{
592+
/* Recover and keep the worker alive; the entry is dropped
593+
* but the error is reported to the log. */
594+
MemoryContextSwitchTo(apply_loop_ctx);
595+
EmitErrorReport();
596+
FlushErrorState();
597+
elog(WARNING, "pgraft: error applying raft entry %lu, skipping",
598+
(unsigned long) apply_entry.raft_index);
599+
}
600+
PG_END_TRY();
601+
}
602+
}
603+
553604
pg_usleep(100000);
554605
sleep_count++;
555606

@@ -562,6 +613,14 @@ pgraft_main(Datum main_arg)
562613

563614
state->status = WORKER_STATUS_STOPPED;
564615
elog(LOG, "pgraft: background worker stopped");
616+
617+
/*
618+
* Exit cleanly (status 0) in response to an explicit shutdown command.
619+
* A background worker that exits with status 0 is de-registered and NOT
620+
* restarted by the postmaster, so the shutdown is durable. (bgw_restart_time
621+
* remains a finite interval so that an actual *crash* is still recovered.)
622+
*/
623+
proc_exit(0);
565624
}
566625

567626
/*
@@ -583,14 +642,24 @@ pgraft_worker_get_state(void)
583642
worker_state->port = 0;
584643
strlcpy(worker_state->address, "127.0.0.1", sizeof(worker_state->address));
585644
worker_state->status = WORKER_STATUS_STOPPED;
586-
645+
646+
/* Initialize the queue spinlock and command id source */
647+
SpinLockInit(&worker_state->mutex);
648+
worker_state->next_command_id = 1;
649+
587650
/* Initialize circular buffers */
588651
worker_state->command_head = 0;
589652
worker_state->command_tail = 0;
590653
worker_state->command_count = 0;
591654
worker_state->status_head = 0;
592655
worker_state->status_tail = 0;
593656
worker_state->status_count = 0;
657+
658+
/* Initialize apply queue */
659+
worker_state->apply_head = 0;
660+
worker_state->apply_tail = 0;
661+
worker_state->apply_count = 0;
662+
worker_state->last_applied_index = 0;
594663
}
595664
}
596665
return worker_state;
@@ -839,6 +908,8 @@ pgraft_update_shared_memory_from_go(void)
839908
elog(LOG, "pgraft: DEBUG - Got nodes JSON from Go: '%s'", nodes_json ? nodes_json : "NULL");
840909
if (nodes_json && strcmp(nodes_json, "[]") != 0)
841910
{
911+
/* NOTE: nodes_json is freed unconditionally below (see the
912+
* matching pgraft_go_free_string after this block). */
842913
/* Parse JSON using separate module */
843914
int node_count;
844915
int32_t node_ids[16];
@@ -870,9 +941,13 @@ pgraft_update_shared_memory_from_go(void)
870941
{
871942
elog(LOG, "pgraft: DEBUG - No valid nodes parsed from JSON");
872943
}
873-
874-
pgraft_go_free_string(nodes_json);
875944
}
945+
946+
/* Free the Go-allocated string in ALL cases (including the common
947+
* "[]" steady-state path), otherwise the worker leaks one string
948+
* every iteration. */
949+
if (nodes_json)
950+
pgraft_go_free_string(nodes_json);
876951
}
877952
}
878953

src/pgraft_apply.c

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ pgraft_apply_entry_to_postgres(uint64 raft_index, const char *data, size_t len)
4848

4949
elog(LOG, "pgraft: applying raft entry %lu to PostgreSQL (len=%zu)", raft_index, len);
5050

51+
/* Defensive: nothing to apply for empty/NULL payloads (e.g. Raft no-op
52+
* entries committed on leader election). Avoid dereferencing data[0]. */
53+
if (data == NULL || len == 0)
54+
{
55+
elog(DEBUG1, "pgraft: skipping empty raft entry %lu", raft_index);
56+
pgraft_record_applied_index(raft_index);
57+
return 0;
58+
}
59+
5160
/* Create a memory context for this operation */
5261
apply_context = AllocSetContextCreate(CurrentMemoryContext,
5362
"PgRaft Apply Context",
@@ -236,9 +245,9 @@ pgraft_serialize_log_entry(PgRaftLogEntry *entry, size_t *out_len)
236245
result = (char *) palloc(8192);
237246

238247
/* Format: index|term|op|database|schema|sql */
239-
len = snprintf(result, 8192, "%lu|%lu|%d|%s|%s|%s",
240-
(unsigned long) entry->index,
241-
(unsigned long) entry->term,
248+
len = snprintf(result, 8192, UINT64_FORMAT "|" UINT64_FORMAT "|%d|%s|%s|%s",
249+
entry->index,
250+
entry->term,
242251
entry->op,
243252
entry->database,
244253
entry->schema,
@@ -257,26 +266,19 @@ void
257266
pgraft_record_applied_index(uint64 index)
258267
{
259268
pgraft_worker_state_t *worker_state;
260-
pgraft_cluster_t *shm_cluster;
261-
262-
/* Get shared memory references */
263-
shm_cluster = pgraft_core_get_shared_memory();
264-
if (!shm_cluster) {
265-
elog(WARNING, "pgraft: failed to get cluster state for recording applied index");
266-
return;
267-
}
268-
269+
269270
worker_state = pgraft_worker_get_state();
270271
if (!worker_state) {
271272
elog(WARNING, "pgraft: failed to get worker state for recording applied index");
272273
return;
273274
}
274-
275-
/* Update the last applied index in shared memory */
276-
SpinLockAcquire(&shm_cluster->mutex);
275+
276+
/* Guard the worker-state field with the worker state's OWN mutex (not the
277+
* cluster mutex, which lives in a different shared-memory struct). */
278+
SpinLockAcquire(&worker_state->mutex);
277279
worker_state->last_applied_index = index;
278-
SpinLockRelease(&shm_cluster->mutex);
279-
280+
SpinLockRelease(&worker_state->mutex);
281+
280282
elog(DEBUG2, "pgraft: recorded applied index %lu in shared memory", index);
281283
}
282284

@@ -287,27 +289,19 @@ uint64
287289
pgraft_get_applied_index(void)
288290
{
289291
pgraft_worker_state_t *worker_state;
290-
pgraft_cluster_t *shm_cluster;
291292
uint64 last_applied;
292-
293-
/* Get shared memory references */
294-
shm_cluster = pgraft_core_get_shared_memory();
295-
if (!shm_cluster) {
296-
elog(WARNING, "pgraft: failed to get cluster state for getting applied index");
297-
return 0;
298-
}
299-
293+
300294
worker_state = pgraft_worker_get_state();
301295
if (!worker_state) {
302296
elog(WARNING, "pgraft: failed to get worker state for getting applied index");
303297
return 0;
304298
}
305-
306-
/* Get the last applied index from shared memory */
307-
SpinLockAcquire(&shm_cluster->mutex);
299+
300+
/* Guard with the worker state's own mutex (matches pgraft_record_applied_index). */
301+
SpinLockAcquire(&worker_state->mutex);
308302
last_applied = worker_state->last_applied_index;
309-
SpinLockRelease(&shm_cluster->mutex);
310-
303+
SpinLockRelease(&worker_state->mutex);
304+
311305
return last_applied;
312306
}
313307

src/pgraft_core.c

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,26 @@ pgraft_core_get_shared_memory(void)
442442
cluster = (pgraft_cluster_t *) ShmemInitStruct("pgraft_cluster",
443443
sizeof(pgraft_cluster_t),
444444
&found);
445-
if (!found)
445+
if (!found && cluster)
446446
{
447-
/* Initialize shared memory when first accessed */
448-
pgraft_core_init_shared_memory();
447+
/*
448+
* We are the first to touch the segment. Initialize it in place.
449+
* Do NOT delegate to pgraft_core_init_shared_memory() here: that
450+
* would call ShmemInitStruct() a second time, observe found==true,
451+
* and skip SpinLockInit(), leaving the mutex uninitialized.
452+
*/
453+
memset(cluster, 0, sizeof(pgraft_cluster_t));
454+
SpinLockInit(&cluster->mutex);
455+
cluster->initialized = false;
456+
cluster->node_id = -1;
457+
cluster->current_term = 0;
458+
cluster->leader_id = -1;
459+
strncpy(cluster->state, "stopped", sizeof(cluster->state) - 1);
460+
cluster->state[sizeof(cluster->state) - 1] = '\0';
461+
cluster->num_nodes = 0;
462+
cluster->messages_processed = 0;
463+
cluster->heartbeats_sent = 0;
464+
cluster->elections_triggered = 0;
449465
}
450466
}
451467
return cluster;

0 commit comments

Comments
 (0)