Class: Clickwrap::Services::AuthorizeExternalAction
- Inherits:
-
Object
- Object
- Clickwrap::Services::AuthorizeExternalAction
- Defined in:
- lib/clickwrap/services/authorize_external_action.rb
Overview
Clickwrap.authorize_external_action! — the outbox.
=========================================================================== READ THIS BEFORE CHANGING ANYTHING HERE.
This is a DISTRIBUTED RELIABILITY PROTOCOL. It is NOT a cross-system ACID transaction, and no amount of care in this file could make it one.
Stripe, an identity service, a timestamp authority, a remote signature provider: none of them can enlist in your database transaction. There is no two-phase commit here, no compensating rollback that reaches into someone else's ledger, and no moment at which "the evidence committed" and "the provider acted" are known to be true together. Clickwrap never claims atomicity across two independent systems, and the receipt this produces does not claim it either.
What this protocol actually gives you is narrower and achievable:
1. ONE local transaction commits the evidence event and a `pending`
outbox row carrying a server-generated idempotency key.
2. The provider is called OUTSIDE that transaction, by the host, with
that key — so a retry reaches the provider as the same request rather
than as a second one.
3. The outcome is appended back idempotently through
`record_provider_success_and_consume!`, `record_provider_failure!`,
or `record_provider_outcome_unknown!`.
4. `unknown` stays `unknown` until someone or something resolves it.
Step 4 is the part people delete first and regret longest. A timeout is not a failure — it is an absence of information. Writing "failed" because the socket closed is how a second debit happens; writing "succeeded" because the retry returned 200 is how a fictional one does. The reconciliation task exists precisely so the ambiguous case can be settled later with better information than we have at the moment it occurs.
Instance Attribute Summary collapse
-
#after_pending_action_is_saved_inside_transaction ⇒ Object
readonly
Returns the value of attribute after_pending_action_is_saved_inside_transaction.
-
#capture_options ⇒ Object
readonly
Returns the value of attribute capture_options.
-
#policy ⇒ Object
readonly
Returns the value of attribute policy.
-
#provider_name ⇒ Object
readonly
Returns the value of attribute provider_name.
Class Method Summary collapse
-
.idempotency_key_for(policy_key:, event_id:) ⇒ Object
The key handed to the provider.
Instance Method Summary collapse
-
#call ⇒ Object
Captures the evidence and commits the pending outbox row in ONE local transaction, then returns the ExternalAction so the caller can hand its id and idempotency key to a job.
-
#initialize(policy:, provider_name: nil, after_pending_action_is_saved_inside_transaction: nil, **capture_options) ⇒ AuthorizeExternalAction
constructor
A new instance of AuthorizeExternalAction.
Constructor Details
#initialize(policy:, provider_name: nil, after_pending_action_is_saved_inside_transaction: nil, **capture_options) ⇒ AuthorizeExternalAction
Returns a new instance of AuthorizeExternalAction.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/clickwrap/services/authorize_external_action.rb', line 54 def initialize(policy:, provider_name: nil, after_pending_action_is_saved_inside_transaction: nil, **) @policy = policy @provider_name = provider_name&.to_s @after_pending_action_is_saved_inside_transaction = after_pending_action_is_saved_inside_transaction @capture_options = return if @after_pending_action_is_saved_inside_transaction.nil? || @after_pending_action_is_saved_inside_transaction.respond_to?(:call) raise ArgumentError, "after_pending_action_is_saved_inside_transaction must be callable. It receives " \ "pending_action: and pending_receipt: as keyword arguments." end |
Instance Attribute Details
#after_pending_action_is_saved_inside_transaction ⇒ Object (readonly)
Returns the value of attribute after_pending_action_is_saved_inside_transaction.
71 72 73 |
# File 'lib/clickwrap/services/authorize_external_action.rb', line 71 def after_pending_action_is_saved_inside_transaction @after_pending_action_is_saved_inside_transaction end |
#capture_options ⇒ Object (readonly)
Returns the value of attribute capture_options.
71 72 73 |
# File 'lib/clickwrap/services/authorize_external_action.rb', line 71 def @capture_options end |
#policy ⇒ Object (readonly)
Returns the value of attribute policy.
71 72 73 |
# File 'lib/clickwrap/services/authorize_external_action.rb', line 71 def policy @policy end |
#provider_name ⇒ Object (readonly)
Returns the value of attribute provider_name.
71 72 73 |
# File 'lib/clickwrap/services/authorize_external_action.rb', line 71 def provider_name @provider_name end |
Class Method Details
.idempotency_key_for(policy_key:, event_id:) ⇒ Object
The key handed to the provider. It is derived from the committed event id, which means one evidence event maps to exactly one external action forever: a retried capture that replays onto the same event reuses this key rather than minting a second one, and a provider that honors idempotency keys will therefore not act twice.
It is server-generated. A client-supplied idempotency key would let a browser decide whether a second debit is a duplicate — which is exactly the decision the server exists to make.
50 51 52 |
# File 'lib/clickwrap/services/authorize_external_action.rb', line 50 def self.idempotency_key_for(policy_key:, event_id:) "clickwrap-#{policy_key}-#{event_id}" end |
Instance Method Details
#call ⇒ Object
Captures the evidence and commits the pending outbox row in ONE local transaction, then returns the ExternalAction so the caller can hand its id and idempotency key to a job.
The provider call belongs after this method returns, never inside it. A transaction held open across someone else's network is a transaction holding locks on evidence rows while waiting for a stranger's DNS.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/clickwrap/services/authorize_external_action.rb', line 81 def call SchemaRequirements.require!(:external_actions) action = nil receipt = Capture.new( policy: policy, **, consume_one_time_authorizations: false, record_protected_outcome: false ).capture_and! do |pending| action = create_pending_action!(pending) end # A replayed capture returns the original receipt without re-running the # block, so the outbox row for that event already exists. Finding it is # the correct answer: the same authorization, with the same key, not a # second one that could produce a second provider call. action ||= ExternalAction.find_by(event_id: receipt.event_id) unless action raise ExternalActionError, "The evidence for #{policy.key} committed as event #{receipt.event_id}, but no " \ "pending external action was found for it. Do not call the provider: without a " \ "committed outbox row there is nothing to resolve the outcome against." end action.reload end |