spree_cm_commissioner 2.14.8 → 2.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/jobs/concerns/spree_cm_commissioner/idempotent_job.rb +7 -6
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +4 -13
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb +2 -21
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +3 -29
- data/app/models/concerns/spree_cm_commissioner/firestore_connection.rb +7 -9
- data/app/models/spree_cm_commissioner/inventory_hold.rb +5 -0
- data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +28 -17
- data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +1 -0
- data/app/services/spree_cm_commissioner/inventory_holds/release.rb +1 -0
- data/app/services/spree_cm_commissioner/queue_draft_orders/publish_status.rb +8 -8
- data/app/services/spree_cm_commissioner/redis_stock/base.rb +7 -1
- data/app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb +1 -8
- data/app/services/spree_cm_commissioner/waiting_room_system_metadata_fetcher.rb +2 -8
- data/config/initializers/firestore.rb +19 -0
- data/lib/spree_cm_commissioner/firestore_clients.rb +49 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5426f6a834611d1c303d1305de10998cea1cd5b6ca72c23f512ddff53614b2a
|
|
4
|
+
data.tar.gz: cb6688095f5daef9df50906c20d5adb5cba20dc4f2657a430b1d2ab00afaaaf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e609a94743b1283a225d077dad5ba3f775ed599985d687f8b6c4408de286bc6f8b6e91e4024e675ec03ae06a67a013fa43cc6242cc50070a334bc2ac4731e7d0
|
|
7
|
+
data.tar.gz: d007f4f9a802452c020d32f32f44c466e88469ea24386c87f8b45c834ced740d1faf55185d953901640fd1dd73f19af686667ef9f29e615b97642aadc8645020
|
data/Gemfile.lock
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module IdempotentJob
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
# The enqueuer mints one key per side effect being mirrored (e.g. one per Redis change) and
|
|
4
|
+
# passes it in the job args, so a retry or re-delivery of that job dedups while a genuinely new
|
|
5
|
+
# change always applies. No fallback: a key derived from the payload made legitimate repeats
|
|
6
|
+
# (unstock after restock, a re-acquired hold) look like duplicates and silently skipped them.
|
|
7
|
+
def with_idempotency(key, &block)
|
|
8
|
+
raise ArgumentError, "#{self.class.name} requires an idempotency_key" if key.blank?
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
def default_idempotency_key
|
|
9
|
-
"#{self.class.name}:#{job_id}"
|
|
10
|
+
SpreeCmCommissioner::IdempotencyKey.run_once(key, &block)
|
|
10
11
|
end
|
|
11
12
|
end
|
|
12
13
|
end
|
|
@@ -6,26 +6,17 @@ module SpreeCmCommissioner
|
|
|
6
6
|
queue_as :inventory_item
|
|
7
7
|
|
|
8
8
|
def perform(options = {})
|
|
9
|
-
with_idempotency(
|
|
9
|
+
with_idempotency(options[:idempotency_key]) do
|
|
10
10
|
bulk_adjust_quantities!(options)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# line items no-ops. An explicit key wins; with no line items we fall back to
|
|
18
|
-
# the per-enqueue job_id (returning nil lets with_idempotency apply that default).
|
|
19
|
-
def idempotency_key_for(options)
|
|
20
|
-
return options[:idempotency_key] if options[:idempotency_key].present?
|
|
21
|
-
return nil if options[:line_item_ids].blank?
|
|
22
|
-
|
|
23
|
-
"line_item:#{options[:line_item_ids].compact.uniq.sort.join(':')}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# :line_item_ids, :inventory_id_and_quantities, :caller_source
|
|
16
|
+
# :line_item_ids, :inventory_id_and_quantities, :caller_source, :idempotency_key
|
|
27
17
|
#
|
|
28
|
-
# :caller_source is a string like "ClassName#method"
|
|
18
|
+
# :line_item_ids is for tracing only. :caller_source is a string like "ClassName#method"
|
|
19
|
+
# identifying who enqueued the job.
|
|
29
20
|
def bulk_adjust_quantities!(options = {})
|
|
30
21
|
SpreeCmCommissioner::InventoryItems::BulkAdjustQuantities.call!(
|
|
31
22
|
inventory_id_and_quantities: options[:inventory_id_and_quantities],
|
data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb
CHANGED
|
@@ -6,33 +6,14 @@ module SpreeCmCommissioner
|
|
|
6
6
|
queue_as :inventory_item
|
|
7
7
|
|
|
8
8
|
def perform(options = {})
|
|
9
|
-
with_idempotency(
|
|
9
|
+
with_idempotency(options[:idempotency_key]) do
|
|
10
10
|
bulk_adjust_quantities_on_convert(options)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# An explicit key wins; with no order_id we fall back to the per-enqueue job_id (returning
|
|
18
|
-
# nil lets with_idempotency apply that default).
|
|
19
|
-
def idempotency_key_for(options)
|
|
20
|
-
return options[:idempotency_key] if options[:idempotency_key].present?
|
|
21
|
-
return nil if options[:order_id].blank?
|
|
22
|
-
|
|
23
|
-
"order:#{options[:order_id]}:convert:#{adjustments_fingerprint(options[:inventory_id_and_quantities])}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Readable, order-independent fingerprint of the adjustment payload, e.g. "1=2,2=-1:locked".
|
|
27
|
-
def adjustments_fingerprint(adjustments)
|
|
28
|
-
Array(adjustments)
|
|
29
|
-
.map { |adj| adj.with_indifferent_access.values_at(:inventory_id, :quantity, :locked) }
|
|
30
|
-
.sort_by { |inventory_id, quantity, locked| [inventory_id.to_s, quantity.to_s, locked.to_s] }
|
|
31
|
-
.map { |inventory_id, quantity, locked| locked ? "#{inventory_id}=#{quantity}:locked" : "#{inventory_id}=#{quantity}" }
|
|
32
|
-
.join(',')
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# :order_id, :inventory_id_and_quantities, :caller_source
|
|
16
|
+
# :order_id, :inventory_id_and_quantities, :caller_source, :idempotency_key
|
|
36
17
|
def bulk_adjust_quantities_on_convert(options = {})
|
|
37
18
|
raise ArgumentError, 'order_id is required' if options[:order_id].blank?
|
|
38
19
|
raise ArgumentError, 'inventory_id_and_quantities is required' if options[:inventory_id_and_quantities].blank?
|
|
@@ -6,42 +6,16 @@ module SpreeCmCommissioner
|
|
|
6
6
|
queue_as :inventory_item
|
|
7
7
|
|
|
8
8
|
def perform(options = {})
|
|
9
|
-
with_idempotency(
|
|
9
|
+
with_idempotency(options[:idempotency_key]) do
|
|
10
10
|
bulk_adjust_quantities_on_hold(options)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# on-hold payload no-ops while a genuinely different one re-applies. An explicit
|
|
18
|
-
# key wins; with no order_id we fall back to the per-enqueue job_id (returning
|
|
19
|
-
# nil lets with_idempotency apply that default).
|
|
20
|
-
def idempotency_key_for(options)
|
|
21
|
-
return options[:idempotency_key] if options[:idempotency_key].present?
|
|
22
|
-
return nil if options[:order_id].blank?
|
|
23
|
-
|
|
24
|
-
"order:#{options[:order_id]}:hold:#{adjustments_fingerprint(options[:inventory_id_and_quantities])}"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Readable, order-independent fingerprint of the adjustment payload, e.g. "1=2,2=-1:locked".
|
|
28
|
-
# Delimiter hierarchy: ":" separates key fields, "," separates adjustments, "=" pairs id/qty.
|
|
29
|
-
#
|
|
30
|
-
# The pool is part of the identity: a mixed order can carry a public and a locked adjustment
|
|
31
|
-
# with the same inventory_id and quantity, and without this they fingerprint identically — so
|
|
32
|
-
# one of the two would be dropped as a duplicate and its counter never mirrored.
|
|
33
|
-
def adjustments_fingerprint(adjustments)
|
|
34
|
-
Array(adjustments)
|
|
35
|
-
.map { |adj| adj.with_indifferent_access.values_at(:inventory_id, :quantity, :locked) }
|
|
36
|
-
.sort_by { |inventory_id, quantity, locked| [inventory_id.to_s, quantity.to_s, locked.to_s] }
|
|
37
|
-
.map { |inventory_id, quantity, locked| locked ? "#{inventory_id}=#{quantity}:locked" : "#{inventory_id}=#{quantity}" }
|
|
38
|
-
.join(',')
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# :order_id, :inventory_id_and_quantities, :caller_source
|
|
16
|
+
# :order_id, :inventory_id_and_quantities, :caller_source, :idempotency_key
|
|
42
17
|
#
|
|
43
|
-
# :order_id is
|
|
44
|
-
# processing, though it's not used in the adjustment itself.
|
|
18
|
+
# :order_id is required for tracing, though it's not used in the adjustment itself.
|
|
45
19
|
# :caller_source is a string like "ClassName#method" identifying who enqueued the job.
|
|
46
20
|
def bulk_adjust_quantities_on_hold(options = {})
|
|
47
21
|
raise ArgumentError, 'order_id is required' if options[:order_id].blank?
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
require 'google/cloud/firestore'
|
|
2
2
|
|
|
3
3
|
module SpreeCmCommissioner
|
|
4
|
-
#
|
|
5
|
-
# WaitingRoom::
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# Cloud Firestore access for services (see WaitingRoom::Admission::CallGuests,
|
|
5
|
+
# WaitingRoom::Positions::Stamp, WaitingRoom::Sessions::Issue) plus a #firestore_available?
|
|
6
|
+
# guard so environments without the credential (dev, test, CI) no-op instead of raising on
|
|
7
|
+
# connect. The client comes from FirestoreClients, shared per process and per project; a service
|
|
8
|
+
# for another Firebase project overrides #service_account.
|
|
8
9
|
module FirestoreConnection
|
|
9
10
|
extend ActiveSupport::Concern
|
|
10
11
|
|
|
@@ -13,14 +14,11 @@ module SpreeCmCommissioner
|
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def firestore
|
|
16
|
-
@firestore ||=
|
|
17
|
-
project_id: service_account[:project_id],
|
|
18
|
-
credentials: service_account
|
|
19
|
-
)
|
|
17
|
+
@firestore ||= SpreeCmCommissioner::FirestoreClients.fetch(service_account)
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
def service_account
|
|
23
|
-
@service_account ||=
|
|
21
|
+
@service_account ||= SpreeCmCommissioner::FirestoreClients.default_service_account
|
|
24
22
|
end
|
|
25
23
|
end
|
|
26
24
|
end
|
|
@@ -23,6 +23,11 @@ module SpreeCmCommissioner
|
|
|
23
23
|
|
|
24
24
|
store_public_metadata :total_quantity_on_hold, :integer
|
|
25
25
|
|
|
26
|
+
# The DB sync for this acquisition (see InventoryHolds::Acquire), kept so a re-entry can re-send
|
|
27
|
+
# it under the same ledger key when the original enqueue never happened.
|
|
28
|
+
store_private_metadata :on_hold_sync_idempotency_key, :string
|
|
29
|
+
store_private_metadata :on_hold_sync_payload, :array
|
|
30
|
+
|
|
26
31
|
def finalized?
|
|
27
32
|
converted? || released?
|
|
28
33
|
end
|
|
@@ -33,7 +33,10 @@ module SpreeCmCommissioner
|
|
|
33
33
|
hold = acquire_hold_for_order!(order, line_items, expires_at)
|
|
34
34
|
|
|
35
35
|
enqueue_release_job(hold)
|
|
36
|
-
|
|
36
|
+
# Re-sent on every call, including re-entry with an existing hold: if this enqueue failed
|
|
37
|
+
# after the hold committed, the retry recovers it. The key stored with the acquisition makes
|
|
38
|
+
# a repeat a no-op in the ledger once the sync has applied.
|
|
39
|
+
enqueue_sync_inventory_on_hold(hold)
|
|
37
40
|
|
|
38
41
|
CmAppLogger.log(
|
|
39
42
|
label: "#{self.class.name}#call success",
|
|
@@ -88,7 +91,11 @@ module SpreeCmCommissioner
|
|
|
88
91
|
payment_locked_at: nil,
|
|
89
92
|
release_reason: nil,
|
|
90
93
|
total_quantity_on_hold: line_items.sum(&:quantity),
|
|
91
|
-
ip_address: order.last_ip_address
|
|
94
|
+
ip_address: order.last_ip_address,
|
|
95
|
+
# Saved in the same transaction as the Redis reservation, so the DB sync for exactly
|
|
96
|
+
# this acquisition can be re-sent later without inventing a new key or payload.
|
|
97
|
+
on_hold_sync_idempotency_key: SecureRandom.uuid,
|
|
98
|
+
on_hold_sync_payload: on_hold_sync_payload(line_items)
|
|
92
99
|
)
|
|
93
100
|
|
|
94
101
|
hold.save!
|
|
@@ -107,6 +114,15 @@ module SpreeCmCommissioner
|
|
|
107
114
|
end
|
|
108
115
|
end
|
|
109
116
|
|
|
117
|
+
# Same line items and active inventory items that acquire_redis_stock! reserves.
|
|
118
|
+
def on_hold_sync_payload(line_items)
|
|
119
|
+
line_items.flat_map do |line_item|
|
|
120
|
+
line_item.inventory_items.active.map do |inventory_item|
|
|
121
|
+
{ inventory_id: inventory_item.id, quantity: line_item.quantity, locked: line_item.locked_stock? }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
110
126
|
def acquire_redis_stock!(line_items)
|
|
111
127
|
stock_keys = []
|
|
112
128
|
hold_keys = []
|
|
@@ -146,25 +162,20 @@ module SpreeCmCommissioner
|
|
|
146
162
|
SpreeCmCommissioner::InventoryHolds::ReleaseJob.set(wait_until: hold.expires_at).perform_later(hold_id: hold.id)
|
|
147
163
|
end
|
|
148
164
|
|
|
149
|
-
def enqueue_sync_inventory_on_hold(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
line_item.inventory_items.active.map do |inventory_item|
|
|
154
|
-
{
|
|
155
|
-
inventory_id: inventory_item.id,
|
|
156
|
-
quantity: line_item.quantity,
|
|
157
|
-
locked: line_item.locked_stock?
|
|
158
|
-
}
|
|
159
|
-
end
|
|
160
|
-
end
|
|
165
|
+
def enqueue_sync_inventory_on_hold(hold)
|
|
166
|
+
key = hold.on_hold_sync_idempotency_key
|
|
167
|
+
payload = hold.on_hold_sync_payload
|
|
161
168
|
|
|
162
|
-
|
|
169
|
+
# Holds acquired before the sync event was stored carry neither; their sync was sent when they
|
|
170
|
+
# were acquired.
|
|
171
|
+
return if key.blank? || payload.blank?
|
|
163
172
|
|
|
164
173
|
SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHoldJob.perform_later(
|
|
165
|
-
order_id:
|
|
174
|
+
order_id: hold.order_id,
|
|
175
|
+
idempotency_key: key,
|
|
166
176
|
caller_source: "#{self.class.name}#enqueue_sync_inventory_on_hold",
|
|
167
|
-
|
|
177
|
+
# Read back from jsonb with string keys; BulkAdjustQuantitiesOnHold reads symbol keys.
|
|
178
|
+
inventory_id_and_quantities: payload.map(&:symbolize_keys)
|
|
168
179
|
)
|
|
169
180
|
end
|
|
170
181
|
|
|
@@ -123,6 +123,7 @@ module SpreeCmCommissioner
|
|
|
123
123
|
|
|
124
124
|
SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnConvertJob.perform_later(
|
|
125
125
|
order_id: order.id,
|
|
126
|
+
idempotency_key: SecureRandom.uuid,
|
|
126
127
|
caller_source: "#{self.class.name}#enqueue_sync_inventory",
|
|
127
128
|
inventory_id_and_quantities: adjustments
|
|
128
129
|
)
|
|
@@ -128,6 +128,7 @@ module SpreeCmCommissioner
|
|
|
128
128
|
|
|
129
129
|
SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHoldJob.perform_later(
|
|
130
130
|
order_id: order.id,
|
|
131
|
+
idempotency_key: SecureRandom.uuid,
|
|
131
132
|
caller_source: "#{self.class.name}#enqueue_sync_inventory_on_hold",
|
|
132
133
|
inventory_id_and_quantities: inventory_id_and_quantities
|
|
133
134
|
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module QueueDraftOrders
|
|
3
3
|
# Writes a queue-draft-order status document's current state and appends a `histories` entry
|
|
4
|
-
# in
|
|
4
|
+
# in one batch commit, so no call site can update one without the other — and the request
|
|
5
|
+
# path (QueueDraftOrders::Enqueue) pays a single Firestore round trip, not two.
|
|
5
6
|
#
|
|
6
7
|
# Firestore layout:
|
|
7
8
|
# statuses/draft_orders/{date}/{document_id} — current status + terminal fields
|
|
@@ -21,13 +22,12 @@ module SpreeCmCommissioner
|
|
|
21
22
|
|
|
22
23
|
now = Time.zone.now
|
|
23
24
|
document = firestore.doc(document_path)
|
|
24
|
-
document.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
message: message,
|
|
29
|
-
|
|
30
|
-
)
|
|
25
|
+
history = document.col('histories').doc
|
|
26
|
+
|
|
27
|
+
firestore.batch do |batch|
|
|
28
|
+
batch.set(document, fields.merge(status: status, updated_at: now), merge: true)
|
|
29
|
+
batch.create(history, { previous_status: previous_status, next_status: status, message: message, created_at: now })
|
|
30
|
+
end
|
|
31
31
|
|
|
32
32
|
success(document_path)
|
|
33
33
|
rescue StandardError => e
|
|
@@ -28,11 +28,17 @@ module SpreeCmCommissioner
|
|
|
28
28
|
SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items).call
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# One key per Redis mutation, minted here rather than derived from line_item_ids: the same line
|
|
32
|
+
# items are legitimately synced several times (unstock on complete, restock on cancel, unstock
|
|
33
|
+
# again on resume), and a shared key made every sync after the first a silent no-op, leaving
|
|
34
|
+
# the DB behind Redis until a later sale tripped the quantity_available >= 0 validation.
|
|
35
|
+
# Carried in the job args, so a retry or re-delivery of this same job still dedups.
|
|
31
36
|
def schedule_sync_inventory(inventory_id_and_quantities)
|
|
32
37
|
SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesJob.perform_later(
|
|
33
38
|
inventory_id_and_quantities: inventory_id_and_quantities,
|
|
34
39
|
line_item_ids: @line_item_ids,
|
|
35
|
-
caller_source: "#{self.class.name}#schedule_sync_inventory"
|
|
40
|
+
caller_source: "#{self.class.name}#schedule_sync_inventory",
|
|
41
|
+
idempotency_key: SecureRandom.uuid
|
|
36
42
|
)
|
|
37
43
|
end
|
|
38
44
|
end
|
|
@@ -15,6 +15,7 @@ module SpreeCmCommissioner
|
|
|
15
15
|
class PublishLobbyPath
|
|
16
16
|
prepend ::Spree::ServiceModule::Base
|
|
17
17
|
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
18
|
+
include SpreeCmCommissioner::FirestoreConnection
|
|
18
19
|
|
|
19
20
|
def call
|
|
20
21
|
# merge: true so we never clobber the lobby's `full` / `available_slots` fields.
|
|
@@ -40,14 +41,6 @@ module SpreeCmCommissioner
|
|
|
40
41
|
def logs_document
|
|
41
42
|
@logs_document ||= lobby_document.col('logs').doc('publish_lobby_path_job')
|
|
42
43
|
end
|
|
43
|
-
|
|
44
|
-
def firestore
|
|
45
|
-
@firestore ||= Google::Cloud::Firestore.new(project_id: service_account[:project_id], credentials: service_account)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def service_account
|
|
49
|
-
@service_account ||= Rails.application.credentials.cloud_firestore_service_account
|
|
50
|
-
end
|
|
51
44
|
end
|
|
52
45
|
end
|
|
53
46
|
end
|
|
@@ -2,6 +2,8 @@ require 'google/cloud/firestore'
|
|
|
2
2
|
|
|
3
3
|
module SpreeCmCommissioner
|
|
4
4
|
class WaitingRoomSystemMetadataFetcher
|
|
5
|
+
include SpreeCmCommissioner::FirestoreConnection
|
|
6
|
+
|
|
5
7
|
attr_reader :document_data
|
|
6
8
|
|
|
7
9
|
def initialize(firestore: nil)
|
|
@@ -56,13 +58,5 @@ module SpreeCmCommissioner
|
|
|
56
58
|
def document
|
|
57
59
|
@document ||= firestore.col('metadata').doc('system')
|
|
58
60
|
end
|
|
59
|
-
|
|
60
|
-
def firestore
|
|
61
|
-
@firestore ||= Google::Cloud::Firestore.new(project_id: service_account[:project_id], credentials: service_account)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def service_account
|
|
65
|
-
@service_account ||= Rails.application.credentials.cloud_firestore_service_account
|
|
66
|
-
end
|
|
67
61
|
end
|
|
68
62
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'google/cloud/firestore'
|
|
2
|
+
|
|
3
|
+
# The generated defaults give point reads/writes a 60s–300s deadline and keep retrying
|
|
4
|
+
# RESOURCE_EXHAUSTED/UNAVAILABLE until it expires, so on a slow Firestore a request-path write
|
|
5
|
+
# (QueueDraftOrders::PublishStatus) outlives Rack::Timeout's 30s and ends as a
|
|
6
|
+
# RequestTimeoutException (an Exception, so no `rescue StandardError` sees it) instead of a clean
|
|
7
|
+
# failure. The deadline covers retries too, so this is the most one call can block.
|
|
8
|
+
#
|
|
9
|
+
# Set on the class-level config because it is every client instance's parent config, so it also
|
|
10
|
+
# reaches services that build their own client. `Google::Cloud::Firestore.new(timeout:)` would not
|
|
11
|
+
# work: the per-RPC defaults take precedence over the client-wide timeout.
|
|
12
|
+
#
|
|
13
|
+
# Queries and streams (run_query, listen, write, ...) keep their defaults — they legitimately page
|
|
14
|
+
# through large result sets and only run from jobs/dashboard reads.
|
|
15
|
+
Google::Cloud::Firestore::V1::Firestore::Client.configure do |config|
|
|
16
|
+
%i[get_document batch_get_documents create_document update_document delete_document commit batch_write].each do |rpc|
|
|
17
|
+
config.rpcs.public_send(rpc).timeout = 10.0
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'google/cloud/firestore'
|
|
2
|
+
|
|
3
|
+
module SpreeCmCommissioner
|
|
4
|
+
# Process-wide registry of Cloud Firestore clients, one per Firebase project (`project_id`), so
|
|
5
|
+
# a service that talks to a second project just passes that project's service account and still
|
|
6
|
+
# gets a shared client.
|
|
7
|
+
#
|
|
8
|
+
# Shared rather than built per service instance: each new client opens its own gRPC channel, TLS
|
|
9
|
+
# handshake and OAuth token fetch, which on request paths like QueueDraftOrders::Enqueue was paid
|
|
10
|
+
# on every request. Clients are dropped when the PID changes so a forked worker (Puma runs with
|
|
11
|
+
# preload_app!) never reuses its parent's client; checking here instead of in a puma
|
|
12
|
+
# on_worker_boot hook also covers Sidekiq, rake and any other fork. That only works while the
|
|
13
|
+
# parent has made no Firestore RPC before forking — gRPC itself refuses to run in a child after
|
|
14
|
+
# the parent used it (unless GRPC_ENABLE_FORK_SUPPORT, Linux only) — so never call Firestore
|
|
15
|
+
# during app boot. RPC deadlines are set in config/initializers/firestore.rb.
|
|
16
|
+
module FirestoreClients
|
|
17
|
+
MUTEX = Mutex.new
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# service_account: [Hash] a Firebase service-account credential; defaults to
|
|
21
|
+
# `cloud_firestore_service_account`.
|
|
22
|
+
def fetch(service_account = default_service_account)
|
|
23
|
+
project_id = service_account[:project_id]
|
|
24
|
+
|
|
25
|
+
MUTEX.synchronize do
|
|
26
|
+
reset_unlocked! if @pid != Process.pid
|
|
27
|
+
@clients[project_id] ||= Google::Cloud::Firestore.new(project_id: project_id, credentials: service_account)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def default_service_account
|
|
32
|
+
Rails.application.credentials.cloud_firestore_service_account
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Specs stub Google::Cloud::Firestore.new per example; without this the first example's
|
|
36
|
+
# double would be served to every later one.
|
|
37
|
+
def reset!
|
|
38
|
+
MUTEX.synchronize { reset_unlocked! }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def reset_unlocked!
|
|
44
|
+
@clients = {}
|
|
45
|
+
@pid = Process.pid
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -21,6 +21,7 @@ require 'spree_cm_commissioner/rate'
|
|
|
21
21
|
require 'spree_cm_commissioner/rate_closings'
|
|
22
22
|
require 'spree_cm_commissioner/rates_availability_grid'
|
|
23
23
|
require 'spree_cm_commissioner/s3_url_generator'
|
|
24
|
+
require 'spree_cm_commissioner/firestore_clients'
|
|
24
25
|
require 'spree_cm_commissioner/service_module_throwable'
|
|
25
26
|
require 'spree_cm_commissioner/serializable_transaction'
|
|
26
27
|
require 'spree_cm_commissioner/jwt_token'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.14.
|
|
4
|
+
version: 2.14.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -3176,6 +3176,7 @@ files:
|
|
|
3176
3176
|
- config/initializers/assets.rb
|
|
3177
3177
|
- config/initializers/doorkeeper.rb
|
|
3178
3178
|
- config/initializers/exception_notification.rb
|
|
3179
|
+
- config/initializers/firestore.rb
|
|
3179
3180
|
- config/initializers/google_map.rb
|
|
3180
3181
|
- config/initializers/paper_trail.rb
|
|
3181
3182
|
- config/initializers/phonelib.rb
|
|
@@ -3808,6 +3809,7 @@ files:
|
|
|
3808
3809
|
- lib/spree_cm_commissioner/distance.rb
|
|
3809
3810
|
- lib/spree_cm_commissioner/engine.rb
|
|
3810
3811
|
- lib/spree_cm_commissioner/factories.rb
|
|
3812
|
+
- lib/spree_cm_commissioner/firestore_clients.rb
|
|
3811
3813
|
- lib/spree_cm_commissioner/intercity_taxi/map_place.rb
|
|
3812
3814
|
- lib/spree_cm_commissioner/jwt_token.rb
|
|
3813
3815
|
- lib/spree_cm_commissioner/line_item_jwt_token.rb
|