Example #1
0
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=(",", ":"),
        ),
    )
Example #2
0
 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())
Example #3
0
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")
Example #4
0
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}'")
Example #5
0
 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")
Example #6
0
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)