diff --git a/webapp/models/source.py b/webapp/models/source.py index 9907409b..5c7f33c1 100644 --- a/webapp/models/source.py +++ b/webapp/models/source.py @@ -79,4 +79,4 @@ def combined_updated_at(self) -> datetime: return self.realtime_data_updated_at if self.static_data_updated_at: return self.static_data_updated_at - return self.modified_at + return self.modified diff --git a/webapp/prometheus_api/prometheus_handler.py b/webapp/prometheus_api/prometheus_handler.py index 37cb0565..345ce1cd 100644 --- a/webapp/prometheus_api/prometheus_handler.py +++ b/webapp/prometheus_api/prometheus_handler.py @@ -43,6 +43,11 @@ def __init__( def get_metrics(self) -> str: sources = self.source_repository.fetch_sources() + last_modified_metrics = Metrics( + help='Last modification in seconds from now', + type=MetricType.gauge, + identifier='app_ocpdb_source_last_modified', + ) last_static_update_metrics = Metrics( help='Last static update in seconds from now', type=MetricType.gauge, @@ -77,6 +82,13 @@ def get_metrics(self) -> str: if source.static_status in [SourceStatus.DISABLED, SourceStatus.PROVISIONED]: continue + last_modified_metrics.metrics.append( + SourceMetric( + source=source.uid, + value=int((datetime.now(tz=timezone.utc) - source.modified).total_seconds()), + ) + ) + if source.static_data_updated_at: last_static_update_metrics.metrics.append( SourceMetric( @@ -121,7 +133,8 @@ def get_metrics(self) -> str: ) metrics = ( - failed_static_sources.to_metrics() + last_modified_metrics.to_metrics() + + failed_static_sources.to_metrics() + last_static_update_metrics.to_metrics() + source_static_errors.to_metrics() + failed_realtime_sources.to_metrics() diff --git a/webapp/repositories/evse_repository.py b/webapp/repositories/evse_repository.py index 924e5c2f..69caf7c8 100644 --- a/webapp/repositories/evse_repository.py +++ b/webapp/repositories/evse_repository.py @@ -159,7 +159,12 @@ def fetch_evses_by_source_and_uids(self, source_uid: str, uids: list[str]) -> li query = ( self.session .query(Evse) - .options(selectinload(Evse.connectors)) + .options( + selectinload(Evse.connectors), + # save_evse_updates() lifts the EVSE last_updated to its location, so eager-load the whole chain: + # with a lot of realtime updates per second, a lazy load per EVSE would be an N+1 on every heartbeat. + joinedload(Evse.charging_station).joinedload(ChargingStation.location), + ) .filter(Evse.uid.in_(uids)) .join(Evse.charging_station) .join(ChargingStation.location) diff --git a/webapp/services/import_services/base_import_service.py b/webapp/services/import_services/base_import_service.py index afe72ab9..6f2cfd9b 100644 --- a/webapp/services/import_services/base_import_service.py +++ b/webapp/services/import_services/base_import_service.py @@ -128,6 +128,14 @@ def save_evse_updates(self, evse_updates: list[EvseUpdate | EvseRealtimeUpdate]) continue setattr(evse, key, value) + # Charging station and location are only as fresh as their freshest EVSE, so lift the new timestamp up + # the chain if it is newer. + charging_station = evse.charging_station + if evse.last_updated > charging_station.last_updated: + charging_station.last_updated = evse.last_updated + if charging_station.last_updated > charging_station.location.last_updated: + charging_station.location.last_updated = charging_station.last_updated + self.evse_repository.save_evse(evse, commit=False) self.evse_repository.session.commit() @@ -255,6 +263,12 @@ def save_location_update( ) location.charging_pool = new_charging_stations + # get_charging_station() already lifted every charging station to its freshest EVSE, so taking the maximum + # over the charging stations propagates the newest EVSE timestamp up to the location. + for charging_station in new_charging_stations: + if charging_station.last_updated > location.last_updated: + location.last_updated = charging_station.last_updated + # Fetch official regional code if not set, if all required fields are available and if country is supported if ( not location.official_region_code @@ -340,6 +354,12 @@ def get_charging_station( charging_station.evses = new_evses + # Same rule as in save_evse_updates(): a charging station is only as fresh as its freshest EVSE. Sources + # regularly send EVSE timestamps which are newer than the charging station timestamp. + for evse in new_evses: + if evse.last_updated > charging_station.last_updated: + charging_station.last_updated = evse.last_updated + return charging_station def get_evse(