Skip to content

Commit 048ce1a

Browse files
authored
Regional leaderboards persist to database (#494)
1 parent 3a80326 commit 048ce1a

File tree

6 files changed

+81
-194
lines changed

6 files changed

+81
-194
lines changed

app/jobs/leaderboard_update_job.rb

Lines changed: 61 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,78 @@ class LeaderboardUpdateJob < ApplicationJob
1010
drop: true
1111
)
1212

13-
def perform(period = :daily, date = Date.current)
13+
def perform(period = :daily, date = Date.current, force_update: false)
1414
date = LeaderboardDateRange.normalize_date(date, period)
1515

16-
Rails.logger.info "Starting leaderboard generation for #{period} on #{date}"
16+
# global
17+
build_leaderboard(date, period, nil, nil, force_update)
1718

18-
board = build_global(date, period)
19-
build_timezones(date, period)
19+
# Build timezone leaderboards
20+
range = LeaderboardDateRange.calculate(date, period)
21+
timezones_for_users_in(range).each do |timezone|
22+
offset = User.timezone_to_utc_offset(timezone)
23+
build_leaderboard(date, period, offset, timezone, force_update)
24+
end
25+
end
2026

21-
Rails.logger.info "Completed leaderboard generation for #{period} on #{date}"
27+
private
2228

23-
board
24-
rescue => e
25-
Rails.logger.error "Failed to update leaderboard: #{e.message}"
26-
Honeybadger.notify(e, context: { period: period, date: date })
27-
raise
29+
def timezones_for_users_in(range)
30+
# Expand range by 1 day in both directions to catch users in all timezones
31+
expanded_range = (range.begin - 1.day)...(range.end + 1.day)
32+
33+
User.joins(:heartbeats)
34+
.where(heartbeats: { time: expanded_range })
35+
.where.not(timezone: nil)
36+
.distinct
37+
.pluck(:timezone)
38+
.compact
2839
end
2940

30-
private
3141

32-
def build_global(date, period)
33-
range = LeaderboardDateRange.calculate(date, period)
42+
def build_leaderboard(date, period, timezone_offset = nil, timezone = nil, force_update = false)
3443
board = ::Leaderboard.find_or_create_by!(
3544
start_date: date,
3645
period_type: period,
37-
timezone_utc_offset: nil
38-
) do |lb|
39-
lb.finished_generating_at = nil
46+
timezone_utc_offset: timezone_offset
47+
)
48+
49+
return board if board.finished_generating_at.present? && !force_update
50+
51+
if timezone_offset
52+
Rails.logger.info "Building timezone leaderboard for #{timezone} (UTC#{timezone_offset >= 0 ? '+' : ''}#{timezone_offset})"
53+
else
54+
Rails.logger.info "Building global leaderboard"
4055
end
4156

42-
return board if board.finished_generating_at.present?
57+
# Calculate timezone-aware range
58+
range = if timezone
59+
Time.use_zone(timezone) { LeaderboardDateRange.calculate(date, period) }
60+
else
61+
LeaderboardDateRange.calculate(date, period)
62+
end
4363

4464
ActiveRecord::Base.transaction do
4565
board.entries.delete_all
46-
data = Heartbeat.where(time: range)
47-
.with_valid_timestamps
48-
.joins(:user)
49-
.coding_only
50-
.where.not(users: { github_uid: nil })
51-
.group(:user_id)
52-
.duration_seconds
5366

54-
data = data.filter { |_, seconds| seconds > 60 }
67+
# Build the base heartbeat query
68+
heartbeat_query = Heartbeat.where(time: range)
69+
.with_valid_timestamps
70+
.joins(:user)
71+
.coding_only
72+
.where.not(users: { github_uid: nil })
73+
.where.not(users: { trust_level: User.trust_levels[:red] })
74+
75+
# Filter by timezone if specified
76+
if timezone_offset
77+
users_in_tz = User.users_in_timezone_offset(timezone_offset).not_convicted
78+
user_ids = users_in_tz.pluck(:id)
79+
return board if user_ids.empty?
80+
heartbeat_query = heartbeat_query.where(user_id: user_ids)
81+
end
5582

56-
convicted = User.where(trust_level: User.trust_levels[:red]).pluck(:id)
57-
data = data.reject { |user_id, _| convicted.include?(user_id) }
83+
data = heartbeat_query.group(:user_id).duration_seconds
84+
.filter { |_, seconds| seconds > 60 }
5885

5986
streaks = Heartbeat.daily_streaks_for_users(data.keys)
6087

@@ -73,41 +100,15 @@ def build_global(date, period)
73100
board.update!(finished_generating_at: Time.current)
74101
end
75102

76-
key = LeaderboardCache.global_key(period, date)
77-
LeaderboardCache.write(key, board)
78-
79-
board
80-
end
81-
82-
def build_timezones(date, period)
83-
range = LeaderboardDateRange.calculate(date, period)
84-
85-
user_timezones = User.joins(:heartbeats)
86-
.where(heartbeats: { time: range })
87-
.where.not(timezone: nil)
88-
.distinct
89-
.pluck(:timezone)
90-
.compact
91-
92-
offsets = user_timezones.map { |tz| User.timezone_to_utc_offset(tz) }.compact.uniq
93-
94-
Rails.logger.info "Generating timezone leaderboards for #{offsets.size} active UTC offsets"
95-
96-
offsets.each do |offset|
97-
build_timezone(date, period, offset)
98-
end
99-
end
100-
101-
def build_timezone(date, period, offset)
102-
key = LeaderboardCache.timezone_key(offset, date, period)
103+
# Cache the board
104+
cache_key = timezone_offset ?
105+
LeaderboardCache.timezone_key(timezone_offset, date, period) :
106+
LeaderboardCache.global_key(period, date)
103107

