ProcessWire and PDO: Ensuring Write Idempotency During Retry Logic
0 reputation · 18 Jan 2022, 08:12 UTC
0 reputation · 18 Jan 2022, 08:12 UTC
When integrating external API triggers with ProcessWire page updates, there is a risk of duplicate data entries if a network timeout occurs after a write operation but before the confirmation is received. While ProcessWire utilizes PDO for database interactions, the framework does not natively provide idempotency keys or a built-in mechanism to deduplicate repeated write requests at the core level.
The primary concern involves the interaction between database transactions and the ProcessWire hook system. A retry attempt that triggers a page save may re-execute Pages::saved or Page::saved hooks, potentially duplicating non-transactional side effects such as external notifications or log entries.
Given that atomicity depends on the underlying storage engine (e.g., InnoDB vs MyISAM), how can a developer implement a retry strategy that ensures a page is updated exactly once without triggering redundant hook logic? What is the recommended approach for tracking request uniqueness within the ProcessWire API to prevent these duplicate writes?
34775 reputation · 18 Jan 2022, 10:29 UTC
ProcessWire does not provide native idempotency keys. To prevent duplicate writes and redundant hook execution during API retries, implement a guard in a Page::saveReady hook. This hook should attempt to insert a unique request token into a dedicated database table with a UNIQUE index. If the insertion fails due to a duplicate key, the save process should be aborted before the Page::saved or Pages::saved hooks are triggered.
The ProcessWire save lifecycle executes Page::saveReady before the database write and Page::saved/Pages::saved after the write. Because external side effects (like API notifications) typically reside in the latter hooks, aborting the process at the saveReady stage prevents these side effects from firing multiple times. If the storage engine is InnoDB, wrapping the token insertion and page save in a transaction ensures atomicity; for MyISAM, the unique index on the token table serves as the final source of truth.
CREATE TABLE `idempotency_log` (
`token` VARCHAR(64) NOT NULL,
`page_id` INT UNSIGNED NOT NULL,
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`token`)
) ENGINE=InnoDB;
Page::saveReady Guard
Add a hook to intercept the save process. If a token is provided in the request, validate it against the log table:
$wire->addHookBefore('Page::saveReady', function(HookEvent $e) {
$page = $e->object;
$token = $this->input->post('idempotency_token');
if (!$token) return;
$db = $this->database;
try {
$db->prepare('INSERT INTO idempotency_log (token, page_id) VALUES (?, ?)')
->execute([$token, $page->id]);
} catch (\PDOException $ex) {
if ($ex->getCode() === '23000') {
// Duplicate token: abort the save to prevent redundant hooks
$e->replaceResult = false;
$page->idempotency_duplicate = true;
return;
}
throw $ex;
}
});
Ensure any external notifications in Pages::saved check for the duplicate flag:
$wire->addHookAfter('Pages::saved', function(HookEvent $e) {
$page = $e->arguments(0);
if (isset($page->idempotency_duplicate) && $page->idempotency_duplicate) return;
// Execute external API calls here
});
Page::saveReady and Pages::saved are standard.$database wrapper (PDO).SHOW TABLE STATUS WHERE Name = 'pages' to verify the storage engine. If MyISAM is used, explicit transactions are ignored, and the unique index on idempotency_log is the only reliable guard.Which storage engine (InnoDB or MyISAM) is currently configured for your ProcessWire installation? This determines if the token insertion and page save can be wrapped in a single atomic transaction.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.