# PreRaceScheduler - Complete Documentation

## Overview

The PreRaceScheduler automatically updates race information at critical time points before each race starts. It ensures that field information, odds, and scratchings are up-to-date for betting and analysis purposes.

## Status: ✅ FULLY IMPLEMENTED

The PreRaceScheduler is **already implemented and operational**. This document explains how it works and how to use it.

## Architecture

### Two-Tier System

The scheduler operates at two critical time points:

1. **T-60 (60 minutes before)**: `PRE_RACE_T60`
   - Captures opening odds
   - Validates field information
   - Records initial runner details
   - Checks for early scratchings

2. **T-15 (15 minutes before)**: `PRE_RACE_T15`
   - Final field check
   - Captures late scratchings
   - Updates current odds
   - Last chance for field changes

### Time Window Strategy

Both schedulers run **every 5 minutes** and search for races within a specific window:

- **T-60 Window**: Races starting in 55-65 minutes (±5 minutes)
- **T-15 Window**: Races starting in 10-20 minutes (±5 minutes)

This window approach ensures races aren't missed between 5-minute check intervals.

## How It Works

### 1. Trigger Mechanism

```typescript
// Runs every 5 minutes via cron
cronExpression: '*/5 * * * *'
timezone: 'Australia/Sydney'
```

### 2. Race Discovery

For each execution, the scheduler:

1. Calculates the target time (now + 60 or 15 minutes)
2. Creates a search window (target ± 5 minutes)
3. Queries database for races in that window
4. Filters out already-closed races

```sql
SELECT * FROM races
WHERE "startTime" BETWEEN :windowStart AND :windowEnd
AND status != 'CLOSED'
ORDER BY "startTime" ASC
```

### 3. Update Process

For each race found:

1. Identifies the meeting containing the race
2. Calls `meetingService.updateMeeting(meetingId)`
3. Fetches fresh data from TAB API
4. Updates all races in that meeting
5. Records success/failure metrics

### 4. Error Handling

- Individual race failures don't stop the batch
- Errors are logged with meetingId and race number
- Metrics track success/failure rates
- JobRun records provide audit trail

## Configuration

### Location

`src/schedulers/config.ts`

### Settings

```typescript
[ScheduleType.PRE_RACE_T60]: {
  cronExpression: '*/5 * * * *',  // Every 5 minutes
  timezone: 'Australia/Sydney',
  enabled: true,
  description: 'Pre-race update 60 minutes before scheduled start',
}

[ScheduleType.PRE_RACE_T15]: {
  cronExpression: '*/5 * * * *',  // Every 5 minutes
  timezone: 'Australia/Sydney',
  enabled: true,
  description: 'Pre-race update 15 minutes before scheduled start',
}
```

### Time Windows

```typescript
TIME_WINDOWS = {
  PRE_RACE_T60: {
    minutesBefore: 60,
    windowMinutes: 10,  // ±5 minutes
  },
  PRE_RACE_T15: {
    minutesBefore: 15,
    windowMinutes: 10,  // ±5 minutes
  },
}
```

## Usage

### Automatic Operation

The schedulers run automatically when you start the application:

```bash
npm run dev
```

Both T-60 and T-15 schedulers will start and check every 5 minutes.

### Manual Testing

Test the schedulers with the provided script:

```bash
npx tsx scripts/test-pre-race-scheduler.ts
```

**Output includes**:
- Current time in NZ timezone
- List of upcoming races in next 2 hours
- Indication of which races fall in T-60 or T-15 windows
- Test results for both schedulers
- Success/error counts
- Processing duration

### Manual Trigger (Programmatic)

```typescript
import { TabApiClient } from './api/tab';
import { PreRaceScheduler } from './schedulers/pre-race-scheduler';
import { ScheduleType } from './schedulers/types';

const apiClient = new TabApiClient({
  baseUrl: process.env.TAB_API_BASE_URL!,
});

// T-60 Scheduler
const schedulerT60 = new PreRaceScheduler(
  apiClient,
  prisma,
  ScheduleType.PRE_RACE_T60
);

const result = await schedulerT60.triggerManually();

console.log(`Updated ${result.itemsProcessed} races`);
```

## Monitoring

### Grafana Dashboard

The Scheduler Monitoring dashboard (`http://localhost:3000/d/scheduler-monitoring`) shows:

- Pre-race scheduler execution rates
- Success/failure counts
- Processing durations
- Races updated per run
- Upcoming races table

### Metrics

```prometheus
# Execution counts
scheduler_runs_total{schedule_type="pre_race_t60", status="success"}
scheduler_runs_total{schedule_type="pre_race_t15", status="success"}

# Duration
scheduler_duration_seconds_bucket{schedule_type="pre_race_t60"}

# Items processed
scheduler_items_processed_total{schedule_type="pre_race_t60"}
```

### Logs

Structured logging with contextual information:

```typescript
// Finding races
logger.debug({
  type: 'pre_race_t60',
  now: '2026-01-14T08:15:00Z',
  targetTime: '2026-01-14T09:15:00Z',
  windowStart: '2026-01-14T09:10:00Z',
  windowEnd: '2026-01-14T09:20:00Z',
}, 'Searching for races in time window');

// Updating race
logger.debug({
  meetingId: 'abc-123',
  raceNumber: 5,
  startTime: '2026-01-14T09:15:00Z',
}, 'Updating race data');

// Completion summary
logger.info({
  type: 'pre_race_t60',
  racesUpdated: 3,
  meetingsUpdated: 2,
  errors: 0,
  durationMs: 1234,
}, 'Pre-race update completed');
```