104-
data = LeaderboardCache.fetch(key) do
105-
users = User.users_in_timezone_offset(offset).not_convicted
106-
LeaderboardBuilder.build_for_users(users, date, "UTC#{offset >= 0 ? '+' : ''}#{offset}", period)
107-
end
108+
LeaderboardCache.write(cache_key, board)
108109

109-
Rails.logger.debug "Cached timezone leaderboard for UTC#{offset >= 0 ? '+' : ''}#{offset} with #{data&.entries&.size || 0} entries"
110+
Rails.logger.debug "Persisted #{timezone_offset ? 'timezone' : 'global'} leaderboard with #{board.entries.count} entries"
110111

111-
data
112+
board
112113
end
113114
end

app/jobs/timezone_leaderboard_job.rb

Lines changed: 0 additions & 41 deletions
This file was deleted.

app/jobs/timezone_leaderboard_update_job.rb

Lines changed: 0 additions & 80 deletions
This file was deleted.

app/services/leaderboard_service.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,31 @@ def get(period: :daily, date: Date.current, offset: nil)
1818
private
1919

2020
def get_timezone(date, period, offset)
21+
date = LeaderboardDateRange.normalize_date(date, period)
2122
key = LeaderboardCache.timezone_key(offset, date, period)
2223
board = LeaderboardCache.read(key)
2324

25+
return board if board.present?
26+
27+
board = ::Leaderboard.where.not(finished_generating_at: nil)
28+
.find_by(start_date: date, period_type: period, timezone_utc_offset: offset, deleted_at: nil)
29+
2430
if board.present?
25-
Rails.logger.debug "Cache HIT for timezone leaderboard UTC#{offset >= 0 ? '+' : ''}#{offset}"
31+
LeaderboardCache.write(key, board)
2632
return board
2733
end
2834

29-
Rails.logger.debug "Cache MISS for timezone leaderboard UTC#{offset >= 0 ? '+' : ''}#{offset}"
30-
31-
TimezoneLeaderboardJob.perform_later(period, date, offset)
32-
Rails.logger.info "Falling back to global leaderboard for UTC#{offset >= 0 ? '+' : ''}#{offset}"
35+
::LeaderboardUpdateJob.perform_later(period, date)
3336
get_global(date, period)
3437
end
3538

3639
def get_global(date, period)
3740
date = LeaderboardDateRange.normalize_date(date, period)
3841
key = LeaderboardCache.global_key(period, date)
3942
board = LeaderboardCache.read(key)
43+
4044
return board if board.present?
45+
4146
board = ::Leaderboard.where.not(finished_generating_at: nil)
4247
.find_by(start_date: date, period_type: period, timezone_utc_offset: nil, deleted_at: nil)
4348

@@ -46,8 +51,7 @@ def get_global(date, period)
4651
return board
4752
end
4853

49-
Rails.logger.info "No leaderboard found for #{period} #{date}, triggering background generation"
50-
LeaderboardUpdateJob.perform_later(period, date)
54+
::LeaderboardUpdateJob.perform_later(period, date)
5155
nil
5256
end
5357
end

app/views/leaderboards/index.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323

2424
<div class="inline-flex rounded-full p-1 mb-4 ml-4">
2525
<%= link_to "Timezone", leaderboards_path(period_type: @period_type, scope: 'timezone'),
26-
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@scope == 'timezone' ? ' text-white' : 'text-muted hover:text-white'}", style: "background:none; border:none;" %>
26+
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@scope == 'timezone' ? ' bg-primary text-white' : 'text-muted hover:text-white'}", style: "background:none; border:none;" %>
2727
<%= link_to "Regional", leaderboards_path(period_type: @period_type, scope: 'regional'),
28-
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@scope == 'regional' ? ' text-white' : 'text-muted hover:text-white'}", style: "background:none; border:none;" %>
28+
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@scope == 'regional' ? ' bg-primary text-white' : 'text-muted hover:text-white'}", style: "background:none; border:none;" %>
2929
<%= link_to "Global", leaderboards_path(period_type: @period_type, scope: 'global'),
30-
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@scope == 'global' ? ' text-white' : 'text-muted hover:text-white'}", style: "background:none; border:none;" %>
30+
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@scope == 'global' ? ' bg-primary text-white' : 'text-muted hover:text-white'}", style: "background:none; border:none;" %>
3131
</div>
3232

3333
<% if current_user && current_user.github_uid.blank? %>

config/initializers/good_job.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,20 @@
3030
daily_leaderboard_update: {
3131
cron: "* * * * *",
3232
class: "LeaderboardUpdateJob",
33-
args: [ :daily ]
33+
args: [ :daily ],
34+
kwargs: { force_update: true }
3435
},
3536
weekly_leaderboard_update: {
3637
cron: "*/2 * * * *",
3738
class: "LeaderboardUpdateJob",
38-
args: [ :weekly ]
39+
args: [ :weekly ],
40+
kwargs: { force_update: true }
3941
},
4042
last_7_days_leaderboard_update: {
4143
cron: "*/7 * * * *",
4244
class: "LeaderboardUpdateJob",
43-
args: [ :last_7_days ]
45+
args: [ :last_7_days ],
46+
kwargs: { force_update: true }
4447
},
4548
# sailors_log_poll: {
4649
# cron: "*/2 * * * *",

0 commit comments

Comments
 (0)