def schedule_contract_invocation( contract_model: "smart_contract_model.SmartContractModel", action: SchedulerActions = SchedulerActions.CREATE) -> None: """Schedule cron smart contracts. Call this method only after confirming sc_type is 'cron' Args: contract_model: a smart contract model which is type cron Raises: exceptions.TimingEventSchedulerError: when contract model is in a bad state """ if (action.value != "delete") and (contract_model.cron is None and contract_model.seconds is None): raise exceptions.TimingEventSchedulerError( "You must provide cron or seconds to schedule a job") redis.lpush_sync( "mq:scheduler", json.dumps( { "action": action.value, "contract_id": contract_model.id, "txn_type": contract_model.txn_type, "execution_order": contract_model.execution_order, "cron": contract_model.cron, "seconds": contract_model.seconds, }, separators=(",", ":"), ), )
def update(self, txn_type: Optional[str] = None, execution_order: Optional[str] = None, cron: Optional[str] = None, seconds: Optional[int] = None) -> None: """Update this event instance Args: cron: cron expression string ex: "* * * * *" seconds: integer of seconds between events. ex: 60 txn_type: updated transaction type to assign to this instance execution_order: "serial" or "parallel Raises: exceptions.TimingEventSchedulerError<BAD_REQUEST> if no provided update parameters """ if (not cron and not seconds) or ( cron and seconds) or not execution_order or not txn_type: raise exceptions.TimingEventSchedulerError("BAD_REQUEST") if cron: self.cron = cron self.seconds = None elif seconds: self.seconds = seconds self.cron = None if txn_type: self.txn_type = txn_type if execution_order: self.execution_order = execution_order self.save() background_scheduler.background_scheduler.reschedule_job( self.id, trigger=self.get_trigger())
def parse_json_or_fail(json_str: Union[str, bytes]) -> Any: """ Common method for parsing json, and failing if malformed. """ try: return json.loads(json_str) except json.decoder.JSONDecodeError: raise exceptions.TimingEventSchedulerError("MALFORMED_JSON")
def worker(change_request: dict) -> None: """Process incoming change requests Args: change_request: dict<ChangeRequest> {contract_id: string, action: SchedulerActions enum, seconds?:int, cron?: string} """ _log.debug(f"Change Request: {change_request}") contract_id = change_request["contract_id"] action = change_request["action"] seconds = change_request.get("seconds") cron = change_request.get("cron") txn_type = change_request["txn_type"] execution_order = change_request["execution_order"] # Delete jobs if action == "delete": if not timing_event.exists(contract_id): raise exceptions.TimingEventSchedulerError("NOT_FOUND") event = timing_event.get_by_id(contract_id) event.delete() # Update job if action == "update": if not timing_event.exists(contract_id): raise exceptions.TimingEventSchedulerError("NOT_FOUND") event = timing_event.get_by_id(contract_id) event.update(cron=cron, seconds=seconds, execution_order=execution_order, txn_type=txn_type) # Create new job if action == "create": event = timing_event.TimingEvent(cron=cron, seconds=seconds, timing_id=contract_id, execution_order=execution_order, txn_type=txn_type) event.start() _log.debug(f"Successful {action} on job '{contract_id}'")
def start(self) -> None: """Start this timing event's scheduler. Raises: exceptions.TimingEventSchedulerError<CONFLICT>: when a job with this ID already exists. """ self.save() _log.debug("Job params successfully saved.") try: background_scheduler.background_scheduler.add_job( self.submit_invocation_request, max_instances=1, trigger=self.get_trigger(), id=self.id) except ConflictingIdError: raise exceptions.TimingEventSchedulerError("CONFLICT")
def get_by_id(timing_id: str) -> "TimingEvent": """Return a TimingEvent instance located by ID. Args: timing_id: The id of a running timing event. Usually a contract_id Returns: Instantiated timingEvent instance if found Raises: exceptions.TimingEventSchedulerError if event is not found """ result = redis.hget_sync(JOB_PARAMS_KEY, timing_id, decode=False) if not result: raise exceptions.TimingEventSchedulerError("NOT_FOUND") params = json.loads(result) return TimingEvent(timing_id=params["contract_id"], seconds=params.get("seconds"), cron=params.get("cron"))
class SchedulerTest(unittest.TestCase): @patch("dragonchain.scheduler.scheduler.worker") @patch("dragonchain.scheduler.scheduler.redis.delete_sync", return_value="OK") @patch("dragonchain.scheduler.scheduler.redis.lpush_sync", return_value="OK") @patch("dragonchain.scheduler.scheduler.redis.brpop_sync", return_value=[ "whatever", '{"action":"create","contract_id":"apples","seconds":60}' ]) def test_subscribe1(self, brpop, lpush, delete, mock_worker): scheduler.subscribe("mq:scheduler") mock_worker.assert_called_with({ "action": "create", "contract_id": "apples", "seconds": 60 }) @patch("dragonchain.scheduler.scheduler.worker", side_effect=exceptions.TimingEventSchedulerError("boom")) @patch("dragonchain.scheduler.scheduler.redis.delete_sync", return_value="OK") @patch("dragonchain.scheduler.scheduler.redis.lpush_sync", return_value="OK") @patch("dragonchain.scheduler.scheduler.redis.brpop_sync", return_value=[ "whatever", '{"action":"create","contract_id":"apples","seconds":60}' ]) def test_subscribe2(self, brpop, lpush, delete, mock_worker): self.assertRaises(exceptions.TimingEventSchedulerError, scheduler.subscribe, "mq:scheduler") lpush.assert_called_with("mq:scheduler:errors", ANY) @patch("dragonchain.scheduler.scheduler.redis.lpush_sync", return_value="OK") @patch("dragonchain.scheduler.scheduler.redis.hgetall_sync", return_value={"banana": '{"contract_id":"banana","seconds":54}'}) def test_revive_dead_workers(self, hgetall, lpush): scheduler.revive_dead_workers() hgetall.assert_called_with("scheduler:params", decode=False) lpush.assert_called_with( "mq:scheduler", '{"contract_id":"banana","seconds":54,"action":"create"}') def test_parse_json_or_fail(self): try: scheduler.parse_json_or_fail("{{{{{}{}") except Exception as e: self.assertEqual(str(e), "MALFORMED_JSON") return self.fail() # Force a failure if no exception thrown @patch("dragonchain.scheduler.scheduler.redis.lpush_sync") def test_schedule_contract_invocation(self, lpush): sc_model = MagicMock() sc_model.id = "my_name" sc_model.cron = "* * * * *" sc_model.seconds = None sc_model.txn_type = "banana" sc_model.execution_order = "serial" scheduler.schedule_contract_invocation(sc_model) lpush.assert_called_with( "mq:scheduler", '{"action":"create","contract_id":"my_name","txn_type":"banana","execution_order":"serial","cron":"* * * * *","seconds":null}', ) @patch("dragonchain.scheduler.scheduler.redis.lpush_sync") def test_schedule_contract_invocation_raises(self, lpush): sc_model = FakeScModel("my_name", None, None) try: scheduler.schedule_contract_invocation(sc_model) self.fail("no error raised") except Exception as e: self.assertEqual( str(e), "You must provide cron or seconds to schedule a job") return self.fail() # Force a failure if no exception thrown # CREATE NON EXISTENT JOB @patch("dragonchain.scheduler.timing_event.redis.hexists_sync", return_value=False) @patch("dragonchain.scheduler.timing_event.redis.hset_sync", return_value="1") def test_create_new_job(self, hset, hexists): change_request = { "action": "create", "contract_id": "goo", "txn_type": "banana", "execution_order": "serial", "cron": "* * * * *" } scheduler.worker(change_request) hset.assert_called_with( "scheduler:params", "goo", '{"cron":"* * * * *","seconds":null,"contract_id":"goo","execution_order":"serial","txn_type":"banana"}' ) # CREATE EXISTING JOB @patch("dragonchain.scheduler.timing_event.redis.hset_sync") @patch("dragonchain.scheduler.timing_event.redis.hexists_sync", return_value=True) @patch("apscheduler.schedulers.background.BackgroundScheduler.add_job", side_effect=ConflictingIdError("goo")) def test_create_existing_job(self, hexists, mock_hexists, mock_hset): self.assertRaises( exceptions.TimingEventSchedulerError, scheduler.worker, { "action": "create", "contract_id": "goo", "txn_type": "banana", "execution_order": "serial", "cron": "* * * * *" }, ) # DELETE EXISTING JOB @patch("dragonchain.scheduler.timing_event.redis.hexists_sync") @patch("dragonchain.scheduler.timing_event.redis.hget_sync", return_value='{"contract_id":"goo","action":"delete","seconds":60}') @patch("dragonchain.scheduler.timing_event.redis.hdel_sync") @patch("apscheduler.schedulers.background.BackgroundScheduler.remove_job") def test_delete_job(self, remove_job, hdel, hget, hexists): change_request = { "action": "delete", "contract_id": "banana", "txn_type": "banana", "execution_order": "serial" } scheduler.worker(change_request) remove_job.assert_called_once() hdel.assert_called_once() # DELETE NON EXISTENT JOB @patch("dragonchain.scheduler.scheduler.timing_event.exists", return_value=False) @patch("dragonchain.scheduler.timing_event.redis.hget_sync", return_value='{"contract_id":"goo","action":"delete","seconds":60}') @patch("dragonchain.scheduler.timing_event.redis.hdel_sync") @patch("apscheduler.schedulers.background.BackgroundScheduler.remove_job") def test_delete_non_existent_job(self, remove_job, hdel, hget, exists): change_request = { "action": "delete", "contract_id": "banana", "txn_type": "banana", "execution_order": "serial" } scheduler.worker(change_request) remove_job.assert_not_called() hdel.assert_not_called() hget.assert_not_called() # UPDATE @patch("dragonchain.scheduler.scheduler.timing_event.exists", return_value=True) @patch( "apscheduler.schedulers.background.BackgroundScheduler.reschedule_job") @patch("dragonchain.scheduler.timing_event.redis.hget_sync", return_value='{"contract_id":"whatever"}') @patch("dragonchain.scheduler.timing_event.redis.hset_sync") def test_update_job(self, mock_hset, mock_hget, reschedule_job, exists): change_request = { "action": "update", "contract_id": "banana", "execution_order": "serial", "txn_type": "banana", "seconds": 61 } scheduler.worker(change_request) reschedule_job.assert_called_with("whatever", trigger=ANY) # UPDATE NON EXISTENT JOB @patch("dragonchain.scheduler.scheduler.timing_event.exists", return_value=False) @patch( "dragonchain.scheduler.scheduler.redis.hgetall_sync", return_value={ "a": '{"action":"update","contract_id":"goo","execution_order":"serial","txn_type":"banana",seconds":60}' }, ) def test_update_non_existent_job(self, hgetall, hexists): change_request = { "action": "update", "contract_id": "banana", "execution_order": "serial", "txn_type": "banana", "seconds": 61 } self.assertRaises(exceptions.TimingEventSchedulerError, scheduler.worker, change_request)