@@ -178,14 +178,14 @@ def test_basic(self, tmp_path, job_manager, job_manager_root_dir, sleep_mock):
178178 )
179179
180180 assert [
181- (r .id , r .status , r .backend_name , r .cpu , r .memory , r . duration , r . costs )
181+ (r .id , r .status , r .backend_name , r .cpu , r .costs )
182182 for r in pd .read_csv (job_db_path ).itertuples ()
183183 ] == [
184- ("job-2018" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
185- ("job-2019" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
186- ("job-2020" , "finished" , "bar" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
187- ("job-2021" , "finished" , "bar" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
188- ("job-2022" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
184+ ("job-2018" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
185+ ("job-2019" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
186+ ("job-2020" , "finished" , "bar" , "1234.5 cpu-seconds" , 123 ),
187+ ("job-2021" , "finished" , "bar" , "1234.5 cpu-seconds" , 123 ),
188+ ("job-2022" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
189189 ]
190190
191191 # Check downloaded results and metadata.
@@ -219,9 +219,89 @@ def test_db_class(self, tmp_path, job_manager, job_manager_root_dir, sleep_mock,
219219 assert set (result .status ) == {"finished" }
220220 assert set (result .backend_name ) == {"foo" , "bar" }
221221 assert set (result .cpu ) == {"1234.5 cpu-seconds" }
222+ assert set (result .costs ) == {123 }
223+
224+ @pytest .mark .parametrize ("db_class" , [CsvJobDatabase , ParquetJobDatabase ])
225+ def test_usage_fields_dynamic_columns (
226+ self , tmp_path , job_manager , dummy_backend_foo , dummy_backend_bar , sleep_mock , db_class
227+ ):
228+ """
229+ All fields reported under "usage" in the job metadata
230+ should dynamically be included as columns in the job db.
231+ """
232+ usage = {
233+ "cpu" : {"unit" : "cpu-seconds" , "value" : 1234.5 },
234+ "memory" : {"unit" : "mb-seconds" , "value" : 34567.89 },
235+ "duration" : {"unit" : "seconds" , "value" : 2345 },
236+ "network" : {"unit" : "b" , "value" : 1000 },
237+ "sentinelhub" : {"unit" : "sentinelhub_processing_unit" , "value" : 7.5 },
238+ }
239+ dummy_backend_foo .next_batch_job_usage = usage
240+ dummy_backend_bar .next_batch_job_usage = usage
241+
242+ df = pd .DataFrame ({"year" : [2018 , 2019 , 2020 , 2021 , 2022 ]})
243+ output_file = tmp_path / "jobs.db"
244+ job_db = db_class (output_file ).initialize_from_df (df )
245+
246+ run_stats = job_manager .run_jobs (job_db = job_db , start_job = self ._create_year_job )
247+ assert run_stats == dirty_equals .IsPartialDict ({"job finished" : 5 })
248+
249+ result = db_class (output_file ).read ()
250+ assert len (result ) == 5
251+ assert set (result .status ) == {"finished" }
252+ assert set (result .cpu ) == {"1234.5 cpu-seconds" }
222253 assert set (result .memory ) == {"34567.89 mb-seconds" }
223254 assert set (result .duration ) == {"2345 seconds" }
224- assert set (result .costs ) == {123 }
255+ assert set (result .network ) == {"1000 b" }
256+ assert set (result .sentinelhub ) == {"7.5 sentinelhub_processing_unit" }
257+
258+ def test_usage_fields_reserved_column_collision (
259+ self , tmp_path , job_manager , dummy_backend_foo , dummy_backend_bar , sleep_mock , caplog
260+ ):
261+ """
262+ Usage fields that collide with reserved job db columns (e.g. "status")
263+ should be skipped instead of overwriting the column.
264+ """
265+ usage = {
266+ "status" : {"unit" : "sneaky" , "value" : 666 },
267+ "network" : {"unit" : "b" , "value" : 1000 },
268+ }
269+ dummy_backend_foo .next_batch_job_usage = usage
270+ dummy_backend_bar .next_batch_job_usage = usage
271+
272+ df = pd .DataFrame ({"year" : [2018 , 2019 ]})
273+ output_file = tmp_path / "jobs.csv"
274+ job_db = CsvJobDatabase (output_file ).initialize_from_df (df )
275+
276+ with caplog .at_level (logging .WARNING ):
277+ job_manager .run_jobs (job_db = job_db , start_job = self ._create_year_job )
278+
279+ result = CsvJobDatabase (output_file ).read ()
280+ assert set (result .status ) == {"finished" }
281+ assert set (result .network ) == {"1000 b" }
282+ assert "Skipping usage field 'status'" in caplog .text
283+
284+ def test_usage_fields_legacy_job_db (self , tmp_path , job_manager , sleep_mock , recwarn ):
285+ """
286+ Resuming from a legacy job db that still has (empty) predefined usage columns
287+ (e.g. "cpu", "memory", "duration") should work without warnings/errors.
288+ """
289+ job_db_path = tmp_path / "jobs.csv"
290+ job_db_path .write_text (
291+ "year,id,backend_name,status,start_time,running_start_time,cpu,memory,duration,costs\n "
292+ "2021,,,not_started,,,,,,\n "
293+ "2022,,,not_started,,,,,,\n "
294+ )
295+
296+ job_db = CsvJobDatabase (job_db_path )
297+ run_stats = job_manager .run_jobs (job_db = job_db , start_job = self ._create_year_job )
298+ assert run_stats == dirty_equals .IsPartialDict ({"start_job call" : 2 , "job finished" : 2 })
299+
300+ result = pd .read_csv (job_db_path )
301+ assert set (result .status ) == {"finished" }
302+ assert set (result .cpu ) == {"1234.5 cpu-seconds" }
303+
304+ assert [(w .category , w .message , str (w )) for w in recwarn .list ] == []
225305
226306 @pytest .mark .parametrize (
227307 ["filename" , "expected_db_class" ],
@@ -274,14 +354,14 @@ def test_start_job_thread_basic(self, tmp_path, job_manager, job_manager_root_di
274354 assert sleep_mock .call_count > 10
275355
276356 assert [
277- (r .id , r .status , r .backend_name , r .cpu , r .memory , r . duration , r . costs )
357+ (r .id , r .status , r .backend_name , r .cpu , r .costs )
278358 for r in pd .read_csv (job_db_path ).itertuples ()
279359 ] == [
280- ("job-2018" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
281- ("job-2019" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
282- ("job-2020" , "finished" , "bar" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
283- ("job-2021" , "finished" , "bar" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
284- ("job-2022" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
360+ ("job-2018" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
361+ ("job-2019" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
362+ ("job-2020" , "finished" , "bar" , "1234.5 cpu-seconds" , 123 ),
363+ ("job-2021" , "finished" , "bar" , "1234.5 cpu-seconds" , 123 ),
364+ ("job-2022" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
285365 ]
286366
287367 # Check downloaded results and metadata.
@@ -301,9 +381,6 @@ def test_normalize_df(self):
301381 "id" ,
302382 "start_time" ,
303383 "running_start_time" ,
304- "cpu" ,
305- "memory" ,
306- "duration" ,
307384 "backend_name" ,
308385 "costs" ,
309386 ]
@@ -358,13 +435,13 @@ def start_worker_thread():
358435 # Also check that we got sensible end results in the job db.
359436 results = pd .read_csv (job_db_path ).replace ({np .nan : None }) # np.nan's are replaced by None for easy comparison
360437 assert [
361- (r .id , r .status , r .backend_name , r .cpu , r .memory , r . duration , r . costs ) for r in results .itertuples ()
438+ (r .id , r .status , r .backend_name , r .cpu , r .costs ) for r in results .itertuples ()
362439 ] == [
363- ("job-2018" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
364- ("job-2019" , "finished" , "foo" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
365- ("job-2020" , "finished" , "bar" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
366- ("job-2021" , "finished" , "bar" , "1234.5 cpu-seconds" , "34567.89 mb-seconds" , "2345 seconds" , 123 ),
367- ("job-2022" , "error" , "foo" , None , None , None , None ),
440+ ("job-2018" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
441+ ("job-2019" , "finished" , "foo" , "1234.5 cpu-seconds" , 123 ),
442+ ("job-2020" , "finished" , "bar" , "1234.5 cpu-seconds" , 123 ),
443+ ("job-2021" , "finished" , "bar" , "1234.5 cpu-seconds" , 123 ),
444+ ("job-2022" , "error" , "foo" , None , None ),
368445 ]
369446
370447 # Check downloaded results and metadata.
@@ -627,7 +704,7 @@ def test_empty_csv_handling(self, tmp_path, sleep_mock, recwarn, job_manager):
627704
628705 assert job_db_path .exists ()
629706 # Simple check for empty columns in the CSV file
630- assert ",,,,, " in job_db_path .read_text ()
707+ assert "2021 ,,,not_started,,, \n " in job_db_path .read_text ()
631708
632709 # Start over with existing file
633710 job_db = CsvJobDatabase (job_db_path )
0 commit comments