Skip to content

Commit 882f208

Browse files
authored
feat: schedule tasks (#493)
Closes #492 Enables scheduling static periodic tasks (repeats the exact same instruction a fixed number of times) in the ephemeral rollup. Refer to the [MIMD](#479) for detailed explanations. # Changes ## Task scheduler A new component that starts along the validator. It listens for changes to a newly introduced special `TaskContext` account. A new `TaskSchedulerConfig` is introduced to set its parameters. ## Magic program Introduces two new Magic program instructions: - `ScheduleTask` creates a new task that will be executed periodically. This instruction can only be called via CPI. When scheduling the task, all accounts used by the task must be signers of the transaction or owned by the program that is calling it. This is to ensure that the scheduled instructions are valid, as we do not verify the signatures of scheduled tasks. - `CancelTask` cancels an existing task.
1 parent e17d745 commit 882f208

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3760
-38
lines changed

Cargo.lock

Lines changed: 32 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ members = [
3232
"magicblock-rpc",
3333
"magicblock-rpc-client",
3434
"magicblock-table-mania",
35+
"magicblock-task-scheduler",
3536
"magicblock-tokens",
3637
"magicblock-transaction-status",
3738
"magicblock-validator",
@@ -69,6 +70,7 @@ bs58 = "0.4.0"
6970
byteorder = "1.5.0"
7071
cargo-expand = "1"
7172
cargo-lock = "10.0.0"
73+
chrono = "0.4"
7274
clap = "4.5.40"
7375
conjunto-transwise = { git = "https://github.com/magicblock-labs/conjunto.git", rev = "bf82b45" }
7476
console-subscriber = "0.2.0"
@@ -122,7 +124,9 @@ magicblock-config = { path = "./magicblock-config" }
122124
magicblock-config-helpers = { path = "./magicblock-config-helpers" }
123125
magicblock-config-macro = { path = "./magicblock-config-macro" }
124126
magicblock-core = { path = "./magicblock-core" }
125-
magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "5fb8d20", features = ["no-entrypoint"] }
127+
magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "5fb8d20", features = [
128+
"no-entrypoint",
129+
] }
126130
magicblock-geyser-plugin = { path = "./magicblock-geyser-plugin" }
127131
magicblock-ledger = { path = "./magicblock-ledger" }
128132
magicblock-metrics = { path = "./magicblock-metrics" }
@@ -135,6 +139,7 @@ magicblock-pubsub = { path = "./magicblock-pubsub" }
135139
magicblock-rpc = { path = "./magicblock-rpc" }
136140
magicblock-rpc-client = { path = "./magicblock-rpc-client" }
137141
magicblock-table-mania = { path = "./magicblock-table-mania" }
142+
magicblock-task-scheduler = { path = "./magicblock-task-scheduler" }
138143
magicblock-tokens = { path = "./magicblock-tokens" }
139144
magicblock-transaction-status = { path = "./magicblock-transaction-status" }
140145
magicblock-validator-admin = { path = "./magicblock-validator-admin" }
@@ -152,7 +157,7 @@ protobuf-src = "1.1"
152157
quote = "1.0"
153158
rand = "0.8.5"
154159
rayon = "1.10.0"
155-
rusqlite = { version = "0.34.0", features = ["bundled"] } # bundled sqlite 3.44
160+
rusqlite = { version = "0.37.0", features = ["bundled"] } # bundled sqlite 3.44
156161
rustc_version = "0.4"
157162
semver = "1.0.22"
158163
serde = "1.0.217"

docs/task-scheduler.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Task Scheduler API
2+
3+
## Architecture
4+
5+
### Components
6+
7+
1. **TaskContext Account**: Stores tasks and cancellation requests on-chain
8+
2. **Task Scheduler Service**: Runs alongside the validator to execute scheduled tasks
9+
3. **Database**: SQLite database for efficient task storage and retrieval
10+
4. **Geyser Integration**: Monitors TaskContext account changes
11+
12+
### Data Flow
13+
14+
1. User schedules task via program instruction
15+
2. Task is stored in TaskContext account
16+
3. Task Scheduler Service monitors TaskContext periodically
17+
4. Service adds task to local database
18+
5. Service executes tasks at scheduled intervals
19+
6. Service updates task state after execution
20+
21+
## Configuration
22+
23+
The task scheduler can be configured via the validator configuration:
24+
25+
```toml
26+
[task_scheduler]
27+
reset = false
28+
millis_per_tick = 100
29+
```
30+
31+
## Security Considerations
32+
33+
- Only task authorities can cancel their own tasks
34+
- Database is protected by file system permissions
35+
36+
## Performance Considerations
37+
38+
- Database is indexed for efficient task retrieval
39+
- Tasks are executed in batches to minimize overhead
40+
- Failed task executions are logged but don't block other tasks
41+
- Database operations are optimized for high-frequency access

