Skip to content

Commit a6c55b3

Browse files
committed
Major data resilience improvements: retry, backfill, parallel polling
USGS Reliability: - Increase timeout from 15s to 45s for slow network paths - Add 3-attempt exponential backoff retry (1s, 2s, 4s delays) - Daily values fallback when instantaneous values timeout - Thread pool for parallel polling (8 workers, prevents one timeout blocking others) Backfill Infrastructure (SQL migration 007): - station_health table tracks poll success/failures per source - backfill_queue table for gap detection and filling - backfill_history audit log - detect_usgs_gaps() function for automated gap discovery - process_backfill_queue() method in daemon Performance: - Parallel USGS polling via threadpool crate - Non-blocking: one station timeout doesn't delay others - Configurable worker count via POLL_WORKERS env var Documentation: - VPN/proxy solution for persistent GCE network issues - NWS API integration plan (replace unreliable IEM/ASOS) - Network diagnostic commands and queries Fixes: - Zone endpoint sensor errors no longer fail entire zone (graceful degradation) - Test fix: search by name not cwms_location for Peoria (now IL07 SHEF ID)
1 parent 2a99704 commit a6c55b3

7 files changed

Lines changed: 931 additions & 23 deletions

File tree

flomon_service/Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flomon_service/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ tiny_http = "0.12"
2323

