Skip to content

Commit ad4dff7

Browse files
Merge pull request #90 from timothyfroehlich/feature/issue-tracking-framework
Add Issue Tracking Framework and Initial Monitoring Issues
2 parents 92ad4d7 + 5e0673e commit ad4dff7

6 files changed

Lines changed: 573 additions & 2 deletions
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Seen Submission Race Condition
2+
3+
**Priority**: 1 **Type**: bug **Status**: open **Created**: 2025-07-04
4+
**Updated**: 2025-07-04
5+
6+
## Description
7+
8+
Race condition exists between filtering submissions and marking them as seen,
9+
potentially allowing duplicate notifications if the monitoring loop runs
10+
multiple times rapidly or if API responses change between calls.
11+
12+
## Reproduction Steps
13+
14+
1. Set up monitoring targets with very short poll intervals (difficult to
15+
reproduce consistently)
16+
2. Force multiple rapid checks using `.trigger` command
17+
3. Monitor for duplicate notifications of the same submission
18+
4. Check database for timing issues in `seen_submissions` table
19+
20+
## Expected vs Actual Behavior
21+
22+
- **Expected**: Each submission should only be notified once, regardless of
23+
timing
24+
- **Actual**: Rapid polling or API inconsistencies could cause duplicate
25+
notifications
26+
27+
## Technical Details
28+
29+
### Code Location
30+
31+
- **File**: `src/cogs/runner.py`
32+
- **Functions**: `filter_new_submissions()` (line 386-398),
33+
`post_submissions()`, `mark_submissions_seen()`
34+
35+
### Race Condition Flow
36+
37+
```python
38+
# Time T1: First check starts
39+
submissions = await fetch_submissions_for_location(...) # API call
40+
new_submissions = self.db.filter_new_submissions(channel_id, submissions) # Query DB
41+
42+
# Time T2: Second check starts (before first completes)
43+
submissions_2 = await fetch_submissions_for_location(...) # Same API response
44+
new_submissions_2 = self.db.filter_new_submissions(channel_id, submissions_2) # Same query result
45+
46+
# Time T3: Both post notifications
47+
await self.post_submissions(new_submissions) # Posts duplicates
48+
await self.post_submissions(new_submissions_2) # Posts duplicates
49+
50+
# Time T4: Both mark as seen
51+
self.db.mark_submissions_seen(channel_id, submission_ids) # Second call gets IntegrityError
52+
```
53+
54+
### Database Protection
55+
56+
- `seen_submissions` table has unique constraint on
57+
`(channel_id, submission_id)`
58+
- `mark_submissions_seen()` handles `IntegrityError` gracefully
59+
- **Problem**: Notifications already sent before constraint violation
60+
61+
### Potential Triggers
62+
63+
- Multiple rapid manual checks (`.trigger` command)
64+
- Cloud Run scaling events causing multiple instances
65+
- API response timing variations
66+
- Database connection delays
67+
68+
## Proposed Solutions
69+
70+
### Option 1: Atomic Transaction
71+
72+
```python
73+
async def process_submissions_atomically(self, channel_id, submissions):
74+
async with self.db.transaction():
75+
new_submissions = self.db.filter_new_submissions(channel_id, submissions)
76+
if new_submissions:
77+
self.db.mark_submissions_seen(channel_id, [s["id"] for s in new_submissions])
78+
await self.post_submissions(new_submissions)
79+
```
80+
81+
### Option 2: Channel-Level Locking
82+
83+
```python
84+
class Runner:
85+
def __init__(self):
86+
self.channel_locks = {}
87+
88+
async def run_checks_for_channel(self, channel_id, config, is_manual_check=False):
89+
if channel_id not in self.channel_locks:
90+
self.channel_locks[channel_id] = asyncio.Lock()
91+
92+
async with self.channel_locks[channel_id]:
93+
# Existing check logic
94+
```
95+
96+
### Option 3: Submission ID Tracking
97+
98+
```python
99+
# Track recently processed submission IDs in memory
100+
self.recently_processed = {} # {channel_id: set(submission_ids)}
101+
102+
async def filter_new_submissions(self, channel_id, submissions):
103+
# Filter by database AND recent memory
104+
recent = self.recently_processed.get(channel_id, set())
105+
truly_new = [s for s in db_filtered if s["id"] not in recent]
106+
return truly_new
107+
```
108+
109+
## Acceptance Criteria
110+
111+
- [ ] No duplicate notifications under rapid polling scenarios
112+
- [ ] Multiple manual checks don't cause duplicates
113+
- [ ] Database integrity maintained
114+
- [ ] Performance impact minimal
115+
- [ ] Cloud Run scaling doesn't trigger race conditions
116+
- [ ] Test with concurrent `.trigger` commands
117+
118+
## Notes
119+
120+
- Difficult to reproduce consistently in testing
121+
- May require load testing to verify fix
122+
- Related to Cloud Run scaling and multiple instances
123+
- Consider distributed locking for multi-instance deployments
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Startup Duplicate Notifications
2+
3+
**Priority**: 1 **Type**: bug **Status**: open **Created**: 2025-07-04
4+
**Updated**: 2025-07-04
5+
6+
## Description
7+
8+
When the bot starts up, it sends notifications for old submissions that have
9+
already been shown previously. The startup check bypasses normal polling logic
10+
and treats all submissions as "new" even though they were previously processed.
11+
12+
## Reproduction Steps
13+
14+
1. Start the bot with existing monitoring targets
15+
2. Observe that one or more channels receive notifications
16+
3. Check log timestamps - notifications are for submissions that occurred before
17+
the current session
18+
4. Compare with previous bot runs - same submissions are re-notified
19+
20+
## Expected vs Actual Behavior
21+
22+
- **Expected**: Startup should only initialize monitoring, not send duplicate
23+
notifications
24+
- **Actual**: Startup sends notifications for previously seen submissions
25+
26+
## Technical Details
27+
28+
### Code Location
29+
30+
- **File**: `src/cogs/runner.py`
31+
- **Function**: `_run_startup_checks()` (lines 457-508)
32+
- **Issue**: Calls `run_checks_for_channel(is_manual_check=False)` during
33+
startup
34+
35+
### Root Cause
36+
37+
1. `_run_startup_checks()` runs immediately when bot becomes ready
38+
2. Calls `run_checks_for_channel()` with `is_manual_check=False`
39+
3. Since `last_poll_at` is None, API fetches all recent submissions without date
40+
filtering
41+
4. `filter_new_submissions()` has no seen submissions in database yet (fresh
42+
start)
43+
5. All submissions get posted as notifications
44+
6. Submissions are then marked as seen, preventing future duplicates
45+
46+
### Database State
47+
48+
- `seen_submissions` table is empty on startup
49+
- `last_poll_at` is None for channels that haven't been polled
50+
- API returns submissions from past 24 hours without filtering
51+
52+
## Proposed Solution
53+
54+
### Option 1: Change Startup to Manual Check
55+
56+
```python
57+
# In _run_startup_checks()
58+
result = await self.run_checks_for_channel(channel_id, config, is_manual_check=True)
59+
```
60+
61+
This would show "Last 5 submissions" format instead of posting as notifications.
62+
63+
### Option 2: Mark as Seen Without Posting
64+
65+
```python
66+
# In _run_startup_checks()
67+
submissions = await self._fetch_submissions_for_channel(channel_id, config)
68+
if submissions:
69+
self.db.mark_submissions_seen(channel_id, [s["id"] for s in submissions])
70+
# Don't call post_submissions()
71+
```
72+
73+
### Option 3: Skip Startup Checks Entirely
74+
75+
Remove `_run_startup_checks()` and let normal 1-minute polling handle first
76+
check.
77+
78+
## Acceptance Criteria
79+
80+
- [ ] Bot startup does not trigger duplicate notifications
81+
- [ ] Normal polling behavior unchanged
82+
- [ ] Manual check commands still show recent submissions
83+
- [ ] No regression in monitoring functionality
84+
- [ ] Test with multiple channels and monitoring targets
85+
86+
## Notes
87+
88+
- This issue is particularly noticeable with active monitoring targets
89+
- Users report confusion when old submissions appear as "new"
90+
- May be related to timezone handling in date filtering logic
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Timezone Handling Inconsistency
2+
3+
**Priority**: 2 **Type**: bug **Status**: open **Created**: 2025-07-04
4+
**Updated**: 2025-07-04
5+
6+
## Description
7+
8+
Inconsistent timezone handling across the application may cause polling schedule
9+
issues, incorrect date filtering in API calls, and database timestamp problems.
10+
11+
## Reproduction Steps
12+
13+
1. Review datetime handling in different parts of the codebase
14+
2. Check for naive vs timezone-aware datetime objects
15+
3. Monitor for polling schedule irregularities
16+
4. Verify API date filtering works correctly across time zones
17+
18+
## Expected vs Actual Behavior
19+
20+
- **Expected**: All datetime operations should be timezone-aware and consistent
21+
- **Actual**: Mix of naive and timezone-aware datetime objects causing potential
22+
issues
23+
24+
## Technical Details
25+
26+
### Code Locations
27+
28+
- **File**: `src/database.py` - `get_channel_config()` (lines 113-131)
29+
- **File**: `src/cogs/runner.py` - Various datetime operations
30+
- **File**: `src/api.py` - Date filtering in API calls
31+
32+
### Specific Issues
33+
34+
#### Database Operations
35+
36+
```python
37+
# In get_channel_config() - timezone conversion happens here
38+
if config.last_poll_at and config.last_poll_at.tzinfo is None:
39+
config.last_poll_at = config.last_poll_at.replace(tzinfo=timezone.utc)
40+
```
41+
42+
#### Polling Schedule Logic
43+
44+
```python
45+
# In _should_poll_channel() - may use naive datetime
46+
current_time = datetime.now() # Potentially naive
47+
time_since_last_poll = current_time - config.last_poll_at # Mixed types
48+
```
49+
50+
#### API Date Filtering
51+
52+
```python
53+
# Date calculations for min_date_of_submission parameter
54+
yesterday = datetime.now() - timedelta(days=1) # Potentially naive
55+
date_str = yesterday.strftime("%Y-%m-%d") # May not account for timezone
56+
```
57+
58+
### Potential Impact
59+
60+
- **Polling Schedule**: Channels may poll too frequently or not frequently
61+
enough
62+
- **API Filtering**: Submissions might be missed or duplicated due to date range
63+
issues
64+
- **Database Consistency**: Timestamps may not align properly
65+
- **Multi-timezone Users**: Different behavior based on server vs user timezones
66+
67+
## Proposed Solution
68+
69+
### Centralized Timezone Handling
70+
71+
```python
72+
# Create utility functions for consistent datetime handling
73+
from datetime import datetime, timezone
74+
import pytz
75+
76+
def now_utc() -> datetime:
77+
"""Get current UTC time, always timezone-aware"""
78+
return datetime.now(timezone.utc)
79+
80+
def ensure_utc(dt: datetime) -> datetime:
81+
"""Ensure datetime is timezone-aware and in UTC"""
82+
if dt.tzinfo is None:
83+
# Assume naive datetime is UTC
84+
return dt.replace(tzinfo=timezone.utc)
85+
return dt.astimezone(timezone.utc)
86+
87+
def format_api_date(dt: datetime) -> str:
88+
"""Format datetime for API calls consistently"""
89+
return ensure_utc(dt).strftime("%Y-%m-%d")
90+
```
91+
92+
### Update All Datetime Operations
93+
94+
1. Replace `datetime.now()` with `now_utc()`
95+
2. Ensure all database datetime fields are timezone-aware
96+
3. Standardize API date formatting
97+
4. Add timezone validation in tests
98+
99+
## Acceptance Criteria
100+
101+
- [ ] All datetime objects are timezone-aware
102+
- [ ] Polling schedules work correctly regardless of server timezone
103+
- [ ] API date filtering is consistent and accurate
104+
- [ ] Database timestamps are properly handled
105+
- [ ] No regression in existing functionality
106+
- [ ] Add timezone-specific tests
107+
108+
## Notes
109+
110+
- May require database migration if existing timestamps are naive
111+
- Consider impact on existing data
112+
- Test with different server timezone configurations
113+
- Document timezone assumptions for deployment

0 commit comments

Comments
 (0)