Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions app/services/events/post_process_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ def initialize(event:)
end

def call
expire_cached_charges
create_enriched_events
track_subscription_activity
customer&.flag_wallets_for_refresh
Expand Down Expand Up @@ -81,16 +80,6 @@ def billable_metric
@billable_metric ||= organization.billable_metrics.find_by(code: event.code)
end

def expire_cached_charges
return if organization.feature_flag_enabled?(:lazy_charge_usage_cache)
return if active_subscription.nil?
return unless billable_metric

charges_and_filters.each do |charge, filter|
Subscriptions::ChargeCacheService.expire_cache(subscription: active_subscription, charge:, charge_filter: filter)
end
end

def create_enriched_events
return unless organization.feature_flag_enabled?(:postgres_enriched_events)
return if active_subscription.nil?
Expand Down
4 changes: 2 additions & 2 deletions app/services/invoices/customer_usage_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,10 @@ def charge_cache_enabled?
(!usage_filters.full_usage || full_usage_cache_enabled?)
end

# Full usage is cached only with lazy validation, the one invalidation that clears its key.
# skip_grouping and filter_by_presentation change the fees but are absent from the cache key, so
# a full usage entry is only written when neither of them narrows the request.
def full_usage_cache_enabled?
organization.granular_lifetime_usage_enabled? &&
organization.feature_flag_enabled?(:lazy_charge_usage_cache) &&
!usage_filters.skip_grouping &&
usage_filters.filter_by_presentation.nil?
end
Expand Down
22 changes: 5 additions & 17 deletions app/services/subscriptions/charge_cache_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

module Subscriptions
class ChargeCacheService < CacheService
CACHE_KEY_VERSION = "1"
# Lazy validation stores a different value shape (wrapped with its creation time), so it uses
# its own version. Enabling the feature flag gradually migrates an organization's entries to
# this version instead of invalidating every organization's cache at once.
LAZY_CACHE_KEY_VERSION = "2"
# Bumped to "2" when lazy validation was introduced: it stores a different value shape (wrapped
# with its creation time) than the legacy eager-invalidation entries.
CACHE_KEY_VERSION = "2"

