Add sync snapshot tracking
This commit is contained in:
parent
c4f182a6a8
commit
26679d87c2
|
|
@ -19,6 +19,7 @@ class TournamentSnapshotBuilder
|
|||
playoff_teams_amount: tournament.playoff_teams_amount,
|
||||
instant_finalists_amount: tournament.instant_finalists_amount,
|
||||
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
||||
sync_generated_at: Time.current.iso8601(6),
|
||||
timer_end: tournament.timer_end&.iso8601,
|
||||
teams: teams_payload,
|
||||
stages: stages_payload,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class TournamentSnapshotImporter
|
|||
def import!
|
||||
Tournament.transaction do
|
||||
tournament.lock!
|
||||
return tournament if stale_snapshot?
|
||||
|
||||
existing_teams = tournament.teams.to_a
|
||||
existing_stages = tournament.stages.to_a
|
||||
|
|
@ -59,6 +60,7 @@ class TournamentSnapshotImporter
|
|||
root_attrs[:code] = tournament.code if code_taken_by_another_tournament?(root_attrs[:code])
|
||||
root_attrs[:timer_end] = parse_time(snapshot[:timer_end])
|
||||
root_attrs[:sync_source_tournament_id] = snapshot[:id]
|
||||
root_attrs[:sync_last_imported_snapshot_at] = snapshot_generated_at
|
||||
tournament.update!(root_attrs)
|
||||
end
|
||||
|
||||
|
|
@ -162,6 +164,17 @@ class TournamentSnapshotImporter
|
|||
Time.zone.parse(value)
|
||||
end
|
||||
|
||||
def snapshot_generated_at
|
||||
parse_time(snapshot[:sync_generated_at])
|
||||
end
|
||||
|
||||
def stale_snapshot?
|
||||
return false if snapshot_generated_at.nil?
|
||||
return false if tournament.sync_last_imported_snapshot_at.nil?
|
||||
|
||||
snapshot_generated_at <= tournament.sync_last_imported_snapshot_at
|
||||
end
|
||||
|
||||
def code_taken_by_another_tournament?(code)
|
||||
return false if code.blank?
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSyncSnapshotTrackingToTournaments < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :tournaments, :sync_last_imported_snapshot_at, :datetime
|
||||
end
|
||||
end
|
||||
|
|
@ -28,13 +28,16 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
|
||||
|
||||
it 'pushes full source lifecycle into read only follower tournament' do
|
||||
log_phase!('seed unrelated tournaments')
|
||||
source_noise = seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Noise', range: 2..4)
|
||||
follower_noise = seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Noise', range: 5..7)
|
||||
|
||||
log_phase!('create source and follower tournaments')
|
||||
source = create_group_stage_tournament(client: source_owner_client, public: false, name_prefix: 'Sync Source')
|
||||
follower = create_follower_tournament(client: follower_owner_client, public: true, name_prefix: 'Sync Follower')
|
||||
expect(source.fetch(:id)).not_to eq(follower.fetch(:id))
|
||||
|
||||
log_phase!('configure sync and verify initial replication')
|
||||
configure_sync = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||
sync_target_url: "#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state",
|
||||
sync_auth_token: sync_token
|
||||
|
|
@ -47,6 +50,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty
|
||||
|
||||
log_phase!('tighten playoff cutoff and rename teams')
|
||||
update_cutoff = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||
playoff_teams_amount: 4,
|
||||
instant_finalists_amount: 4,
|
||||
|
|
@ -68,9 +72,11 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise', range: 3..4))
|
||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
||||
|
||||
log_phase!('play group stage with deciders')
|
||||
play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) })
|
||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
||||
|
||||
log_phase!('verify playoff generation replicated')
|
||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||
|
|
@ -87,9 +93,11 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise', range: 2..5))
|
||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
||||
|
||||
log_phase!('finish playoff bracket')
|
||||
finish_playoff_bracket!(source_id: source.fetch(:id))
|
||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
||||
|
||||
log_phase!('verify final follower state and takeover')
|
||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||
|
|
@ -104,6 +112,11 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
expect(takeover_update[:status]).to eq(200)
|
||||
end
|
||||
|
||||
def log_phase!(message)
|
||||
puts "phase #{message}"
|
||||
$stdout.flush
|
||||
end
|
||||
|
||||
def login_client(base_url:, email:, password:)
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
response = client.login!(email: email, password: password)
|
||||
|
|
|
|||
|
|
@ -20,5 +20,22 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
expect(follower.matches.count).to eq(source.matches.count)
|
||||
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map { |team| team.fetch(:id) })
|
||||
end
|
||||
|
||||
it 'ignores stale snapshots that arrive after a newer one' do
|
||||
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
|
||||
|
||||
newer_source = create(:tournament, name: 'Newer name')
|
||||
older_source = create(:tournament, name: 'Older name')
|
||||
|
||||
newer_snapshot = TournamentSnapshotBuilder.build(newer_source).merge(sync_generated_at: '2026-04-14T10:30:02Z')
|
||||
older_snapshot = TournamentSnapshotBuilder.build(older_source).merge(sync_generated_at: '2026-04-14T10:30:01Z')
|
||||
|
||||
described_class.import!(tournament: follower, snapshot: newer_snapshot)
|
||||
described_class.import!(tournament: follower, snapshot: older_snapshot)
|
||||
|
||||
follower.reload
|
||||
expect(follower.name).to eq('Newer name')
|
||||
expect(follower.sync_last_imported_snapshot_at).to eq(Time.zone.parse('2026-04-14T10:30:02Z'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue