1+ import logging
12import os
23from dataclasses import dataclass
34from datetime import datetime , timedelta
45from enum import auto
56
6- from influxdb import InfluxDBClient
7-
7+ from alfalfa_worker .lib .alfalfa_connections_manager import (
8+ AlafalfaConnectionsManager
9+ )
810from alfalfa_worker .lib .constants import DATETIME_FORMAT
911from alfalfa_worker .lib .enums import AutoName , RunStatus
1012from 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
82102class 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
0 commit comments