Skip to content

Commit 415aebc

Browse files
committed
Cleanup and document StepRunBase
1 parent 8637193 commit 415aebc

File tree

5 files changed

+542
-249
lines changed

5 files changed

+542
-249
lines changed
Lines changed: 69 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import logging
12
import os
23
from dataclasses import dataclass
34
from datetime import datetime, timedelta
45
from enum import auto
56

6-
from influxdb import InfluxDBClient
7-
7+
from alfalfa_worker.lib.alfalfa_connections_manager import (
8+
AlafalfaConnectionsManager
9+
)
810
from alfalfa_worker.lib.constants import DATETIME_FORMAT
911
from alfalfa_worker.lib.enums import AutoName, RunStatus
1012
from alfalfa_worker.lib.job import Job, message
@@ -39,48 +41,66 @@ class Options:
3941
# Should points be logged to InfluxDB
4042
historian_enabled: bool = os.environ.get('HISTORIAN_ENABLE', False) == 'true'
4143

44+
# Timeouts
45+
# These are mostly used for the StepRunProcess class to clean up the subprocess
46+
# when it hangs.
47+
48+
# How long can an advance call run before something bad is assumed to have happened.
4249
advance_timeout: int = 45
50+
# How long can it take a simulation to start before something bad is assumed to have happened.
4351
start_timeout: int = 300
52+
# Same as previous timeouts, but related to stopping
4453
stop_timeout: int = 45
54+
4555
# How many timesteps can a timescale run lag behind before being stopped
4656
timescale_lag_limit: int = 2
4757

4858
def __init__(self, realtime: bool, timescale: int, external_clock: bool, start_datetime: str, end_datetime: str):
49-
print(f"external_clock: {external_clock}")
59+
self.logger = logging.getLogger(self.__class__.__name__)
60+
5061
if external_clock:
5162
self.clock_source = ClockSource.EXTERNAL
5263
else:
53-
print(f"setting source to: {ClockSource.INTERNAL}")
5464
self.clock_source = ClockSource.INTERNAL
5565

56-
print(f"clock_source: {self.clock_source}")
57-
print(f"internal is: {ClockSource.INTERNAL}")
66+
self.logger.debug(f"Clock Source: {self.clock_source}")
5867

5968
self.start_datetime = datetime.strptime(start_datetime, DATETIME_FORMAT)
6069
self.end_datetime = datetime.strptime(end_datetime, DATETIME_FORMAT)
70+
self.logger.debug(f"Start Datetime: {self.start_datetime}")
71+
self.logger.debug(f"End Datetime: {self.end_datetime}")
6172

6273
if realtime:
6374
self.timescale = 1
6475
else:
6576
self.timescale = timescale
6677

78+
if self.clock_source == ClockSource.INTERNAL:
79+
self.logger.debug(f"Timescale: {self.timescale}")
80+
6781
# Check for at least one of the required parameters
6882
if not realtime and not timescale and not external_clock:
6983
raise JobException("At least one of 'external_clock', 'timescale', or 'realtime' must be specified")
7084

7185
@property
7286
def advance_interval(self) -> timedelta:
87+
"""Get the timedelta for how often a simulation should be advanced
88+
based on the timescale and timestep_duration
89+
"""
7390
if self.timestep_duration and self.timescale:
7491
return self.timestep_duration / self.timescale
7592
return None
7693

7794
@property
7895
def timesteps_per_hour(self) -> int:
96+
"""Get advance step time in terms of how many steps are requried
97+
to advance the model one hour
98+
"""
7999
return int(timedelta(hours=1) / self.timestep_duration)
80100

81101

82102
class StepRunBase(Job):
83-
def __init__(self, run_id: str, realtime: bool, timescale: int, external_clock: bool, start_datetime: str, end_datetime: str, **kwargs) -> None:
103+
def __init__(self, run_id: str, realtime: bool, timescale: int, external_clock: bool, start_datetime: str, end_datetime: str) -> None:
84104
"""Base class for all jobs to step a run. The init handles the basic configuration needed
85105
for the derived classes.
86106
@@ -91,96 +111,89 @@ def __init__(self, run_id: str, realtime: bool, timescale: int, external_clock:
91111
external_clock (bool): Use an external clock to step the simulation.
92112
start_datetime (str): Start datetime. #TODO: this should be typed datetime
93113
end_datetime (str): End datetime. #TODO: this should be typed datetime
94-
**skip_site_init (bool): Skip the initialization of the site database object. This is mainly used in testing.
95114
"""
96115
super().__init__()
97116
self.set_run_status(RunStatus.STARTING)
98117
self.options: Options = Options(to_bool(realtime), int(timescale), to_bool(external_clock), start_datetime, end_datetime)
99118

