def test_process_stop_cycle_point(get_point, options, expected): # Mock the get_point function - we don't care here get_point.return_value = None inputs = { 'is_restart': False, 'cfg': { 'scheduling': { 'stop after cycle point': '1993' } }, 'final_point': '2000', } inputs.update(options) # Create a mock scheduler object and assign values to it. scheduler = create_autospec(Scheduler) scheduler.is_restart = inputs.get('is_restart') scheduler.options = RunOptions(stopcp=inputs.get('cli_stop_point')) # Set-up fake config object scheduler.config = SimpleNamespace(final_point=inputs.get('final_point'), cfg=inputs.get('cfg')) # Set up fake taskpool scheduler.pool = Mock(stop_point=inputs.get('db_stop_point')) Scheduler.process_stop_cycle_point(scheduler) assert scheduler.options.stopcp == expected
async def _run_flow( caplog: Optional[pytest.LogCaptureFixture], schd: Scheduler, level: int = logging.INFO ): """Start a scheduler and set it running.""" if caplog: caplog.set_level(level, CYLC_LOG) # install await schd.install() # start try: await schd.start() except Exception as exc: async with timeout(5): await schd.shutdown(exc) # run try: task = asyncio.create_task(schd.run_scheduler()) yield caplog # stop finally: async with timeout(5): # ask the scheduler to shut down nicely schd._set_stop(StopMode.REQUEST_NOW_NOW) await task if task: # leave everything nice and tidy task.cancel()
async def _run_flow(run_dir: Path, caplog: Optional[pytest.LogCaptureFixture], scheduler: Scheduler, level: int = logging.INFO): """Start a scheduler.""" contact = (run_dir / scheduler.workflow / WorkflowFiles.Service.DIRNAME / WorkflowFiles.Service.CONTACT) if caplog: caplog.set_level(level, CYLC_LOG) task = None started = False await scheduler.install() try: task = asyncio.get_event_loop().create_task(scheduler.run()) started = await _poll_file(contact) yield caplog finally: if started: # ask the scheduler to shut down nicely async with timeout(5): scheduler._set_stop(StopMode.REQUEST_NOW_NOW) await task if task: # leave everything nice and tidy task.cancel()
def test_check_startup_opts(opts: Dict[str, Optional[str]], is_restart: bool, err: List[str]): """Test Scheduler.process_cycle_point_opts()""" mocked_scheduler = Mock() mocked_scheduler.options = Mock(spec=[], **opts) mocked_scheduler.is_restart = is_restart with pytest.raises(SchedulerError) as excinfo: Scheduler._check_startup_opts(mocked_scheduler) assert (err in str(excinfo))
def test_check_startup_opts(opts_to_test: List[str], is_restart: bool, err_msg: str) -> None: """Test Scheduler._check_startup_opts()""" for opt in opts_to_test: mocked_scheduler = Mock(is_restart=is_restart) mocked_scheduler.options = SimpleNamespace(**{opt: 'reload'}) with pytest.raises(UserInputError) as excinfo: Scheduler._check_startup_opts(mocked_scheduler) assert (err_msg.format(opt) in str(excinfo))
def scheduler_cli(parser, options, args, is_restart=False): """CLI main.""" reg = args[0] # Check suite is not already running before start of host selection. try: suite_files.detect_old_contact_file(reg) except SuiteServiceFileError as exc: sys.exit(exc) suite_run_dir = get_suite_run_dir(reg) if not os.path.exists(suite_run_dir): sys.stderr.write(f'suite service directory not found ' f'at: {suite_run_dir}\n') sys.exit(1) # Create auth files if needed. suite_files.create_auth_files(reg) # Extract job.sh from library, for use in job scripts. extract_resources(suite_files.get_suite_srv_dir(reg), ['etc/job.sh']) # Check whether a run host is explicitly specified, else select one. if not options.host: try: host = HostAppointer().appoint_host() except EmptyHostList as exc: if cylc.flow.flags.debug: raise else: sys.exit(str(exc)) if is_remote_host(host): if is_restart: base_cmd = ["restart"] + sys.argv[1:] else: base_cmd = ["run"] + sys.argv[1:] # Prevent recursive host selection base_cmd.append("--host=localhost") return remote_cylc_cmd(base_cmd, host=host) if remrun(set_rel_local=True): # State localhost as above. sys.exit() try: suite_files.get_suite_source_dir(args[0], options.owner) except SuiteServiceFileError: # Source path is assumed to be the run directory suite_files.register(args[0], get_suite_run_dir(args[0])) try: scheduler = Scheduler(is_restart, options, args) except SuiteServiceFileError as exc: sys.exit(exc) scheduler.start()
def _make_scheduler(reg, **opts): """Return a scheduler object for a flow registration.""" # This allows paused_start to be overriden: opts = {'paused_start': True, **opts} options = RunOptions(**opts) # create workflow return Scheduler(reg, options)
def test_process_cycle_point_opts(opts: Dict[str, Optional[str]], is_restart: bool, expected_set_opts: Dict[str, Optional[str]], expected_warnings: List[str], caplog: Fixture): """Test Scheduler.process_cycle_point_opts()""" caplog.set_level(logging.WARNING, CYLC_LOG) mocked_scheduler = Mock() mocked_scheduler.options = Mock(spec=[], **opts) mocked_scheduler.is_restart = is_restart Scheduler.process_cycle_point_opts(mocked_scheduler) for opt, value in expected_set_opts.items(): assert getattr(mocked_scheduler.options, opt) == value actual_warnings: List[str] = [rec.message for rec in caplog.records] assert sorted(actual_warnings) == sorted(expected_warnings)
def __make_scheduler(reg: str, **opts: Any) -> Scheduler: # This allows paused_start to be overriden: opts = {'paused_start': True, **opts} options = RunOptions(**opts) # create workflow nonlocal schd schd = Scheduler(reg, options) return schd
def test_ioerror_is_ignored(self, mocked_get_suite_source_dir): """Test that IOError's are ignored when closing Scheduler logs. When a disk errors occurs, the scheduler.close_logs method may result in an IOError. This, combined with other variables, may cause an infinite loop. So it is better that it is ignored.""" mocked_get_suite_source_dir.return_value = '.' options = Options() args = ["suiteA"] scheduler = Scheduler(is_restart=False, options=options, args=args) handler = mock.MagicMock() handler.close.side_effect = IOError handler.level = logging.INFO LOG.addHandler(handler) scheduler.close_logs() self.assertEqual(1, handler.close.call_count) LOG.removeHandler(handler)
def _make_scheduler(reg, is_restart=False, **opts): """Return a scheduler object for a flow registration.""" opts = {'hold_start': True, **opts} # get options object if is_restart: options = RestartOptions(**opts) else: options = RunOptions(**opts) # create workflow return Scheduler(reg, options, is_restart=is_restart)
def scheduler_cli(parser, options, args, is_restart=False): """CLI main.""" # Check suite is not already running before start of host selection. try: SuiteSrvFilesManager().detect_old_contact_file(args[0]) except SuiteServiceFileError as exc: sys.exit(exc) # Create auth files if needed. SuiteSrvFilesManager().create_auth_files(args[0]) # Check whether a run host is explicitly specified, else select one. if not options.host: try: host = HostAppointer().appoint_host() except EmptyHostList as exc: if cylc.flow.flags.debug: raise else: sys.exit(str(exc)) if is_remote_host(host): if is_restart: base_cmd = ["restart"] + sys.argv[1:] else: base_cmd = ["run"] + sys.argv[1:] # Prevent recursive host selection base_cmd.append("--host=localhost") return remote_cylc_cmd(base_cmd, host=host) if remrun(set_rel_local=True): # State localhost as above. sys.exit() try: SuiteSrvFilesManager().get_suite_source_dir(args[0], options.owner) except SuiteServiceFileError: # Source path is assumed to be the run directory SuiteSrvFilesManager().register(args[0], get_suite_run_dir(args[0])) try: scheduler = Scheduler(is_restart, options, args) except SuiteServiceFileError as exc: sys.exit(exc) scheduler.start()
def test_ioerror_is_ignored(self, mocked_suite_srv_files_mgr, mocked_suite_db_mgr, mocked_broadcast_mgr): """Test that IOError's are ignored when closing Scheduler logs. When a disk errors occurs, the scheduler.close_logs method may result in an IOError. This, combined with other variables, may cause an infinite loop. So it is better that it is ignored.""" mocked_suite_srv_files_mgr.return_value\ .get_suite_source_dir.return_value = "." options = Options() args = ["suiteA"] scheduler = Scheduler(is_restart=False, options=options, args=args) handler = mock.MagicMock() handler.close.side_effect = IOError handler.level = logging.INFO LOG.addHandler(handler) scheduler.close_logs() self.assertEqual(1, handler.close.call_count) LOG.removeHandler(handler)
def scheduler_cli(parser, options, args, is_restart=False): """Implement cylc (run|restart). This function should contain all of the command line facing functionality of the Scheduler, exit codes, logging, etc. The Scheduler itself should be a Python object you can import and run in a regular Python session so cannot contain this kind of functionality. """ reg = args[0] # Check suite is not already running before start of host selection. try: suite_files.detect_old_contact_file(reg) except SuiteServiceFileError as exc: sys.exit(exc) _check_registration(reg) # re-execute on another host if required _distribute(options.host, is_restart) # print the start message if options.no_detach or options.format == 'plain': _start_print_blurb() # setup the scheduler # NOTE: asyncio.run opens an event loop, runs your coro, # then shutdown async generators and closes the event loop scheduler = Scheduler(reg, options, is_restart=is_restart) asyncio.run(_setup(parser, options, reg, is_restart, scheduler)) # daemonize if requested # NOTE: asyncio event loops cannot persist across daemonization # ensure you have tidied up all threads etc before daemonizing if not options.no_detach: from cylc.flow.daemonize import daemonize daemonize(scheduler) # setup loggers _open_logs(reg, options.no_detach) # run the workflow ret = asyncio.run(_run(parser, options, reg, is_restart, scheduler)) # exit # NOTE: we must clean up all asyncio / threading stuff before exiting # NOTE: any threads which include sleep statements could cause # sys.exit to hang if not shutdown properly LOG.info("DONE") _close_logs() sys.exit(ret)
async def _run_flow(caplog: Optional[pytest.LogCaptureFixture], schd: Scheduler, level: int = logging.INFO): """Start a scheduler and set the main loop running.""" if caplog: caplog.set_level(level, CYLC_LOG) await schd.install() task: Optional[asyncio.Task] = None try: # Nested `try...finally` to ensure caplog always yielded even if # exception occurs in Scheduler try: await schd.start() # Do not await as we need to yield control to the main loop: task = asyncio.create_task(schd.run_scheduler()) finally: # After this `yield`, the `with` block of the context manager # is executed: yield caplog finally: # Cleanup - this always runs after the `with` block of the # context manager. # Need to shut down Scheduler, but time out in case something # goes wrong: async with timeout(5): if task: # ask the scheduler to shut down nicely, # let main loop handle it: schd._set_stop(StopMode.REQUEST_NOW_NOW) await task if schd.contact_data: async with timeout(5): # Scheduler still running... try more forceful tear down: await schd.shutdown(SchedulerStop("integration test teardown")) if task: # Brute force cleanup if something went wrong: task.cancel()
def scheduler_cli(options: 'Values', workflow_id: str) -> None: """Run the workflow. This function should contain all of the command line facing functionality of the Scheduler, exit codes, logging, etc. The Scheduler itself should be a Python object you can import and run in a regular Python session so cannot contain this kind of functionality. """ # Parse workflow name but delay Cylc 7 suiter.rc deprecation warning # until after the start-up splash is printed. # TODO: singleton (workflow_id, ), _ = parse_ids( workflow_id, constraint='workflows', max_workflows=1, # warn_depr=False, # TODO ) try: detect_old_contact_file(workflow_id) except ServiceFileError as exc: print(f"Resuming already-running workflow\n\n{exc}") pclient = WorkflowRuntimeClient( workflow_id, timeout=options.comms_timeout, ) mutation_kwargs = { 'request_string': RESUME_MUTATION, 'variables': { 'wFlows': [workflow_id] } } pclient('graphql', mutation_kwargs) sys.exit(0) # re-execute on another host if required _distribute(options.host) # print the start message if (cylc.flow.flags.verbosity > -1 and (options.no_detach or options.format == 'plain')): print(cparse(cylc_header())) if cylc.flow.flags.cylc7_back_compat: LOG.warning(SUITERC_DEPR_MSG) # setup the scheduler # NOTE: asyncio.run opens an event loop, runs your coro, # then shutdown async generators and closes the event loop scheduler = Scheduler(workflow_id, options) asyncio.run(_setup(scheduler)) # daemonize if requested # NOTE: asyncio event loops cannot persist across daemonization # ensure you have tidied up all threads etc before daemonizing if not options.no_detach: from cylc.flow.daemonize import daemonize daemonize(scheduler) # setup loggers _open_logs(workflow_id, options.no_detach) # run the workflow ret = asyncio.run(_run(scheduler)) # exit # NOTE: we must clean up all asyncio / threading stuff before exiting # NOTE: any threads which include sleep statements could cause # sys.exit to hang if not shutdown properly LOG.info("DONE") close_log(LOG) sys.exit(ret)
def scheduler_cli(parser, options, reg): """Run the workflow. This function should contain all of the command line facing functionality of the Scheduler, exit codes, logging, etc. The Scheduler itself should be a Python object you can import and run in a regular Python session so cannot contain this kind of functionality. """ workflow_files.validate_flow_name(reg) reg = os.path.normpath(reg) try: workflow_files.detect_old_contact_file(reg) except ServiceFileError as exc: print(f"Resuming already-running workflow\n\n{exc}") pclient = WorkflowRuntimeClient(reg, timeout=options.comms_timeout) mutation_kwargs = { 'request_string': RESUME_MUTATION, 'variables': { 'wFlows': [reg] } } pclient('graphql', mutation_kwargs) sys.exit(0) # re-execute on another host if required _distribute(options.host) # print the start message if (cylc.flow.flags.verbosity > -1 and (options.no_detach or options.format == 'plain')): print(cparse(cylc_header())) # setup the scheduler # NOTE: asyncio.run opens an event loop, runs your coro, # then shutdown async generators and closes the event loop scheduler = Scheduler(reg, options) asyncio.run(_setup(scheduler)) # daemonize if requested # NOTE: asyncio event loops cannot persist across daemonization # ensure you have tidied up all threads etc before daemonizing if not options.no_detach: from cylc.flow.daemonize import daemonize daemonize(scheduler) # setup loggers _open_logs(reg, options.no_detach) # run the workflow ret = asyncio.run(_run(scheduler)) # exit # NOTE: we must clean up all asyncio / threading stuff before exiting # NOTE: any threads which include sleep statements could cause # sys.exit to hang if not shutdown properly LOG.info("DONE") _close_logs() sys.exit(ret)