## Database Impact

### Queries Per Execution

1. **Race lookup**: 1 query (filtered by time window)
2. **Meeting updates**: 1 query per unique meeting
3. **Race updates**: Batched upserts per meeting

### Update Frequency

- Runs every 5 minutes
- Only updates races in target window
- Typical: 0-5 races per execution during racing hours
- Peak: Up to 20 races if multiple meetings overlap

### Deduplication

The scheduler updates by meeting, not by race:
- If 8 races from same meeting fall in window
- Only fetches meeting data once
- Updates all 8 races together
- Efficient API usage

## Best Practices

### Timezone Awareness

- Database stores timestamps in UTC
- Scheduler converts to Australia/Sydney timezone
- Race times displayed in local NZ time in dashboard
- Be careful with daylight saving transitions

### Race Status

The scheduler skips races with status = 'CLOSED':
- Prevents unnecessary updates for completed races
- Reduces API calls
- Focuses on actionable data

### Error Recovery

- Failed updates don't cascade
- Each race/meeting is independent
- Errors logged with full context
- Next execution (5 min later) retries automatically

## Common Scenarios

### Scenario 1: Normal Operation

```
Time: 8:00 AM NZ
Race at: 9:05 AM NZ (65 minutes away)

T-60 scheduler runs:
- Searches: 8:55 AM - 9:10 AM window
- Finds: Race at 9:05 AM ✓
- Updates: Meeting data fetched
- Result: Race updated successfully
```

### Scenario 2: Multiple Races

```
Time: 8:00 AM NZ
Races at: 9:05, 9:08, 9:12, 9:15 AM

T-60 scheduler runs:
- Window: 8:55 - 9:10 AM
- Finds: 3 races (9:05, 9:08 in range; 9:15 outside)
- Updates: 2 meetings (assuming different venues)
- Result: 3 races updated
```

### Scenario 3: Race Already Closed

```
Time: 8:00 AM NZ
Race at: 9:05 AM (status: 'CLOSED')

T-60 scheduler runs:
- Window: 8:55 - 9:10 AM
- Query filters: status != 'CLOSED'
- Finds: 0 races
- Result: No updates needed
```

### Scenario 4: API Failure

```
Time: 8:00 AM NZ
Race at: 9:05 AM

T-60 scheduler runs:
- Finds: 1 race
- Attempts: Update meeting
- API: Returns 503 error
- Logs: Error with meeting ID
- Result: Fails gracefully, will retry in 5 min
```

## Integration Points

### Depends On

1. **Morning Scraper**: Must run first to populate race data
2. **TAB API**: Must be accessible for updates
3. **Database**: PostgreSQL with race records

### Used By

1. **Grafana Dashboard**: Displays scheduler metrics
2. **Odds Analysis**: Consumes updated odds data
3. **Race Status**: Downstream systems rely on current data

## Troubleshooting

### No Races Updated

**Symptoms**: Scheduler runs but itemsProcessed = 0

**Causes**:
1. No races in time window (normal)
2. All races already closed
3. Morning scraper hasn't run yet

**Check**:
```sql
-- See upcoming races
SELECT "startTime", status
FROM races
WHERE "startTime" > NOW()
ORDER BY "startTime"
LIMIT 10;
```

### Errors During Update

**Symptoms**: errors.length > 0 in result

**Causes**:
1. TAB API unavailable
2. Network timeout
3. Invalid meeting data
4. Database constraint violation

**Check**:
- Application logs for error details
- Grafana dashboard error count
- JobRun table for error messages

### Scheduler Not Running

**Symptoms**: No metrics, no logs

**Causes**:
1. Scheduler not started
2. Config has enabled: false
3. Application not running
4. Cron expression invalid

**Check**:
```bash
# Check if app is running
ps aux | grep node

# Check scheduler status via API
curl http://localhost:8080/health

# View logs
docker compose logs app -f
```

## Future Enhancements

Potential improvements (not yet implemented):

1. **Smart Scheduling**: Adjust check frequency based on race density
2. **Odds History**: Track odds changes over time
3. **Scratchings Alerts**: Notify on late scratchings
4. **Duplicate Detection**: Skip recently updated meetings
5. **Retry Logic**: Exponential backoff for failed updates

## Files

**Implementation**:
- `src/schedulers/pre-race-scheduler.ts` - Main scheduler class
- `src/schedulers/config.ts` - Configuration
- `src/schedulers/types.ts` - Type definitions
- `src/schedulers/base-scheduler.ts` - Base class

**Services**:
- `src/services/meeting-service.ts` - Meeting update logic

**Tests**:
- `scripts/test-pre-race-scheduler.ts` - Manual test script

**Monitoring**:
- `grafana/dashboards/scheduler-monitoring.json` - Grafana dashboard

## Summary

✅ **PreRaceScheduler is production-ready**

- Fully implemented with T-60 and T-15 variants
- Runs every 5 minutes automatically
- Uses time windows to catch all races
- Comprehensive error handling
- Full observability (logs, metrics, traces)
- Tested and validated

The scheduler requires no additional work - it's ready to use!
