StripeKit 0.1.1
Stripe integration toolkit for C++
Loading...
Searching...
No Matches
WebhookDedupStore.h
1/** WebhookDedupStore.h
2 *
3 * Webhook event-id tracking used to prevent processing the same event twice.
4 *
5 * @author Hans de Ruiter
6 *
7 * @license See LICENSE.md for details.
8 */
9
10#pragma once
11
12#include <chrono>
13#include <memory>
14#include <mutex>
15#include <string>
16#include <string_view>
17#include <unordered_map>
18
19namespace stripekit {
20
21/**
22 * @addtogroup webhooks
23 * @{
24 */
25
26/** How long an unfinished claim is honoured before another process may take it. */
27inline constexpr std::chrono::minutes defaultStaleInflightTimeout{ 15 };
28
29/**
30 * The result of trying to reserve an event for processing.
31 */
32enum class ClaimOutcome {
33 Claimed, /**< This worker reserved the event id and should process it now. */
34 DuplicateProcessed, /**< A previous delivery already completed processing. */
35 DuplicateInflight, /**< Another worker is already processing the same event. */
36 RetryableStoreError, /**< The store failed temporarily and the caller should retry. */
37 FatalStoreError /**< The store failed in a non-recoverable way. */
38};
39
40/**
41 * What to tell Stripe about a delivery.
42 */
44 AcknowledgeSuccess, /**< Answer 2xx. Stripe stops retrying this event. */
45 RetryLater /**< Answer 5xx. Stripe delivers the event again later. */
46};
47
48/**
49 * Asks the store to reserve an event id.
50 *
51 * The store must hand out a reservation to exactly one caller. If the id is
52 * already reserved or already finished, it reports a duplicate instead, which
53 * is what keeps repeated Stripe deliveries from being processed twice.
54 */
56 /** The Stripe event id to reserve. */
57 std::string event_id;
58
59 /** Current time, used for claim timestamps and staleness checks. */
60 std::chrono::system_clock::time_point now;
61
62 /** How old an unfinished claim must be before it can be taken over. */
64};
65
66/**
67 * The store's answer to a ClaimRequest.
68 */
70 /** What happened. */
72
73 /** Optional free-form detail from the implementation, for diagnostics. */
74 std::string detail;
75
76 /**
77 * Reports whether the caller won the reservation.
78 *
79 * @return True only for ClaimOutcome::Claimed.
80 */
81 bool should_process() const;
82};
83
84/**
85 * Remembers which webhook events have been handled.
86 *
87 * Stripe delivers the same event more than once whenever it cannot confirm
88 * receipt. This interface skips completed events and reserves unfinished events
89 * for one worker at a time. Handlers must still be safe to retry after failure
90 * or a crash.
91 *
92 * Which implementation to use depends on the deployment. Use
93 * InMemoryWebhookDedupStore for tests, SqliteWebhookDedupStore for a single
94 * host, and an implementation backed by the application's shared database when
95 * several instances handle the same endpoint.
96 *
97 * Implementations must be thread-safe, and claim() must be atomic: two
98 * instances racing on the same event id must produce exactly one
99 * ClaimOutcome::Claimed. A read followed by a write is not sufficient; use the
100 * database's conditional insert.
101 */
103public:
104 /**
105 * Destroys the store instance.
106 */
107 virtual ~WebhookDedupStore() = default;
108
109 /**
110 * Tries to reserve an event id for processing.
111 *
112 * @param request The event id to reserve, and the timing rules to apply.
113 * @return Whether this caller won the reservation, and why not if it did
114 * not. Store failures are reported here rather than thrown.
115 */
116 virtual ClaimResult claim(const ClaimRequest& request) = 0;
117
118 /**
119 * Records that an event has been fully handled.
120 *
121 * Later deliveries of the same event then report
122 * ClaimOutcome::DuplicateProcessed.
123 *
124 * @param event_id The event id that was handled.
125 * @param processed_at When it finished.
126 * @return True if a claim record was updated; false if there was nothing
127 * to update.
128 */
129 virtual bool mark_processed(std::string_view event_id,
130 std::chrono::system_clock::time_point processed_at) = 0;
131
132 /**
133 * Releases an unfinished claim after its handler has stopped.
134 *
135 * Delete only the unprocessed record whose claim time matches @p claimed_at,
136 * so an older attempt cannot release a newer worker's reservation.
137 *
138 * @param event_id The event to allow another delivery to retry.
139 * @param claimed_at The successful ClaimRequest::now for this attempt.
140 * @return True if the matching claim was removed; false if it no longer exists.
141 * @throws StripeKitException The store could not release the claim.
142 */
143 virtual bool release_claim(std::string_view event_id,
144 std::chrono::system_clock::time_point claimed_at) = 0;
145
146 /**
147 * Deletes records older than the retention period.
148 *
149 * Call this periodically to stop the store growing without limit. Keep the
150 * retention comfortably longer than Stripe's retry window, which runs for
151 * several days, or an old event could be processed a second time.
152 *
153 * @param now Current time.
154 * @param retention How long a record must be kept.
155 */
156 virtual void erase_expired(std::chrono::system_clock::time_point now,
157 std::chrono::hours retention) = 0;
158};
159
160/**
161 * A de-duplication store that keeps everything in memory.
162 *
163 * Suitable for tests and local development. It forgets everything when the
164 * process exits and is not shared between processes, so it is not appropriate
165 * for production.
166 *
167 * Safe to use from several threads.
168 */
170public:
171 /**
172 * Creates an empty store.
173 */
175
176 /**
177 * Destroys the store instance.
178 */
180
181 /**
182 * Tries to reserve an event id for processing.
183 *
184 * @param request The event id to reserve, and the timing rules to apply.
185 * @return Whether this caller won the reservation.
186 */
187 ClaimResult claim(const ClaimRequest& request) override;
188
189 /**
190 * Records that an event has been fully handled.
191 *
192 * @param event_id The event id that was handled.
193 * @param processed_at When it finished.
194 * @return True if a claim record was updated.
195 */
196 bool mark_processed(std::string_view event_id,
197 std::chrono::system_clock::time_point processed_at) override;
198
199 /** @copydoc WebhookDedupStore::release_claim */
200 bool release_claim(std::string_view event_id,
201 std::chrono::system_clock::time_point claimed_at) override;
202
203 /**
204 * Deletes records older than the retention period.
205 *
206 * @param now Current time.
207 * @param retention How long a record must be kept.
208 */
209 void erase_expired(std::chrono::system_clock::time_point now,
210 std::chrono::hours retention) override;
211
212private:
213 struct Entry {
214 bool processed = false;
215 std::chrono::system_clock::time_point claimed_at;
216 std::chrono::system_clock::time_point processed_at;
217 };
218
219 std::mutex mutex;
220 std::unordered_map<std::string, Entry> records;
221};
222
223/**
224 * Works out what to tell Stripe, given a claim outcome.
225 *
226 * Already-processed duplicates count as success. In-flight duplicates and
227 * store failures ask Stripe to retry.
228 *
229 * @param outcome The result of a claim attempt.
230 * @return WebhookResponseAction::AcknowledgeSuccess for a claim or processed
231 * duplicate; WebhookResponseAction::RetryLater otherwise.
232 */
234
235/** @} */
236
237} // namespace stripekit
~InMemoryWebhookDedupStore() override
Destroys the store instance.
bool release_claim(std::string_view event_id, std::chrono::system_clock::time_point claimed_at) override
Releases an unfinished claim after its handler has stopped.
InMemoryWebhookDedupStore()
Creates an empty store.
ClaimResult claim(const ClaimRequest &request) override
Tries to reserve an event id for processing.
void erase_expired(std::chrono::system_clock::time_point now, std::chrono::hours retention) override
Deletes records older than the retention period.
bool mark_processed(std::string_view event_id, std::chrono::system_clock::time_point processed_at) override
Records that an event has been fully handled.
Remembers which webhook events have been handled.
virtual bool release_claim(std::string_view event_id, std::chrono::system_clock::time_point claimed_at)=0
Releases an unfinished claim after its handler has stopped.
virtual ~WebhookDedupStore()=default
Destroys the store instance.
virtual bool mark_processed(std::string_view event_id, std::chrono::system_clock::time_point processed_at)=0
Records that an event has been fully handled.
virtual void erase_expired(std::chrono::system_clock::time_point now, std::chrono::hours retention)=0
Deletes records older than the retention period.
virtual ClaimResult claim(const ClaimRequest &request)=0
Tries to reserve an event id for processing.
WebhookResponseAction
What to tell Stripe about a delivery.
ClaimOutcome
The result of trying to reserve an event for processing.
WebhookResponseAction map_claim_outcome_to_response(ClaimOutcome outcome)
Works out what to tell Stripe, given a claim outcome.
constexpr std::chrono::minutes defaultStaleInflightTimeout
How long an unfinished claim is honoured before another process may take it.
@ RetryableStoreError
The store failed temporarily and the caller should retry.
@ FatalStoreError
The store failed in a non-recoverable way.
@ Claimed
This worker reserved the event id and should process it now.
@ DuplicateProcessed
A previous delivery already completed processing.
@ DuplicateInflight
Another worker is already processing the same event.
Async.h.
Definition Async.h:20
Asks the store to reserve an event id.
std::chrono::minutes stale_inflight_timeout
How old an unfinished claim must be before it can be taken over.
std::string event_id
The Stripe event id to reserve.
std::chrono::system_clock::time_point now
Current time, used for claim timestamps and staleness checks.
The store's answer to a ClaimRequest.
bool should_process() const
Reports whether the caller won the reservation.
ClaimOutcome outcome
What happened.
std::string detail
Optional free-form detail from the implementation, for diagnostics.