@@ -10,51 +10,78 @@ class LeaderboardUpdateJob < ApplicationJob
10
10
drop : true
11
11
)
12
12
13
- def perform ( period = :daily , date = Date . current )
13
+ def perform ( period = :daily , date = Date . current , force_update : false )
14
14
date = LeaderboardDateRange . normalize_date ( date , period )
15
15
16
- Rails . logger . info "Starting leaderboard generation for #{ period } on #{ date } "
16
+ # global
17
+ build_leaderboard ( date , period , nil , nil , force_update )
17
18
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
20
26
21
- Rails . logger . info "Completed leaderboard generation for #{ period } on #{ date } "
27
+ private
22
28
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
28
39
end
29
40
30
- private
31
41
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 )
34
43
board = ::Leaderboard . find_or_create_by! (
35
44
start_date : date ,
36
45
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"
40
55
end
41
56
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
43
63
44
64
ActiveRecord ::Base . transaction do
45
65
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
53
66
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
55
82
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 }
58
85
59
86
streaks = Heartbeat . daily_streaks_for_users ( data . keys )
60
87
@@ -73,41 +100,15 @@ def build_global(date, period)
73
100
board . update! ( finished_generating_at : Time . current )
74
101
end
75
102
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 )
103
107
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 )
108
109
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"
110
111
111
- data
112
+ board
112
113
end
113
114
end
0 commit comments