Skip to content

Commit 3b63451

Browse files
joyvuu-daveclaude
andcommitted
Backfill WAS_RUNNING events for currently-running app processes
One-time data migration that inserts a WAS_RUNNING usage event for every process currently in STARTED state. Joins follow the same shape as AppUsageEventRepository#purge_and_reseed_started_apps! but the migration appends rather than truncates, generates fresh UUIDs, and sets state='WAS_RUNNING' with previous_state=NULL so consumers can distinguish synthetic baseline rows from real lifecycle events. Combined with PR cloudfoundry#4646's keep-running cleanup (extended to recognize WAS_RUNNING in the previous commit), this gives billing consumers on existing foundations a kept event per running process to bootstrap from, replacing the need for destructively_purge_all_and_reseed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b7a6983 commit 3b63451

2 files changed

Lines changed: 191 additions & 0 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
Sequel.migration do
2+
up do
3+
uuid_fn = case database_type
4+
when :postgres then 'get_uuid()'
5+
when :mysql then 'UUID()'
6+
else raise "unsupported database: #{database_type}"
7+
end
8+
9+
transaction do
10+
run <<~SQL.squish
11+
INSERT INTO app_usage_events (
12+
guid, created_at,
13+
state, previous_state,
14+
instance_count, previous_instance_count,
15+
memory_in_mb_per_instance, previous_memory_in_mb_per_instance,
16+
app_guid, app_name,
17+
parent_app_guid, parent_app_name,
18+
space_guid, space_name, org_guid,
19+
buildpack_guid, buildpack_name,
20+
package_state, previous_package_state
21+
)
22+
SELECT
23+
#{uuid_fn}, CURRENT_TIMESTAMP,
24+
'WAS_RUNNING', NULL,
25+
p.instances, p.instances,
26+
p.memory, p.memory,
27+
p.guid, parent_app.name,
28+
parent_app.guid, parent_app.name,
29+
spaces.guid, spaces.name, organizations.guid,
30+
desired_droplet.buildpack_receipt_buildpack_guid, desired_droplet.buildpack_receipt_buildpack,
31+
CASE
32+
WHEN latest_droplet.state = 'FAILED' THEN 'FAILED'
33+
WHEN latest_droplet.state = 'STAGED' AND latest_droplet.guid = parent_app.droplet_guid THEN 'STAGED'
34+
WHEN latest_package.state = 'FAILED' THEN 'FAILED'
35+
ELSE 'PENDING'
36+
END,
37+
'UNKNOWN'
38+
FROM processes p
39+
INNER JOIN apps parent_app ON parent_app.guid = p.app_guid
40+
INNER JOIN spaces ON spaces.guid = parent_app.space_guid
41+
INNER JOIN organizations ON organizations.id = spaces.organization_id
42+
LEFT JOIN droplets desired_droplet ON desired_droplet.guid = parent_app.droplet_guid
43+
LEFT JOIN (
44+
SELECT pkg.guid, pkg.app_guid, pkg.state
45+
FROM packages pkg
46+
INNER JOIN (
47+
SELECT app_guid, MAX(id) AS max_id FROM packages GROUP BY app_guid
48+
) lp_ids ON lp_ids.app_guid = pkg.app_guid AND lp_ids.max_id = pkg.id
49+
) latest_package ON latest_package.app_guid = parent_app.guid
50+
LEFT JOIN (
51+
SELECT d.guid, d.package_guid, d.state
52+
FROM droplets d
53+
INNER JOIN (
54+
SELECT package_guid, MAX(id) AS max_id FROM droplets GROUP BY package_guid
55+
) ld_ids ON ld_ids.package_guid = d.package_guid AND ld_ids.max_id = d.id
56+
) latest_droplet ON latest_droplet.package_guid = latest_package.guid
57+
WHERE p.state = 'STARTED'
58+
ORDER BY p.id
59+
SQL
60+
end
61+
end
62+
63+
down do
64+
self[:app_usage_events].where(state: 'WAS_RUNNING').delete
65+
end
66+
end
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/migration_shared_context'
3+
4+
RSpec.describe 'migration to seed WAS_RUNNING events for currently-running app processes', isolation: :truncation, type: :migration do
5+
include_context 'migration' do
6+
let(:migration_filename) { '20260428120000_seed_was_running_app_usage_events.rb' }
7+
end
8+
9+
let(:run_migration) do
10+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
11+
end
12+
13+
let(:revert_migration) do
14+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true)
15+
end
16+
17+
describe 'up migration' do
18+
context 'when there are no processes' do
19+
it 'inserts no rows' do
20+
expect { run_migration }.not_to change { db[:app_usage_events].where(state: 'WAS_RUNNING').count }.from(0)
21+
end
22+
end
23+
24+
context 'when there is one STARTED process' do
25+
let(:parent_app) { VCAP::CloudController::AppModel.make(name: 'my-app') }
26+
let!(:process) { VCAP::CloudController::ProcessModelFactory.make(app: parent_app, type: 'web', state: 'STARTED', instances: 3, memory: 512) }
27+
28+
it 'inserts one WAS_RUNNING row with the expected fields' do
29+
expect { run_migration }.to change { db[:app_usage_events].where(state: 'WAS_RUNNING').count }.from(0).to(1)
30+
31+
row = db[:app_usage_events].where(state: 'WAS_RUNNING').first
32+
expect(row[:guid]).to be_present
33+
expect(row[:state]).to eq('WAS_RUNNING')
34+
expect(row[:previous_state]).to be_nil
35+
expect(row[:app_guid]).to eq(process.guid)
36+
expect(row[:app_name]).to eq('my-app')
37+
expect(row[:parent_app_guid]).to eq(parent_app.guid)
38+
expect(row[:parent_app_name]).to eq('my-app')
39+
expect(row[:space_guid]).to eq(parent_app.space.guid)
40+
expect(row[:space_name]).to eq(parent_app.space.name)
41+
expect(row[:org_guid]).to eq(parent_app.space.organization.guid)
42+
expect(row[:instance_count]).to eq(3)
43+
expect(row[:previous_instance_count]).to eq(3)
44+
expect(row[:memory_in_mb_per_instance]).to eq(512)
45+
expect(row[:previous_memory_in_mb_per_instance]).to eq(512)
46+
expect(row[:previous_package_state]).to eq('UNKNOWN')
47+
end
48+
end
49+
50+
context 'when there is a STOPPED process' do
51+
let(:parent_app) { VCAP::CloudController::AppModel.make }
52+
let!(:process) { VCAP::CloudController::ProcessModelFactory.make(app: parent_app, state: 'STOPPED') }
53+
54+
it 'does not insert a row for the stopped process' do
55+
expect { run_migration }.not_to change { db[:app_usage_events].where(state: 'WAS_RUNNING').count }.from(0)
56+
end
57+
end
58+
59+
context 'when there is a mix of STARTED and STOPPED processes' do
60+
let(:running_app) { VCAP::CloudController::AppModel.make }
61+
let(:stopped_app) { VCAP::CloudController::AppModel.make }
62+
let!(:running_process) { VCAP::CloudController::ProcessModelFactory.make(app: running_app, state: 'STARTED') }
63+
let!(:stopped_process) { VCAP::CloudController::ProcessModelFactory.make(app: stopped_app, state: 'STOPPED') }
64+
65+
it 'inserts a WAS_RUNNING row only for the started process' do
66+
run_migration
67+
68+
rows = db[:app_usage_events].where(state: 'WAS_RUNNING').all
69+
expect(rows.size).to eq(1)
70+
expect(rows.first[:app_guid]).to eq(running_process.guid)
71+
end
72+
end
73+
74+
context 'when there are pre-existing rows in app_usage_events' do
75+
let(:parent_app) { VCAP::CloudController::AppModel.make }
76+
let!(:process) { VCAP::CloudController::ProcessModelFactory.make(app: parent_app, state: 'STARTED') }
77+
let!(:existing_event) { VCAP::CloudController::AppUsageEvent.make(state: 'STARTED', app_guid: parent_app.guid) }
78+
79+
it 'preserves the existing rows (no truncate)' do
80+
expect { run_migration }.to change(VCAP::CloudController::AppUsageEvent, :count).by(1)
81+
expect(VCAP::CloudController::AppUsageEvent.where(guid: existing_event.guid).first).to be_present
82+
end
83+
end
84+
85+
context 'when an app has a desired droplet in STAGED state' do
86+
let(:parent_app) { VCAP::CloudController::AppModel.make }
87+
let!(:process) { VCAP::CloudController::ProcessModelFactory.make(app: parent_app, state: 'STARTED') }
88+
89+
it 'sets package_state to STAGED' do
90+
run_migration
91+
row = db[:app_usage_events].where(state: 'WAS_RUNNING').first
92+
expect(row[:package_state]).to eq('STAGED')
93+
end
94+
end
95+
96+
context 'when multiple started processes exist' do
97+
let(:apps) { Array.new(3) { VCAP::CloudController::AppModel.make } }
98+
99+
before do
100+
apps.each { |app| VCAP::CloudController::ProcessModelFactory.make(app: app, state: 'STARTED') }
101+
end
102+
103+
it 'inserts one WAS_RUNNING row per running process' do
104+
expect { run_migration }.to change { db[:app_usage_events].where(state: 'WAS_RUNNING').count }.from(0).to(3)
105+
end
106+
end
107+
end
108+
109+
describe 'down migration' do
110+
let(:parent_app) { VCAP::CloudController::AppModel.make }
111+
let!(:process) { VCAP::CloudController::ProcessModelFactory.make(app: parent_app, state: 'STARTED') }
112+
let!(:unrelated_event) { VCAP::CloudController::AppUsageEvent.make(state: 'STARTED', app_guid: parent_app.guid) }
113+
114+
before { run_migration }
115+
116+
it 'removes only the WAS_RUNNING rows' do
117+
expect(db[:app_usage_events].where(state: 'WAS_RUNNING').count).to eq(1)
118+
119+
revert_migration
120+
121+
expect(db[:app_usage_events].where(state: 'WAS_RUNNING').count).to eq(0)
122+
expect(db[:app_usage_events].where(guid: unrelated_event.guid).count).to eq(1)
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)