100-
self.first_step_time: datetime = None
101-
self.next_step_time: datetime = None
102-
self.setup_connections()
119+
self.warmup_cleared = not self.options.warmup_is_first_step
120+
121+
connections_manager = AlafalfaConnectionsManager()
122+
self.influx_db_name = connections_manager.influx_db_name
123+
self.influx_client = connections_manager.influx_client
124+
self.historian_enabled = connections_manager.historian_enabled
103125

104126
def exec(self) -> None:
127+
self.logger.info("Initializing simulation...")
105128
self.initialize_simulation()
106-
self.logger.info("Done initializing simulation")
129+
self.logger.info("Simulation initialized.")
107130
self.set_run_status(RunStatus.STARTED)
131+
132+
self.logger.info("Advancing to start time...")
108133
self.advance_to_start_time()
109-
if self.options.warmup_is_first_step and self.options.clock_source == ClockSource.INTERNAL:
134+
if not self.warmup_cleared and self.options.clock_source == ClockSource.INTERNAL:
110135
self.advance()
136+
self.logger.info("Advanced to start time.")
137+
111138
self.set_run_status(RunStatus.RUNNING)
112-
self.logger.info(f"Clock source: {self.options.clock_source}")
113139
if self.options.clock_source == ClockSource.INTERNAL:
114-
self.logger.info("Running Simulation with Internal Clock")
140+
self.logger.info("Running Simulation with Internal Clock.")
115141
self.run_timescale()
116142
elif self.options.clock_source == ClockSource.EXTERNAL:
117-
self.logger.info("Running Simulations with External Clock")
143+
self.logger.info("Running Simulations with External Clock.")
118144
self.start_message_loop()
119145

120146
def initialize_simulation(self) -> None:
121147
"""Placeholder for all things necessary to initialize simulation"""
122148
raise NotImplementedError
123149

124150
def advance_to_start_time(self):
125-
self.logger.info("Advancing to start time")
126151
while self.run.sim_time < self.options.start_datetime:
127-
self.logger.info("Calling advance")
128152
self.advance()
129-
130-
def create_tag_dictionaries(self):
131-
"""Placeholder for method necessary to create Haystack entities and records"""
153+
self.warmup_cleared = True
132154

133155
def run_timescale(self):
134-
self.first_timestep_time = datetime.now()
135-
self.next_step_time = datetime.now() + self.options.advance_interval
136-
self.logger.info(f"Next step time: {self.next_step_time}")
137-
self.logger.info(f"Advance interval is: {self.options.advance_interval}")
156+
"""Run simulation at timescale"""
157+
next_advance_time = datetime.now() + self.options.advance_interval
158+
self.logger.debug(f"Advance interval is: {self.options.advance_interval}")
159+
self.logger.debug(f"Next step time: {next_advance_time}")
160+
138161
while self.is_running:
139-
if self.options.clock_source == ClockSource.INTERNAL:
140-
if datetime.now() >= self.next_step_time:
141-
steps_behind = (datetime.now() - self.next_step_time) / self.options.advance_interval
142-
if steps_behind > self.options.timescale_lag_limit:
143-
raise JobExceptionSimulation(f"Timescale too high. Simulation more than {self.options.timescale_lag_limit} timesteps behind")
144-
self.next_step_time = self.next_step_time + self.options.advance_interval
145-
self.logger.info(f"Internal clock called advance at {self.run.sim_time}")
146-
self.logger.info(f"Next step time: {self.next_step_time}")
147-
self.advance()
148-
149-
if self.check_simulation_stop_conditions() or self.run.sim_time >= self.options.end_datetime:
150-
self.logger.info(f"Stopping at time: {self.run.sim_time}")
151-
self.stop()
152-
break
162+
if datetime.now() >= next_advance_time:
163+
steps_behind = (datetime.now() - next_advance_time) / self.options.advance_interval
164+
if steps_behind > self.options.timescale_lag_limit:
165+
raise JobExceptionSimulation(f"Timescale too high. Simulation more than {self.options.timescale_lag_limit} timesteps behind")
166+
next_advance_time = next_advance_time + self.options.advance_interval
167+
168+
self.logger.debug(f"Internal clock called advance at {self.run.sim_time}")
169+
self.logger.debug(f"Next advance time: {next_advance_time}")
170+
self.advance()
171+
172+
if self.check_simulation_stop_conditions() or self.run.sim_time >= self.options.end_datetime:
173+
self.logger.debug(f"Stopping at time: {self.run.sim_time}")
174+
self.stop()
175+
break
153176

