@@ -9,10 +9,11 @@ import (
99
1010// PRState tracks the complete state of a PR including blocking history.
1111type PRState struct {
12- FirstBlockedAt time.Time
13- LastSeenBlocked time.Time
14- PR PR
15- HasNotified bool
12+ FirstBlockedAt time.Time
13+ LastSeenBlocked time.Time
14+ PR PR
15+ HasNotified bool
16+ IsInitialDiscovery bool // True if this PR was discovered as already blocked during startup
1617}
1718
1819// PRStateManager manages all PR states with proper synchronization.
@@ -34,13 +35,18 @@ func NewPRStateManager(startTime time.Time) *PRStateManager {
3435
3536// UpdatePRs updates the state with new PR data and returns which PRs need notifications.
3637// This function is thread-safe and handles all state transitions atomically.
37- func (m * PRStateManager ) UpdatePRs (incoming , outgoing []PR , hiddenOrgs map [string ]bool ) (toNotify []PR ) {
38+ // isInitialDiscovery should be true only on the very first poll to prevent notifications for already-blocked PRs.
39+ func (m * PRStateManager ) UpdatePRs (incoming , outgoing []PR , hiddenOrgs map [string ]bool , isInitialDiscovery bool ) (toNotify []PR ) {
3840 m .mu .Lock ()
3941 defer m .mu .Unlock ()
4042
4143 now := time .Now ()
4244 inGracePeriod := time .Since (m .startTime ) < time .Duration (m .gracePeriodSeconds )* time .Second
4345
46+ slog .Debug ("[STATE] UpdatePRs called" ,
47+ "incoming" , len (incoming ), "outgoing" , len (outgoing ),
48+ "existing_states" , len (m .states ), "in_grace_period" , inGracePeriod , "is_initial_discovery" , isInitialDiscovery )
49+
4450 // Track which PRs are currently blocked
4551 currentlyBlocked := make (map [string ]bool )
4652
@@ -62,7 +68,10 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
6268 if ! isBlocked {
6369 // PR is not blocked - remove from tracking if it was
6470 if state , exists := m .states [pr .URL ]; exists && state != nil {
65- slog .Info ("[STATE] PR no longer blocked" , "repo" , pr .Repository , "number" , pr .Number )
71+ slog .Info ("[STATE] State transition: blocked -> unblocked" ,
72+ "repo" , pr .Repository , "number" , pr .Number , "url" , pr .URL ,
73+ "was_blocked_since" , state .FirstBlockedAt .Format (time .RFC3339 ),
74+ "blocked_duration" , time .Since (state .FirstBlockedAt ).Round (time .Second ))
6675 delete (m .states , pr .URL )
6776 }
6877 continue
@@ -73,37 +82,44 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
7382 // Get or create state for this PR
7483 state , exists := m .states [pr .URL ]
7584 if ! exists {
76- // Check if PR is too old to be considered "newly blocked"
77- // If the PR hasn't been updated in over an hour, don't treat it as new
78- const maxAgeForNewlyBlocked = 1 * time .Hour
79- prAge := time .Since (pr .UpdatedAt )
80-
81- if prAge > maxAgeForNewlyBlocked {
82- // Old PR, track it but don't notify
85+ // This PR was not in our state before
86+ if isInitialDiscovery {
87+ // Initial discovery: PR was already blocked when we started, no state transition
8388 state = & PRState {
84- PR : pr ,
85- FirstBlockedAt : now ,
86- LastSeenBlocked : now ,
87- HasNotified : true , // Mark as already notified to prevent future notifications
89+ PR : pr ,
90+ FirstBlockedAt : now ,
91+ LastSeenBlocked : now ,
92+ HasNotified : false , // Don't consider this as notified since no actual notification was sent
93+ IsInitialDiscovery : true , // Mark as initial discovery to prevent notifications and party poppers
8894 }
8995 m .states [pr .URL ] = state
90- slog .Debug ("[STATE] Blocked PR detected but too old to notify" ,
91- "repo" , pr .Repository , "number" , pr .Number ,
92- "age" , prAge .Round (time .Minute ), "maxAge" , maxAgeForNewlyBlocked )
96+
97+ slog .Info ("[STATE] Initial discovery: already blocked PR" ,
98+ "repo" , pr .Repository ,
99+ "number" , pr .Number ,
100+ "url" , pr .URL ,
101+ "pr_updated_at" , pr .UpdatedAt .Format (time .RFC3339 ),
102+ "firstBlockedAt" , state .FirstBlockedAt .Format (time .RFC3339 ))
93103 } else {
94- // New blocked PR that was recently updated!
104+ // Actual state transition: unblocked -> blocked
95105 state = & PRState {
96- PR : pr ,
97- FirstBlockedAt : now ,
98- LastSeenBlocked : now ,
99- HasNotified : false ,
106+ PR : pr ,
107+ FirstBlockedAt : now ,
108+ LastSeenBlocked : now ,
109+ HasNotified : false ,
110+ IsInitialDiscovery : false , // This is a real state transition
100111 }
101112 m .states [pr .URL ] = state
102113
103- slog .Info ("[STATE] New blocked PR detected" , "repo" , pr .Repository , "number" , pr .Number ,
104- "age" , prAge .Round (time .Minute ))
114+ slog .Info ("[STATE] State transition: unblocked -> blocked" ,
115+ "repo" , pr .Repository ,
116+ "number" , pr .Number ,
117+ "url" , pr .URL ,
118+ "pr_updated_at" , pr .UpdatedAt .Format (time .RFC3339 ),
119+ "firstBlockedAt" , state .FirstBlockedAt .Format (time .RFC3339 ),
120+ "inGracePeriod" , inGracePeriod )
105121
106- // Should we notify?
122+ // Should we notify for actual state transitions ?
107123 if ! inGracePeriod && ! state .HasNotified {
108124 slog .Debug ("[STATE] Will notify for newly blocked PR" , "repo" , pr .Repository , "number" , pr .Number )
109125 toNotify = append (toNotify , pr )
@@ -113,12 +129,20 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
113129 }
114130 }
115131 } else {
116- // PR was already blocked
132+ // PR was already blocked in our state - just update data, preserve FirstBlockedAt
133+ originalFirstBlocked := state .FirstBlockedAt
117134 state .LastSeenBlocked = now
118135 state .PR = pr // Update PR data
119136
137+ slog .Debug ("[STATE] State transition: blocked -> blocked (no change)" ,
138+ "repo" , pr .Repository , "number" , pr .Number , "url" , pr .URL ,
139+ "original_first_blocked" , originalFirstBlocked .Format (time .RFC3339 ),
140+ "time_since_first_blocked" , time .Since (originalFirstBlocked ).Round (time .Second ),
141+ "has_notified" , state .HasNotified )
142+
120143 // If we haven't notified yet and we're past grace period, notify now
121- if ! state .HasNotified && ! inGracePeriod {
144+ // But don't notify for initial discovery PRs
145+ if ! state .HasNotified && ! inGracePeriod && ! state .IsInitialDiscovery {
122146 slog .Info ("[STATE] Past grace period, notifying for previously blocked PR" ,
123147 "repo" , pr .Repository , "number" , pr .Number )
124148 toNotify = append (toNotify , pr )
@@ -128,13 +152,26 @@ func (m *PRStateManager) UpdatePRs(incoming, outgoing []PR, hiddenOrgs map[strin
128152 }
129153
130154 // Clean up states for PRs that are no longer in our lists
131- for url := range m .states {
155+ // Add more conservative cleanup with logging
156+ removedCount := 0
157+ for url , state := range m .states {
132158 if ! currentlyBlocked [url ] {
133- slog .Debug ("[STATE] Removing stale state for PR" , "url" , url )
159+ timeSinceLastSeen := time .Since (state .LastSeenBlocked )
160+ slog .Info ("[STATE] Removing stale PR state (no longer blocked)" ,
161+ "url" , url , "repo" , state .PR .Repository , "number" , state .PR .Number ,
162+ "first_blocked_at" , state .FirstBlockedAt .Format (time .RFC3339 ),
163+ "last_seen_blocked" , state .LastSeenBlocked .Format (time .RFC3339 ),
164+ "time_since_last_seen" , timeSinceLastSeen .Round (time .Second ),
165+ "was_notified" , state .HasNotified )
134166 delete (m .states , url )
167+ removedCount ++
135168 }
136169 }
137170
171+ if removedCount > 0 {
172+ slog .Info ("[STATE] State cleanup completed" , "removed_states" , removedCount , "remaining_states" , len (m .states ))
173+ }
174+
138175 return toNotify
139176}
140177
0 commit comments