Assumption, the task runner is external to our system, and has unlimited resources to run tasks, for example, it's a serverless function.
List the key functional requirements for the system (Ask the AI for hints if stuck)...
List the key non-functional requirements (performance, scalability, reliability, etc.)...
For such system, I can think the task creation or delte frequency is low, while the task execution frequence and concurrency is high.
A task representation can be like below:
1 record consumes just 1KB data, if there are total 10k tasks, then I only need 10MB storage.
A task execution record can be like:
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
For task:
create_task
params:
response:
delete_task
params:
For task execution:
create_task_executions:
params:
Response:
get_task_execution:
params:
update_task_execution:
user
tasks
executions
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
The system contains the following components:
To create, delete or get task record.
To create task execution, or update task execution state.
Based on task's scheduling policy, create the specific executions.
Monitor the executions to be executed in the near future, and push the message into the queue.
It will trigger the task runner instance(for example AWS serverless function), and then monitor the task runner instance status by event listening or long polling. After the task runner instance finishes, it will update the execution's status.
Decouple the scheduler and execution workers, make them can scale independently.
I can use RabbitMQ here because it supports avdanced mechanism such as message ACK and support high consumer concurrency.
And the following are external components which may not be maintained by us:
Notify the task's owner that the task execution state.
The real process to run the task, it may be a k8s job, or some async job system.
To clarrify the system's running process, let's summarize what happends for a user submitted scheduling task:
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
To achieve minimal delay in the system, I will apply cache with database for the task service and execution service:
Execution service is the possible bottleneck, let me introduce it in detail:
The API `create_executions` with until may be invoked frequently by the Execution monitor.
It will store a task's record inside database, if new executions are created, then the task record's last_schedule_time will be updated, then the cache item is removed. If no new executions need to be created, then the task record will be write into the cache if it doesn't exist.
And the execution's record will also be written into the cache after it's updated, so that the query will be efficient.
Execution monitor is a stateless process, and it will check to-be-executed executions in the near future periodically. I can deploy multiple instances for high reliability, and each of them works independently. And they use row-level locking in the database to avoid duplicate execution messages. If an execution state is already SCHEDULED, nothing will happen.
Its logic is as follows:
```
// find all to-be-executed executions in the near future
// randomize the executions
// iterate every execution
// lock the execution row
// if the execution state is already SCHEDULED, abort
// create the execution message
// update the execution state
// commit the DB update
```
This is row-level locking, so the execution creation can run with high concurrency.
The scheduler is very similar to the execution monitor for high performance and reliability. It will check the task record row and lock it to call `create_task_executions`.
It's the only stateful component of the system. I must ensure that it can recover from failure gracefully to achieve high reliability.
Even the execution worker is stateful, but its state is dumped into the database. If it crashes unexpectedly, the execution message will be automatically redistributed to another execution worker and re-executed, without triggering a new external task runner.
It's evident that the execution worker is light load, and it's very easy to scale the workers horizontally. The high reliability and scalability are achieved. If I find that the average delay between created and start time is increasing, we can scale the replicas of execution worker.
The system's execution data is massive, for example 10 0k rows every day, but the execution records will not be used for scheduling after the execution finished. We can move the finished execution data every week or month to the archive storage for lower cost and higher performance of the DB.