# Full usage aggregates from subscription.started_at, not the current period start. The two
# windows can coincide, but started_at is editable, so they never share an entry.
Expand Down Expand Up @@ -57,7 +55,7 @@ def initialize(subscription:, charge:, charge_filter: nil, full_usage: false, ex
def cache_key
[
"charge-usage",
cache_key_version,
CACHE_KEY_VERSION,
charge.id,
subscription.id,
charge.updated_at.iso8601,
Expand All @@ -71,18 +69,8 @@ def cache_key

attr_reader :subscription, :charge, :charge_filter, :full_usage

def cache_key_version
lazy_validation? ? LAZY_CACHE_KEY_VERSION : CACHE_KEY_VERSION
end

def track_created_at?
lazy_validation?
end

def lazy_validation?
return @lazy_validation if defined?(@lazy_validation)

@lazy_validation = subscription.organization.feature_flag_enabled?(:lazy_charge_usage_cache)
true
end
end
end
38 changes: 3 additions & 35 deletions spec/services/events/post_process_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,6 @@
.with(subscription:, organization:, date: expected_date)
end

describe "charge usage cache expiration" do
it "expires the charge usage cache for the matching charges" do
allow(Subscriptions::ChargeCacheService).to receive(:expire_cache)

process_service.call

expect(Subscriptions::ChargeCacheService).to have_received(:expire_cache)
.with(subscription:, charge:, charge_filter: nil)
end

context "when the lazy charge usage cache flag is enabled" do
let(:organization) { create(:organization, feature_flags: [:lazy_charge_usage_cache]) }

it "does not expire the cache, letting the read-time validation handle it" do
allow(Subscriptions::ChargeCacheService).to receive(:expire_cache)

process_service.call

expect(Subscriptions::ChargeCacheService).not_to have_received(:expire_cache)
end
end
end

context "with events enrichment" do
it "does not create an enriched event" do
expect { process_service.call }.not_to change(EnrichedEvent, :count)
Expand All @@ -95,15 +72,6 @@
end
let(:event_properties) { {"item_id" => "12"} }

it "falls back on the currently active subscription to expire the charge usage cache" do
allow(Subscriptions::ChargeCacheService).to receive(:expire_cache)

process_service.call

expect(Subscriptions::ChargeCacheService).to have_received(:expire_cache)
.with(subscription:, charge:, charge_filter: nil)
end

it "tracks subscription activity on the fallback subscription" do
allow(UsageMonitoring::TrackSubscriptionActivityService).to receive(:call)

Expand All @@ -119,12 +87,12 @@
end

context "with a non-recurring billable metric" do
it "does not fall back and skips the charge usage cache expiration" do
allow(Subscriptions::ChargeCacheService).to receive(:expire_cache)
it "does not fall back on the currently active subscription" do
allow(UsageMonitoring::TrackSubscriptionActivityService).to receive(:call)

process_service.call

expect(Subscriptions::ChargeCacheService).not_to have_received(:expire_cache)
expect(UsageMonitoring::TrackSubscriptionActivityService).not_to have_received(:call)
end
end
end
Expand Down
61 changes: 12 additions & 49 deletions spec/services/invoices/customer_usage_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
)
end

# created_at predates the aggregation: CacheService refuses to store a value whose watermark is
# younger than SETTLE_WINDOW, so freshly ingested events would never populate the charge cache.
let(:events) do
create_list(
:event,
Expand All @@ -52,7 +54,8 @@
subscription:,
customer:,
code: billable_metric.code,
timestamp:
timestamp:,
created_at: 1.hour.ago
)
end

Expand Down Expand Up @@ -540,7 +543,6 @@
end

context "when granular_lifetime_usage is enabled", :premium do
# Both keys are built through the service so their version follows the lazy validation flag.
let(:current_usage_cache_key) do
Subscriptions::ChargeCacheService.new(subscription:, charge:).cache_key
end
Expand All @@ -549,10 +551,7 @@
Subscriptions::ChargeCacheService.new(subscription:, charge:, full_usage: true).cache_key
end

before do
organization.update!(premium_integrations: %w[granular_lifetime_usage])
organization.enable_feature_flag!(:lazy_charge_usage_cache)
end
before { organization.update!(premium_integrations: %w[granular_lifetime_usage]) }

context "when filter_by_charge_id is provided and no prorated charges" do
subject(:usage_service) do
Expand Down Expand Up @@ -750,18 +749,6 @@
expect(Rails.cache.exist?(current_usage_cache_key)).to be(false)
end
end

context "when the organization does not lazily validate the cache" do
before { organization.disable_feature_flag!(:lazy_charge_usage_cache) }

it "does not cache the charge at all" do
travel_to(current_date) do
expect { usage_service.call }.not_to change { Rails.cache.exist?(full_usage_cache_key) }.from(false)

expect(Rails.cache.exist?(current_usage_cache_key)).to be(false)
end
end
end
end
end
end
Expand Down Expand Up @@ -899,7 +886,6 @@
end
end

# Full usage is cached only where lazy validation can reject a stale entry.
context "when the full usage is queried outside of the first billing period", :premium do
subject(:usage_service) do
described_class.new(
Expand All @@ -921,31 +907,13 @@

before { organization.update!(premium_integrations: %w[granular_lifetime_usage]) }

it "skips both the cache and the ingestion timestamps" do
expect { usage_service.call }.not_to change { Rails.cache.exist?(full_usage_cache_key) }.from(false)
it "caches the charge under the full usage key and requests the ingestion timestamps" do
expect { usage_service.call }
.to change { Rails.cache.exist?(full_usage_cache_key) }.from(false).to(true)

expect(Rails.cache.exist?(current_usage_cache_key)).to be(false)
expect(Events::BillingPeriodFilterService).to have_received(:for_charges!)
.with(hash_including(with_last_seen_at: false))
end

context "when the organization lazily validates the cache" do
# created_at predates the aggregation: CacheService refuses to store a value whose
# watermark is younger than SETTLE_WINDOW.
let(:events) do
create_list(:event, 2, organization:, subscription:, customer:,
code: billable_metric.code, timestamp:, created_at: 1.hour.ago)
end

before { organization.enable_feature_flag!(:lazy_charge_usage_cache) }

it "caches the charge under the full usage key and requests the ingestion timestamps" do
expect { usage_service.call }
.to change { Rails.cache.exist?(full_usage_cache_key) }.from(false).to(true)

expect(Rails.cache.exist?(current_usage_cache_key)).to be(false)
expect(Events::BillingPeriodFilterService).to have_received(:for_charges!)
.with(hash_including(with_last_seen_at: true))
end
.with(hash_including(with_last_seen_at: true))
end
end

Expand All @@ -961,8 +929,6 @@
)
end

before { organization.enable_feature_flag!(:lazy_charge_usage_cache) }

it "refuses the request and caches nothing" do
result = usage_service.call

Expand Down Expand Up @@ -993,11 +959,8 @@
Subscriptions::ChargeCacheService.new(subscription:, charge:).cache_key
end

# Lazy validation is on, so the refusal can only come from the filter shape.
before do
organization.update!(premium_integrations: %w[granular_lifetime_usage])
organization.enable_feature_flag!(:lazy_charge_usage_cache)
end
# granular_lifetime_usage is on, so the refusal can only come from the filter shape.
before { organization.update!(premium_integrations: %w[granular_lifetime_usage]) }

it "skips both the cache and the ingestion timestamps" do
expect { usage_service.call }.not_to change { Rails.cache.exist?(full_usage_cache_key) }.from(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
end

before do
subscription.organization.enable_feature_flag!(:lazy_charge_usage_cache)
Rails.cache.clear
end

Expand Down
9 changes: 0 additions & 9 deletions spec/services/subscriptions/charge_cache_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@
end
end

context "when the lazy charge usage cache flag is enabled" do
before { subscription.organization.enable_feature_flag!(:lazy_charge_usage_cache) }

it "uses the lazy cache key version" do
expect(cache_service.cache_key)
.to eq("charge-usage/#{described_class::LAZY_CACHE_KEY_VERSION}/#{charge.id}/#{subscription.id}/#{charge.updated_at.iso8601}")
end
end

context "with full usage" do
subject(:cache_service) { described_class.new(subscription:, charge:, charge_filter:, full_usage: true) }

Expand Down
Loading