fix(ruby): require shared replay reservations#235
Conversation
Greptile SummaryThis PR enforces that non-localnet MPP servers supply a replay store that is both durable and cross-process shared, closing a correctness gap where a
Confidence Score: 5/5Safe to merge; the validation logic, harness store, and test suite are all consistent and the previously noted arity-check edge case is tracked in an open thread. The change is purely additive in the enforcement direction — it makes it harder to misconfigure a production store, not easier. The new HarnessReplayStore uses well-understood POSIX primitives (flock + atomic rename) and the cross-process test exercises it with eight concurrent forked workers. The one known limitation (arity -1 false-positive for single-optional-arg methods) is already documented in a previous review thread. runtime.rb — the accepts_two_positional_arguments? helper, as discussed in the existing review thread. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["PayKit::Protocols::Mpp.create(replay_store:)"] --> B{replay_store == DEV_ONLY_MEMORY_STORE?}
B -- Yes --> C{localnet?}
C -- No --> ERR1["ConfigurationError\n'durable replay_store shared\nacross workers required'"]
C -- Yes --> WARN["warn: volatile MemoryStore\n(dev only)"] --> MEM["replay_store = MemoryStore.new"]
B -- No --> D{replay_store nil?}
D -- Yes --> ERR2["ConfigurationError\n'nil is not a valid store'"]
D -- No --> E{localnet?}
E -- Yes --> OK["Server::Charge.new(...)"]
E -- No --> F["durable_shared_replay_store?(store)"]
F --> G{durable? == true}
G -- No --> ERR3["ConfigurationError\n'durable replay_store shared\nacross workers required'"]
G -- Yes --> H{shared? == true}
H -- No --> ERR3
H -- Yes --> I{put_if_absent overridden\nfrom base Store?}
I -- No --> ERR3
I -- Yes --> J{accepts_two_positional_args?}
J -- No --> ERR3
J -- Yes --> OK
MEM --> OK
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["PayKit::Protocols::Mpp.create(replay_store:)"] --> B{replay_store == DEV_ONLY_MEMORY_STORE?}
B -- Yes --> C{localnet?}
C -- No --> ERR1["ConfigurationError\n'durable replay_store shared\nacross workers required'"]
C -- Yes --> WARN["warn: volatile MemoryStore\n(dev only)"] --> MEM["replay_store = MemoryStore.new"]
B -- No --> D{replay_store nil?}
D -- Yes --> ERR2["ConfigurationError\n'nil is not a valid store'"]
D -- No --> E{localnet?}
E -- Yes --> OK["Server::Charge.new(...)"]
E -- No --> F["durable_shared_replay_store?(store)"]
F --> G{durable? == true}
G -- No --> ERR3["ConfigurationError\n'durable replay_store shared\nacross workers required'"]
G -- Yes --> H{shared? == true}
H -- No --> ERR3
H -- Yes --> I{put_if_absent overridden\nfrom base Store?}
I -- No --> ERR3
I -- Yes --> J{accepts_two_positional_args?}
J -- No --> ERR3
J -- Yes --> OK
MEM --> OK
Reviews (10): Last reviewed commit: "Merge remote-tracking branch 'origin/fix..." | Re-trigger Greptile |
|
@greptile-apps please review. |
…o fix/ruby-replay-store-capability
|
@greptile-apps please re-review exact head a20b530. This hardens the harness replay store with cross-process atomic replacement and tightens durable/shared store capability validation. |
…hygiene gates The ruby native-guard probe follows #235's durable_shared_replay_store? predicate rename (the guard itself is unchanged and stronger), and the missing-ATA hygiene gate matches the reconciled deliver-or-reject step name while still asserting the step stays blocking with the red-fault env.
…o fix/ruby-replay-store-capability
…o fix/ruby-replay-store-capability
…o fix/ruby-replay-store-capability
| def self.accepts_two_positional_arguments?(method) | ||
| arity = method.arity | ||
| arity == 2 || (arity.negative? && (-arity - 1) <= 2) | ||
| end |
There was a problem hiding this comment.
Arity check has a false-positive for single-optional-arg signatures
Method#arity reports -1 for both def put_if_absent(*args) (unlimited) and def put_if_absent(key = nil) (maximum 1 arg). The formula -(-1) - 1 = 0 ≤ 2 passes both, but calling the second with two positional arguments raises ArgumentError (wrong number of arguments (given 2, expected 0..1)) at runtime — meaning a store that declares durable? and shared? but accidentally implements put_if_absent with one optional arg passes validation and then crashes on the first real payment attempt. Method#parameters provides exact per-parameter types and lets you check both minimum and maximum positional capacity without this ambiguity.
| def self.accepts_two_positional_arguments?(method) | |
| arity = method.arity | |
| arity == 2 || (arity.negative? && (-arity - 1) <= 2) | |
| end | |
| def self.accepts_two_positional_arguments?(method) | |
| params = method.parameters | |
| positional_req = params.count { |type, _| type == :req } | |
| positional_opt = params.count { |type, _| type == :opt } | |
| has_splat = params.any? { |type, _| type == :rest } | |
| max_positional = has_splat ? Float::INFINITY : positional_req + positional_opt | |
| positional_req <= 2 && max_positional >= 2 | |
| end |
…o fix/ruby-replay-store-capability
…o fix/ruby-replay-store-capability
Require an explicit durable-and-shared replay-store capability outside localnet.
FileStoreremains suitable for single-process examples but is rejected for multi-worker production because its mutex is not cross-process.Tests:
mise exec ruby@3.3 -- bundle exec rake test(484 runs, 1461 assertions)