1+ from __future__ import unicode_literals
2+
3+ import threading
4+ import time
5+
16import copy
27import datetime
38import json
1823 NotebookResultError ,
1924 python_template_dir ,
2025)
21- from notebooker .serialization .serialization import get_serializer_from_cls
26+ from notebooker .serialization .serialization import get_serializer_from_cls , initialize_serializer_from_config
2227from notebooker .settings import BaseConfig
2328from notebooker .utils .conversion import _output_ipynb_name , generate_ipynb_from_py , ipython_to_html , ipython_to_pdf
2429from notebooker .utils .filesystem import initialise_base_dirs
@@ -104,11 +109,7 @@ def _run_checks(
104109
105110 logger .info ("Executing notebook at {} using parameters {} --> {}" .format (ipynb_raw_path , overrides , output_ipynb ))
106111 pm .execute_notebook (
107- ipynb_raw_path ,
108- ipynb_executed_path ,
109- parameters = overrides ,
110- log_output = True ,
111- prepare_only = prepare_only ,
112+ ipynb_raw_path , ipynb_executed_path , parameters = overrides , log_output = True , prepare_only = prepare_only
112113 )
113114 with open (ipynb_executed_path , "r" ) as f :
114115 raw_executed_ipynb = f .read ()
@@ -167,11 +168,7 @@ def run_report(
167168 job_id = job_id or str (uuid .uuid4 ())
168169 stop_execution = os .getenv ("NOTEBOOKER_APP_STOPPING" )
169170 if stop_execution :
170- logger .info (
171- "Aborting attempt to run %s, jobid=%s as app is shutting down." ,
172- report_name ,
173- job_id ,
174- )
171+ logger .info ("Aborting attempt to run %s, jobid=%s as app is shutting down." , report_name , job_id )
175172 result_serializer .update_check_status (job_id , JobStatus .CANCELLED , error_info = CANCEL_MESSAGE )
176173 return
177174 try :
@@ -182,10 +179,7 @@ def run_report(
182179 attempts_remaining ,
183180 )
184181 result_serializer .update_check_status (
185- job_id ,
186- report_name = report_name ,
187- job_start_time = job_submit_time ,
188- status = JobStatus .PENDING ,
182+ job_id , report_name = report_name , job_start_time = job_submit_time , status = JobStatus .PENDING
189183 )
190184 result = _run_checks (
191185 job_id ,
@@ -439,3 +433,126 @@ def docker_compose_entrypoint():
439433 logger .info ("Received a request to run a report with the following parameters:" )
440434 logger .info (args_to_execute )
441435 return subprocess .Popen (args_to_execute ).wait ()
436+
437+
438+ def _monitor_stderr (process , job_id , serializer_cls , serializer_args ):
439+ stderr = []
440+ # Unsure whether flask app contexts are thread-safe; just reinitialise the serializer here.
441+ result_serializer = get_serializer_from_cls (serializer_cls , ** serializer_args )
442+ while True :
443+ line = process .stderr .readline ().decode ("utf-8" )
444+ if line != "" :
445+ stderr .append (line )
446+ logger .info (line ) # So that we have it in the log, not just in memory.
447+ result_serializer .update_stdout (job_id , new_lines = [line ])
448+ elif process .poll () is not None :
449+ result_serializer .update_stdout (job_id , stderr , replace = True )
450+ break
451+ return "" .join (stderr )
452+
453+
454+ def run_report_in_subprocess (
455+ base_config ,
456+ report_name ,
457+ report_title ,
458+ mailto ,
459+ overrides ,
460+ * ,
461+ hide_code = False ,
462+ generate_pdf_output = False ,
463+ prepare_only = False ,
464+ scheduler_job_id = None ,
465+ run_synchronously = False ,
466+ mailfrom = None ,
467+ n_retries = 3 ,
468+ is_slideshow = False ,
469+ ) -> str :
470+ """
471+ Execute the Notebooker report in a subprocess.
472+ Uses a subprocess to execute the report asynchronously, which is identical to the non-webapp entrypoint.
473+ :param base_config: `BaseConfig` A set of configuration options which specify serialisation parameters.
474+ :param report_name: `str` The report which we are executing
475+ :param report_title: `str` The user-specified title of the report
476+ :param mailto: `Optional[str]` Who the results will be emailed to
477+ :param overrides: `Optional[Dict[str, Any]]` The parameters to be passed into the report
478+ :param generate_pdf_output: `bool` Whether we're generating a PDF. Defaults to False.
479+ :param prepare_only: `bool` Whether to do everything except execute the notebook. Useful for testing.
480+ :param scheduler_job_id: `Optional[str]` if the job was triggered from the scheduler, this is the scheduler's job id
481+ :param run_synchronously: `bool` If True, then we will join the stderr monitoring thread until the job has completed
482+ :param mailfrom: `str` if passed, then this string will be used in the from field
483+ :param n_retries: The number of retries to attempt.
484+ :param is_slideshow: Whether the notebook is a reveal.js slideshow or not.
485+ :return: The unique job_id.
486+ """
487+ job_id = str (uuid .uuid4 ())
488+ job_start_time = datetime .datetime .now ()
489+ result_serializer = initialize_serializer_from_config (base_config )
490+ result_serializer .save_check_stub (
491+ job_id ,
492+ report_name ,
493+ report_title = report_title ,
494+ job_start_time = job_start_time ,
495+ status = JobStatus .SUBMITTED ,
496+ overrides = overrides ,
497+ mailto = mailto ,
498+ generate_pdf_output = generate_pdf_output ,
499+ hide_code = hide_code ,
500+ scheduler_job_id = scheduler_job_id ,
501+ is_slideshow = is_slideshow ,
502+ )
503+
504+ command = (
505+ [
506+ os .path .join (sys .exec_prefix , "bin" , "notebooker-cli" ),
507+ "--output-base-dir" ,
508+ base_config .OUTPUT_DIR ,
509+ "--template-base-dir" ,
510+ base_config .TEMPLATE_DIR ,
511+ "--py-template-base-dir" ,
512+ base_config .PY_TEMPLATE_BASE_DIR ,
513+ "--py-template-subdir" ,
514+ base_config .PY_TEMPLATE_SUBDIR ,
515+ "--default-mailfrom" ,
516+ base_config .DEFAULT_MAILFROM ,
517+ ]
518+ + (["--notebooker-disable-git" ] if base_config .NOTEBOOKER_DISABLE_GIT else [])
519+ + ["--serializer-cls" , result_serializer .__class__ .__name__ ]
520+ + result_serializer .serializer_args_to_cmdline_args ()
521+ + [
522+ "execute-notebook" ,
523+ "--job-id" ,
524+ job_id ,
525+ "--report-name" ,
526+ report_name ,
527+ "--report-title" ,
528+ report_title ,
529+ "--mailto" ,
530+ mailto ,
531+ "--overrides-as-json" ,
532+ json .dumps (overrides ),
533+ "--pdf-output" if generate_pdf_output else "--no-pdf-output" ,
534+ "--hide-code" if hide_code else "--show-code" ,
535+ "--n-retries" ,
536+ str (n_retries ),
537+ ]
538+ + (["--prepare-notebook-only" ] if prepare_only else [])
539+ + (["--is-slideshow" ] if is_slideshow else [])
540+ + ([f"--scheduler-job-id={ scheduler_job_id } " ] if scheduler_job_id is not None else [])
541+ + ([f"--mailfrom={ mailfrom } " ] if mailfrom is not None else [])
542+ )
543+ p = subprocess .Popen (command , stdout = subprocess .PIPE , stderr = subprocess .PIPE )
544+
545+ stderr_thread = threading .Thread (
546+ target = _monitor_stderr , args = (p , job_id , base_config .SERIALIZER_CLS , base_config .SERIALIZER_CONFIG )
547+ )
548+ stderr_thread .daemon = True
549+ stderr_thread .start ()
550+ if run_synchronously :
551+ p .wait ()
552+ else :
553+ time .sleep (1 )
554+ p .poll ()
555+ if p .returncode :
556+ raise RuntimeError (f"The report execution failed with exit code { p .returncode } " )
557+
558+ return job_id
0 commit comments