Skip to content

Commit 05b4600

Browse files
committed
Keep usage event records of running apps and services
- Add keep_running_records parameter to OldRecordCleanup - Implement exclude_running_records method to retain START/CREATED records without corresponding STOP/DELETED records - Enable keep_running_records for AppUsageEvent and ServiceUsageEvent cleanup - Add indexes for increasing performance on state queries - Add comprehensive unit tests The exclude_running_records method identifies START records that have matching STOP records while keeping START records without matching STOPs. This prevents premature deletion of usage event records for long-running apps and services, ensuring consumers can always determine the true start time of active resources.
1 parent a385f18 commit 05b4600

File tree

7 files changed

+455
-15
lines changed

7 files changed

+455
-15
lines changed

app/repositories/app_usage_event_repository.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def purge_and_reseed_started_apps!
152152
end
153153

154154
def delete_events_older_than(cutoff_age_in_days)
155-
Database::OldRecordCleanup.new(AppUsageEvent, cutoff_age_in_days: cutoff_age_in_days, keep_at_least_one_record: true).delete
155+
Database::OldRecordCleanup.new(AppUsageEvent, cutoff_age_in_days: cutoff_age_in_days, keep_at_least_one_record: true, keep_running_records: true).delete
156156
end
157157

158158
private

app/repositories/service_usage_event_repository.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def purge_and_reseed_service_instances!
9292
end
9393

9494
def delete_events_older_than(cutoff_age_in_days)
95-
Database::OldRecordCleanup.new(ServiceUsageEvent, cutoff_age_in_days: cutoff_age_in_days, keep_at_least_one_record: true).delete
95+
Database::OldRecordCleanup.new(ServiceUsageEvent, cutoff_age_in_days: cutoff_age_in_days, keep_at_least_one_record: true, keep_running_records: true).delete
9696
end
9797
end
9898
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Sequel.migration do
2+
no_transaction # to use the 'concurrently' option
3+
4+
up do
5+
if database_type == :postgres
6+
VCAP::Migration.with_concurrent_timeout(self) do
7+
add_index :app_usage_events, %i[state app_guid id],
8+
name: :app_usage_events_lifecycle_index,
9+
if_not_exists: true,
10+
concurrently: true
11+
12+
add_index :service_usage_events, %i[state service_instance_guid id],
13+
name: :service_usage_events_lifecycle_index,
14+
if_not_exists: true,
15+
concurrently: true
16+
end
17+
18+
elsif database_type == :mysql
19+
alter_table :app_usage_events do
20+
# rubocop:disable Sequel/ConcurrentIndex
21+
add_index %i[state app_guid id], name: :app_usage_events_lifecycle_index unless @db.indexes(:app_usage_events).include?(:app_usage_events_lifecycle_index)
22+
# rubocop:enable Sequel/ConcurrentIndex
23+
end
24+
25+
alter_table :service_usage_events do
26+
# rubocop:disable Sequel/ConcurrentIndex
27+
unless @db.indexes(:service_usage_events).include?(:service_usage_events_lifecycle_index)
28+
add_index %i[state service_instance_guid id],
29+
name: :service_usage_events_lifecycle_index
30+
end
31+
# rubocop:enable Sequel/ConcurrentIndex
32+
end
33+
end
34+
end
35+
36+
down do
37+
if database_type == :postgres
38+
VCAP::Migration.with_concurrent_timeout(self) do
39+
drop_index :app_usage_events, %i[state app_guid id],
40+
name: :app_usage_events_lifecycle_index,
41+
if_exists: true,
42+
concurrently: true
43+
44+
drop_index :service_usage_events, %i[state service_instance_guid id],
45+
name: :service_usage_events_lifecycle_index,
46+
if_exists: true,
47+
concurrently: true
48+
end
49+
end
50+
51+
if database_type == :mysql
52+
alter_table :app_usage_events do
53+
# rubocop:disable Sequel/ConcurrentIndex
54+
drop_index %i[state app_guid id], name: :app_usage_events_lifecycle_index if @db.indexes(:app_usage_events).include?(:app_usage_events_lifecycle_index)
55+
# rubocop:enable Sequel/ConcurrentIndex
56+
end
57+
58+
alter_table :service_usage_events do
59+
# rubocop:disable Sequel/ConcurrentIndex
60+
if @db.indexes(:service_usage_events).include?(:service_usage_events_lifecycle_index)
61+
drop_index %i[state service_instance_guid id],
62+
name: :service_usage_events_lifecycle_index
63+
end
64+
# rubocop:enable Sequel/ConcurrentIndex
65+
end
66+
end
67+
end
68+
end

lib/database/old_record_cleanup.rb

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
module Database
44
class OldRecordCleanup
55
class NoCurrentTimestampError < StandardError; end
6-
attr_reader :model, :cutoff_age_in_days, :keep_at_least_one_record
6+
attr_reader :model, :cutoff_age_in_days, :keep_at_least_one_record, :keep_running_records
77