2424
# Base64 encoding for Pub/Sub message payloads
2525
base64 = "0.22"
26+
27+
# Thread pool for parallel data collection
28+
threadpool = "1.8"
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# NOAA NWS API Integration Plan
2+
3+
## Current State: IEM/ASOS
4+
5+
**Issues:**
6+
- Iowa Environmental Mesonet ASOS API frequently returns 503 errors
7+
- Regional service (Iowa State University) not as reliable as federal sources
8+
- No SLA or guaranteed uptime
9+
10+
**Usage:**
11+
- Precipitation data for flood modeling
12+
- Currently monitoring: PIA (Peoria), BMI (Bloomington), SPI (Springfield), ORD (Chicago), PWK (Wheeling), GBG (Galesburg)
13+
14+
## Proposed: NOAA NWS API
15+
16+
**Advantages:**
17+
- Federal service with high uptime (>99.5%)
18+
- Modern RESTful API with JSON responses
19+
- No API key required (request User-Agent header only)
20+
- Comprehensive weather data including precipitation, warnings, forecasts
21+
22+
**Base URL:** `https://api.weather.gov`
23+
24+
**Documentation:** https://www.weather.gov/documentation/services-web-api
25+
26+
### Relevant Endpoints
27+
28+
#### 1. Observation Stations by State/Zone
29+
```
30+
GET /stations?state=IL
31+
```
32+
Returns list of all observation stations in Illinois. Use to discover station IDs for our monitoring region.
33+
34+
#### 2. Latest Observation
35+
```
36+
GET /stations/{stationId}/observations/latest
37+
```
38+
Returns most recent observation including:
39+
- `precipitationLastHour` (in meters, convert to inches)
40+
- `temperature`
41+
- `windSpeed`
42+
- `timestamp`
43+
44+
Example for Peoria (KPIA):
45+
```bash
46+
curl -H "User-Agent: FloodMonitor/0.1 (flomon@example.com)" \
47+
https://api.weather.gov/stations/KPIA/observations/latest
48+
```
49+
50+
Response excerpt:
51+
```json
52+
{
53+
"properties": {
54+
"timestamp": "2026-04-06T16:54:00+00:00",
55+
"temperature": {
56+
"value": 15.6,
57+
"unitCode": "wmoUnit:degC"
58+
},
59+
"precipitationLastHour": {
60+
"value": 0.0,
61+
"unitCode": "wmoUnit:m",
62+
"qualityControl": "qc:V"
63+
}
64+
}
65+
}
66+
```
67+
68+
#### 3. Observation History
69+
```
70+
GET /stations/{stationId}/observations?start={ISO8601}&end={ISO8601}
71+
```
72+
For backfilling gaps in precipitation data (limited to 7 days history).
73+
74+
### Implementation Plan
75+
76+
#### Phase 1: Add NWS Module (Parallel to ASOS)
77+
- Create `flomon_service/src/ingest/nws.rs`
78+
- Implement `fetch_nws_observation(station_id)` -> `WeatherObservation`
79+
- Add to daemon polling loop alongside ASOS
80+
- Compare data quality for 7 days before switching
81+
82+
#### Phase 2: Database Schema
83+
```sql
84+
-- Option A: Reuse existing asos_observations table (rename to weather_observations)
85+
ALTER TABLE public.asos_observations RENAME TO weather_observations;
86+
ALTER TABLE public.weather_observations ADD COLUMN source TEXT DEFAULT 'ASOS';
87+
CREATE INDEX idx_weather_observations_source ON weather_observations(source);
88+
89+
-- Option B: New nws_observations table (if schema differs significantly)
90+
CREATE TABLE public.nws_observations (
91+
id SERIAL PRIMARY KEY,
92+
station_id TEXT NOT NULL,
93+
observation_time TIMESTAMPTZ NOT NULL,
94+
temperature_c NUMERIC(5,2),
95+
precip_1hr_mm NUMERIC(6,2), -- Store in mm, convert to inches in query
96+
wind_speed_ms NUMERIC(5,2),
97+
raw_json JSONB, -- Store full response for debugging
98+
UNIQUE(station_id, observation_time)
99+
);
100+
```
101+
102+
#### Phase 3: Migrate Configuration
103+
Update `iem_asos.toml` (or create `nws_stations.toml`):
104+
```toml
105+
[[stations]]
106+
station_id = "KPIA"
107+
location = "Peoria, IL"
108+
relevance = "PRIMARY"
109+
zone_assignments = [2] # Upper Peoria Lake zone
110+
111+
[[stations]]
112+
station_id = "KBMI"
113+
location = "Bloomington, IL"
114+
relevance = "UPSTREAM"
115+
zone_assignments = [3] # Mackinaw tributary zone
116+
```
117+
118+
#### Phase 4: Update Endpoint Query
119+
Modify `flomon_service/src/endpoint.rs` to query from new table/column:
120+
```rust
121+
let rows = client.query(
122+
"SELECT
123+
CASE
124+
WHEN precip_1hr_mm IS NOT NULL THEN precip_1hr_mm * 0.0393701 -- mm to inches
125+
ELSE precip_1hr_in -- Fallback to old ASOS column
126+
END AS precip_1hr_in,
127+
observation_time
128+
FROM public.weather_observations
129+
WHERE station_id = $1
130+
ORDER BY observation_time DESC
131+
LIMIT 1",
132+
&[station_id]
133+
)?;
134+
```
135+
136+
#### Phase 5: Deprecate ASOS
137+
- After 30 days of successful NWS operation, remove IEM/ASOS code
138+
- Archive `iem_asos.toml` as `iem_asos.toml.deprecated`
139+
- Update documentation to reflect NWS as primary weather source
140+
141+
### Code Skeleton
142+
143+
**`flomon_service/src/ingest/nws.rs`:**
144+
```rust
145+
use serde::Deserialize;
146+
use chrono::{DateTime, Utc};
147+
148+
#[derive(Deserialize)]
149+
struct NwsObservationResponse {
150+
properties: NwsProperties,
151+
}
152+
153+
#[derive(Deserialize)]
154+
struct NwsProperties {
155+
timestamp: String,
156+
#[serde(rename = "precipitationLastHour")]
157+
precipitation_last_hour: Option<NwsValue>,
158+
temperature: Option<NwsValue>,
159+
}
160+
161+
#[derive(Deserialize)]
162+
struct NwsValue {
163+
value: Option<f64>,
164+
#[serde(rename = "unitCode")]
165+
unit_code: String,
166+
}
167+
168+
pub struct NwsObservation {
169+
pub station_id: String,
170+
pub observation_time: DateTime<Utc>,
171+
pub precip_1hr_mm: Option<f64>,
172+
pub temperature_c: Option<f64>,
173+
}
174+
175+
pub fn fetch_nws_observation(station_id: &str) -> Result<NwsObservation, Box<dyn std::error::Error>> {
176+
let url = format!("https://api.weather.gov/stations/{}/observations/latest", station_id);
177+
178+
let client = reqwest::blocking::Client::builder()
179+
.user_agent("FloodMonitor/0.1 (contact@example.com)")
180+
.timeout(std::time::Duration::from_secs(15))
181+
.build()?;
182+
183+
let response = client.get(&url).send()?;
184+
185+
if !response.status().is_success() {
186+
return Err(format!("NWS API returned status {}", response.status()).into());
187+
}
188+
189+
let body = response.text()?;
190+
let data: NwsObservationResponse = serde_json::from_str(&body)?;
191+
192+
let observation_time = DateTime::parse_from_rfc3339(&data.properties.timestamp)?
193+
.with_timezone(&Utc);
194+
195+
let precip_1hr_mm = data.properties.precipitation_last_hour
196+
.and_then(|v| v.value)
197+
.map(|m| m * 1000.0); // Convert meters to millimeters
198+
199+
let temperature_c = data.properties.temperature
200+
.and_then(|v| v.value);
201+
202+
Ok(NwsObservation {
203+
station_id: station_id.to_string(),
204+
observation_time,
205+
precip_1hr_mm,
206+
temperature_c,
207+
})
208+
}
209+
```
210+
211+
### Rate Limiting
212+
213+
NWS API guidelines:
214+
- No hard rate limit enforced
215+
- Recommended: <1000 requests/hour per IP
216+
- Current load: 6 stations × 4 polls/hour = 24 requests/hour (well within limits)
217+
218+
### Error Handling
219+
220+
NWS API is highly reliable but can have temporary outages. Implement same retry strategy as USGS:
221+
- 3 attempts with exponential backoff
222+
- Gracefully degrade (skip precipitation sensors if unavailable)
223+
- Alert if >24 hours without successful weather data
224+
225+
### Testing Plan
226+
227+
1. **Endpoint Validation**: Verify each station ID returns data
228+
```bash
229+
for station in KPIA KBMI KSPI KORD KPWK KGBG; do
230+
echo "Testing $station..."
231+
curl -s -H "User-Agent: Test/1.0" \
232+
"https://api.weather.gov/stations/$station/observations/latest" \
233+
| jq '.properties.precipitationLastHour'
234+
done
235+
```
236+
237+
2. **Integration Test**: Add to `tests/data_source_integration.rs`
238+
```rust
239+
#[test]
240+
fn nws_api_returns_recent_observation() {
241+
let obs = fetch_nws_observation("KPIA").unwrap();
242+
assert!(obs.observation_time > Utc::now() - Duration::hours(1));
243+
}
244+
```
245+
246+
3. **Comparison Test**: Run both ASOS and NWS for 7 days, compare:
247+
- Data freshness (latency)
248+
- Uptime (successful polls %)
249+
- Precipitation values (should be identical)
250+
251+
### Migration Timeline
252+
253+
- **Week 1**: Implement NWS module, add to daemon (parallel)
254+
- **Week 2-3**: Monitor data quality, resolve any issues
255+
- **Week 4**: Switch zones.toml to use NWS stations, keep ASOS as fallback
256+
- **Week 5**: Remove ASOS polling if NWS stable
257+
- **Week 6**: Clean up code, update documentation
258+
259+
### Rollback Plan
260+
261+
If NWS API proves unreliable:
262+
1. Revert zones.toml to ASOS station_ids
263+
2. Re-enable IEM ASOS polling in daemon
264+
3. Consider hybrid approach (both NWS and ASOS, use whichever is fresher)
265+
266+
## References
267+
268+
- NWS API Docs: https://www.weather.gov/documentation/services-web-api
269+
- NWS API Specification: https://api.weather.gov/openapi.json
270+
- Station List: https://api.weather.gov/stations?state=IL
271+
- Example Station: https://api.weather.gov/stations/KPIA/observations/latest

0 commit comments

Comments
 (0)