154177
self._check_messages()
155-
self.logger.info("Exitting timescale run")
178+
self.logger.info("Internal clock simulation has exited.")
156179

157180
def get_sim_time(self) -> datetime:
158181
"""Placeholder for method which retrieves time in the simulation"""
159182
raise NotImplementedError
160183

161184
def update_run_time(self) -> None:
185+
"""Update the sim_time in the Run object to match the most up to date sim_time"""
162186
self.set_run_time(self.get_sim_time())
163187

164188
def check_simulation_stop_conditions(self) -> bool:
165-
"""Placeholder to determine whether a simulation should stop"""
189+
"""Placeholder to determine whether a simulation should stop.
190+
This can be used to signal that the simulation has exited and the job can now continue
191+
to clean up."""
166192
return False
167193

168-
def setup_connections(self):
169-
"""Placeholder until all db/connections operations can be completely moved out of the job"""
170-
# InfluxDB
171-
self.historian_enabled = os.environ.get('HISTORIAN_ENABLE', False) == 'true'
172-
if self.historian_enabled:
173-
self.influx_db_name = os.environ['INFLUXDB_DB']
174-
self.influx_client = InfluxDBClient(host=os.environ['INFLUXDB_HOST'],
175-
username=os.environ['INFLUXDB_ADMIN_USER'],
176-
password=os.environ['INFLUXDB_ADMIN_PASSWORD'])
177-
else:
178-
self.influx_db_name = None
179-
self.influx_client = None
180-
181194
@message
182195
def stop(self) -> None:
183-
self.logger.info("Received stop command")
196+
self.logger.info("Stopping simulation.")
184197
super().stop()
185198
self.set_run_status(RunStatus.STOPPING)
186199

@@ -190,4 +203,5 @@ def cleanup(self) -> None:
190203

191204
@message
192205
def advance(self) -> None:
206+
"""Placeholder for method which advances the simulation one timestep"""
193207
raise NotImplementedError

alfalfa_worker/lib/alfalfa_connections_manager.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22

33
import boto3
4+
from influxdb import InfluxDBClient
45
from mongoengine import connect
56
from redis import Redis
67

@@ -19,3 +20,14 @@ def __init__(self) -> None:
1920

2021
# Redis
2122
self.redis = Redis(host=os.environ['REDIS_HOST'])
23+
24+
# InfluxDB
25+
self.historian_enabled = os.environ.get('HISTORIAN_ENABLE', False) == 'true'
26+
if self.historian_enabled:
27+
self.influx_db_name = os.environ['INFLUXDB_DB']
28+
self.influx_client = InfluxDBClient(host=os.environ['INFLUXDB_HOST'],
29+
username=os.environ['INFLUXDB_ADMIN_USER'],
30+
password=os.environ['INFLUXDB_ADMIN_PASSWORD'])
31+
else:
32+
self.influx_db_name = None
33+
self.influx_client = None

alfalfa_worker/lib/job.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import logging
3-
import os
43
from datetime import datetime
54
from enum import Enum
65
from json.decoder import JSONDecodeError
@@ -76,7 +75,6 @@ def __new_init__(self: "Job", *args, **kwargs):
7675
# Redis
7776
self.redis = connections_manager.redis
7877
self.redis_pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
79-
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
8078
self.logger = logging.getLogger(self.__class__.__name__)
8179

8280
# Message Handlers
@@ -169,6 +167,7 @@ def status(self) -> "JobStatus":
169167

170168
@property
171169
def is_running(self) -> bool:
170+
"""Easily check if the state of the job is running or not"""
172171
return self._status.value < JobStatus.STOPPING.value
173172

174173
@property

0 commit comments

Comments
 (0)