8-
def initialize(model, cutoff_age_in_days:, keep_at_least_one_record: false)
8+
def initialize(model, cutoff_age_in_days:, keep_at_least_one_record: false, keep_running_records: false)
99
@model = model
1010
@cutoff_age_in_days = cutoff_age_in_days
1111
@keep_at_least_one_record = keep_at_least_one_record
12+
@keep_running_records = keep_running_records
1213
end
1314

1415
def delete
@@ -21,6 +22,8 @@ def delete
2122
end
2223
logger.info("Cleaning up #{old_records.count} #{model.table_name} table rows")
2324

25+
old_records = exclude_running_records(old_records) if keep_running_records
26+
2427
Database::BatchDelete.new(old_records, 1000).delete
2528
end
2629

@@ -35,5 +38,66 @@ def current_timestamp_from_database
3538
def logger
3639
@logger ||= Steno.logger('cc.old_record_cleanup')
3740
end
41+
42+
def exclude_running_records(old_records)
43+
return old_records unless has_duration?(model)
44+
45+
beginning_string = beginning_string(model)
46+
ending_string = ending_string(model)
47+
guid_symbol = guid_symbol(model)
48+
49+
raise "Invalid duration model: #{model}" if beginning_string.nil? || ending_string.nil? || guid_symbol.nil?
50+
51+
# Create subqueries for START and STOP records within the old records set
52+
# Using from_self creates a subquery, allowing us to reference these in complex joins
53+
initial_records = old_records.where(state: beginning_string).from_self(alias: :initial_records)
54+
final_records = old_records.where(state: ending_string).from_self(alias: :final_records)
55+
56+
# For each START record, check if there exists a STOP record that:
57+
# 1. Has the same resource GUID (app_guid or service_instance_guid)
58+
# 2. Was created AFTER the START record (higher ID implies later creation)
59+
exists_condition = final_records.where(Sequel[:final_records][guid_symbol] => Sequel[:initial_records][guid_symbol]).where do
60+
Sequel[:final_records][:id] > Sequel[:initial_records][:id]
61+
end.select(1).exists
62+
63+
prunable_initial_records = initial_records.where(exists_condition)
64+
65+
# Include records with states other than START/STOP
66+
other_records = old_records.exclude(state: [beginning_string, ending_string])
67+
68+
# Return the UNION of:
69+
# 1. START records that have a matching STOP (safe to delete)
70+
# 2. All STOP records (always safe to delete)
71+
# 3. Other state records (always safe to delete)
72+
prunable_initial_records.union(final_records, all: true).union(other_records, all: true)
73+
end
74+
75+
def has_duration?(model)
76+
return true if model == VCAP::CloudController::AppUsageEvent
77+
return true if model == VCAP::CloudController::ServiceUsageEvent
78+
79+
false
80+
end
81+
82+
def beginning_string(model)
83+
return VCAP::CloudController::ProcessModel::STARTED if model == VCAP::CloudController::AppUsageEvent
84+
return VCAP::CloudController::Repositories::ServiceUsageEventRepository::CREATED_EVENT_STATE if model == VCAP::CloudController::ServiceUsageEvent
85+
86+
nil
87+
end
88+
89+
def ending_string(model)
90+
return VCAP::CloudController::ProcessModel::STOPPED if model == VCAP::CloudController::AppUsageEvent
91+
return VCAP::CloudController::Repositories::ServiceUsageEventRepository::DELETED_EVENT_STATE if model == VCAP::CloudController::ServiceUsageEvent
92+
93+
nil
94+
end
95+
96+
def guid_symbol(model)
97+
return :app_guid if model == VCAP::CloudController::AppUsageEvent
98+
return :service_instance_guid if model == VCAP::CloudController::ServiceUsageEvent
99+
100+
nil
101+
end
38102
end
39103
end

spec/unit/jobs/runtime/app_usage_events_cleanup_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Jobs::Runtime
55
RSpec.describe AppUsageEventsCleanup, job_context: :worker do
66
let(:cutoff_age_in_days) { 30 }
77
let(:logger) { double(Steno::Logger, info: nil) }
8-
let!(:event_before_threshold) { AppUsageEvent.make(created_at: (cutoff_age_in_days + 1).days.ago) }
8+
let!(:event_before_threshold) { AppUsageEvent.make(created_at: (cutoff_age_in_days + 1).days.ago, state: 'STOPPED') }
99
let!(:event_after_threshold) { AppUsageEvent.make(created_at: (cutoff_age_in_days - 1).days.ago) }
1010

1111
subject(:job) do

spec/unit/jobs/services/service_usage_events_cleanup_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Jobs::Services
55
RSpec.describe ServiceUsageEventsCleanup, job_context: :worker do
66
let(:cutoff_age_in_days) { 30 }
77
let(:logger) { double(Steno::Logger, info: nil) }
8-
let!(:event_before_threshold) { ServiceUsageEvent.make(created_at: (cutoff_age_in_days + 1).days.ago) }
8+
let!(:event_before_threshold) { ServiceUsageEvent.make(created_at: (cutoff_age_in_days + 1).days.ago, state: 'DELETED') }
99
let!(:event_after_threshold) { ServiceUsageEvent.make(created_at: (cutoff_age_in_days - 1).days.ago) }
1010

1111
subject(:job) do

0 commit comments

Comments
 (0)