magicblock-account-cloner/src/account_cloner.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ pub fn standard_blacklisted_accounts(
213213
blacklisted_accounts.insert(NATIVE_SOL_ID);
214214
blacklisted_accounts.insert(magicblock_program::ID);
215215
blacklisted_accounts.insert(magicblock_program::MAGIC_CONTEXT_PUBKEY);
216+
blacklisted_accounts.insert(magicblock_program::TASK_CONTEXT_PUBKEY);
216217
blacklisted_accounts.insert(*validator_id);
217218
blacklisted_accounts.insert(*faucet_id);
218219
blacklisted_accounts

magicblock-accounts-db/src/storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ impl AccountsStorage {
141141
// https://github.com/magicblock-labs/magicblock-validator/issues/334
142142
assert!(
143143
head.load(Relaxed) < self.meta.total_blocks as u64,
144-
"database is full"
144+
"database is full",
145145
);
146146

147147
// SAFETY:

magicblock-accounts/src/external_accounts_manager.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ use magicblock_committor_service::{
2929
transactions::MAX_PROCESS_PER_TX,
3030
types::{ScheduledBaseIntentWrapper, TriggerType},
3131
};
32-
use magicblock_magic_program_api::{self, MAGIC_CONTEXT_PUBKEY};
32+
use magicblock_magic_program_api::{
33+
self, MAGIC_CONTEXT_PUBKEY, TASK_CONTEXT_PUBKEY,
34+
};
3335
use magicblock_program::{
3436
magic_scheduled_base_intent::{
3537
CommitType, CommittedAccount, MagicBaseIntent, ScheduledBaseIntent,
@@ -445,7 +447,7 @@ where
445447
}
446448

447449
fn should_clone_account(pubkey: &Pubkey) -> bool {
448-
pubkey != &MAGIC_CONTEXT_PUBKEY
450+
pubkey != &MAGIC_CONTEXT_PUBKEY && pubkey != &TASK_CONTEXT_PUBKEY
449451
}
450452

451453
/// Creates deterministic hashes from account lamports, owner and data

magicblock-api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ magicblock-pubsub = { workspace = true }
3838
magicblock-rpc = { workspace = true }
3939
magicblock-transaction-status = { workspace = true }
4040
magicblock-validator-admin = { workspace = true }
41+
magicblock-task-scheduler = { workspace = true }
4142
magic-domain-program = { workspace = true }
4243
solana-geyser-plugin-interface = { workspace = true }
4344
solana-rpc-client = { workspace = true }

magicblock-api/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,12 @@ pub enum ApiError {
9090
#[error("Accounts Database couldn't be initialized"
9191
)]
9292
AccountsDbError(#[from] AccountsDbError),
93+
94+
#[error("TaskSchedulerServiceError")]
95+
TaskSchedulerServiceError(
96+
#[from] magicblock_task_scheduler::TaskSchedulerError,
97+
),
98+
99+
#[error("Failed to sanitize transaction: {0}")]
100+
FailedToSanitizeTransaction(#[from] solana_sdk::transaction::TransactionError),
93101
}

magicblock-api/src/fund_account.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use std::path::Path;
22

33
use magicblock_bank::bank::Bank;
44
use magicblock_magic_program_api::{
5-
self, MAGIC_CONTEXT_PUBKEY, MAGIC_CONTEXT_SIZE,
5+
self, MAGIC_CONTEXT_PUBKEY, TASK_CONTEXT_PUBKEY,
66
};
7+
use magicblock_program::{MagicContext, TaskContext};
78
use solana_sdk::{
89
account::{Account, WritableAccount},
910
clock::Epoch,
@@ -77,6 +78,15 @@ pub(crate) fn fund_magic_context(bank: &Bank) {
7778
bank,
7879
&MAGIC_CONTEXT_PUBKEY,
7980
u64::MAX,
80-
vec![0; MAGIC_CONTEXT_SIZE],
81+
MagicContext::ZERO.to_vec(),
82+
);
83+
}
84+
85+
pub(crate) fn fund_task_context(bank: &Bank) {
86+
fund_account_with_data(
87+
bank,
88+
&TASK_CONTEXT_PUBKEY,
89+
u64::MAX,
90+
TaskContext::ZERO.to_vec(),
8191
);
8292
}

magicblock-api/src/magic_validator.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ use magicblock_metrics::MetricsService;
5656
use magicblock_perf_service::SamplePerformanceService;
5757
use magicblock_processor::execute_transaction::TRANSACTION_INDEX_LOCK;
5858
use magicblock_program::{
59-
init_persister, validator, validator::validator_authority,
59+
init_persister,
60+
validator::{self, validator_authority},
6061
TransactionScheduler,
6162
};
6263
use magicblock_pubsub::pubsub_service::{
@@ -65,6 +66,7 @@ use magicblock_pubsub::pubsub_service::{
6566
use magicblock_rpc::{
6667
json_rpc_request_processor::JsonRpcConfig, json_rpc_service::JsonRpcService,
6768
};
69+
use magicblock_task_scheduler::{SchedulerDatabase, TaskSchedulerService};
6870
use magicblock_transaction_status::{
6971
TransactionStatusMessage, TransactionStatusSender,
7072
};
@@ -96,7 +98,8 @@ use crate::{
9698
errors::{ApiError, ApiResult},
9799
external_config::{cluster_from_remote, try_convert_accounts_config},
98100
fund_account::{
99-
fund_magic_context, fund_validator_identity, funded_faucet,
101+
fund_magic_context, fund_task_context, fund_validator_identity,
102+
funded_faucet,
100103
},
101104
geyser_transaction_notify_listener::GeyserTransactionNotifyListener,
102105
init_geyser_service::{init_geyser_service, InitGeyserServiceConfig},
@@ -175,6 +178,7 @@ pub struct MagicValidator {
175178
pubsub_config: PubsubConfig,
176179
pub transaction_status_sender: TransactionStatusSender,
177180
claim_fees_task: ClaimFeesTask,
181+
task_scheduler_handle: Option<tokio::task::JoinHandle<()>>,
178182
}
179183

180184
impl MagicValidator {
@@ -247,6 +251,7 @@ impl MagicValidator {
247251

248252
fund_validator_identity(&bank, &validator_pubkey);
249253
fund_magic_context(&bank);
254+
fund_task_context(&bank);
250255
let faucet_keypair =
251256
funded_faucet(&bank, ledger.ledger_path().as_path())?;
252257

@@ -444,6 +449,7 @@ impl MagicValidator {
444449
transaction_listener,
445450
transaction_status_sender,
446451
claim_fees_task: ClaimFeesTask::new(),
452+
task_scheduler_handle: None,
447453
})
448454
}
449455

@@ -788,6 +794,42 @@ impl MagicValidator {
788794
self.pubsub_handle.write().unwrap().replace(pubsub_handle);
789795
self.pubsub_close_handle = pubsub_close_handle;
790796

797+
let task_scheduler_db_path =
798+
SchedulerDatabase::path(self.ledger.ledger_path().parent().expect(
799+
"ledger_path didn't have a parent, should never happen",
800+
));
801+
debug!(
802+
"Task scheduler persists to: {}",
803+
task_scheduler_db_path.display()
804+
);
805+
let task_scheduler_handle = TaskSchedulerService::start(
806+
&task_scheduler_db_path,
807+
&self.config.task_scheduler,
808+
self.bank.clone(),
809+
self.token.clone(),
810+
)?;
811+
// TODO: we should shutdown gracefully.
812+
// This is discussed in this comment:
813+
// https://github.com/magicblock-labs/magicblock-validator/pull/493#discussion_r2324560798
814+
// However there is no proper solution for this right now.
815+
// An issue to create a shutdown system is open here:
816+
// https://github.com/magicblock-labs/magicblock-validator/issues/524
817+
self.task_scheduler_handle = Some(tokio::spawn(async move {
818+
match task_scheduler_handle.await {
819+
Ok(Ok(())) => {}
820+
Ok(Err(err)) => {
821+
error!("An error occurred while running the task scheduler: {:?}", err);
822+
error!("Exiting process...");
823+
std::process::exit(1);
824+
}
825+
Err(err) => {
826+
error!("Failed to start task scheduler: {:?}", err);
827+
error!("Exiting process...");
828+
std::process::exit(1);
829+
}
830+
}
831+
}));
832+
791833
self.sample_performance_service
792834
.replace(SamplePerformanceService::new(
793835
&self.bank,

0 commit comments

Comments
 (0)