A scheduler is your function on a timer, written in any language of API Maker. It runs every few seconds, every night at two, on the first of the month, or on any cron expression, in the time zone you pick. On a cluster of servers, each run happens once, on one server, and moves to another server when one goes down. Or it runs on every server at the same moment, and the runs share the work.
In short
A scheduler is your code on a timer : every minute, every night at two, or any cron.
Each run happens once, on one server, even when you have many servers.
Its time zone, its time limit and its next runs are on its page.
Its name, how long it may run, and whether it is on.
Unique in the account. It names the folder in Git ; a rename is followed by the log profiles.
Timeout in minutes
The run is stopped after this long, in the sandbox and on the native process alike. Empty : 10 minutes.
Runs on
One server (each run happens once, on one server of the installation) or Every server, then Once per server or Once per runner : see Run on every server.
Code
The function, with the global object g.
Intervals
One or more, each with a time interval, a time zone and an active switch.
Active
The master switch of the scheduler.
Run Native
Run on the native process of the server, not in a sandbox.
import*asTfrom'types';asyncfunctionmain(g:T.IAMGlobal){constbefore=Date.now()-7*86400000;// a week ago, in milliseconds since 1970 : what updated_at holdsconstremoved=awaitg.sys.db.removeByQuery({instance:'mongodb',database:'shop',collection:'carts',find:{updated_at:{$lt:before}},});g.logger.log(`${removed.deletedRowsCount} carts removed`);return{removed:removed.deletedRowsCount};}module.exports=main;
importtimefromamimportGasyncdefmain(g:G):before=int(time.time()*1000)-7*86400000# a week ago, in milliseconds since 1970 : what updated_at holdsremoved=awaitg.sys.db.remove_by_query('mongodb','shop','carts',find={'updated_at':{'$lt':before}})g.logger.log(f'{removed.deleted_rows_count} carts removed')return{'removed':removed.deleted_rows_count}
<?phpfunctionmain(AmG$g){$before=(int)(microtime(true)*1000)-7*86400000;// a week ago, in milliseconds since 1970 : what updated_at holds$removed=$g->sys->db->removeByQuery('mongodb','shop','carts',find:['updated_at'=>['$lt'=>$before]]);$g->logger->log("{$removed->deletedRowsCount} carts removed");return['removed'=>$removed->deletedRowsCount];}
filestaticclassApi{publicstaticasyncTask<object?>Main(Gg){varbefore=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()-7*86400000L;// a week ago, in milliseconds since 1970 : what updated_at holdsvarremoved=awaitg.Sys.Db.RemoveByQuery(newRemoveByQuery("mongodb","shop","carts"){Find=newJsonObject{["updated_at"]=newJsonObject{["$lt"]=before}},});g.Logger.Log($"{removed.DeletedRowsCount} carts removed");returnnew{removed=removed.DeletedRowsCount};}}
packagecodeimport("fmt""time""apimaker/am")funcMain(g*am.G)(any,error){before:=time.Now().UnixMilli()-7*86400000// a week ago, in milliseconds since 1970 : what updated_at holdsremoved,err:=g.Sys.Db.RemoveByQuery(am.RemoveByQuery{Instance:"mongodb",Database:"shop",Collection:"carts",Find:am.M{"updated_at":am.M{"$lt":before}},})iferr!=nil{returnnil,err}g.Logger.Log(fmt.Sprintf("%d carts removed",removed.DeletedRowsCount))returnam.M{"removed":removed.DeletedRowsCount},nil}
usestd::time::{SystemTime,UNIX_EPOCH};useam::{Errors,G,RemoveByQuery,Value,json};pubfnmain(g:&mutG)->Result<Value,Errors>{letnow=SystemTime::now().duration_since(UNIX_EPOCH).map_or(0,|since|since.as_millis()asi64);letbefore=now-7*86_400_000;// a week ago, in milliseconds since 1970 : what updated_at holdsletremoved=g.sys.db.remove_by_query(RemoveByQuery::new("mongodb","shop","carts").find(json!({"updated_at":{"$lt":before}})))?;g.logger.log(format!("{} carts removed",removed.deleted_rows_count));Ok(json!({"removed":removed.deleted_rows_count}))}
importam.G;importam.RemoveByQuery;importam.RemoveByQueryResult;importjava.util.Map;classApi{staticObjectmain(Gg){longbefore=System.currentTimeMillis()-7*86400000L;// a week ago, in milliseconds since 1970 : what updated_at holdsRemoveByQueryResultremoved=g.sys.db.removeByQuery(newRemoveByQuery("mongodb","shop","carts").find(Map.of("updated_at",Map.of("$lt",before))));g.logger.log(removed.getDeletedRowsCount()+" carts removed");returnMap.of("removed",removed.getDeletedRowsCount());}}
importam.Gimportam.RemoveByQueryfunmain(g:G):Any?{valbefore=System.currentTimeMillis()-7*86400000L// a week ago, in milliseconds since 1970 : what updated_at holdsvalremoved=g.sys.db.removeByQuery(RemoveByQuery("mongodb","shop","carts").find(mapOf("updated_at"tomapOf("\$lt"tobefore))))g.logger.log("${removed.deletedRowsCount} carts removed")returnmapOf("removed"toremoved.deletedRowsCount)}
The same g as a custom API, without a request : g.req.body and g.req.query are empty. Keep state between runs in the cache : the next run may be on another server.
A scheduler has one or more intervals. Each interval has its time, its time zone and its own active switch. A scheduler with two intervals runs at the moments of both.
Six fields, seconds first : second minute hour day-of-month month day-of-week. Five fields work too (the seconds default to 0).
Field
Values
second, minute
0-59
hour
0-23
day of the month
1-31
month
1-12, or jan-dec
day of the week
0-7 (0 and 7 are Sunday), or sun-sat
Each field takes *, a value, a range 1-5, a list 1,15 and a step */10. The presets @yearly, @monthly, @weekly, @daily, @hourly, @minutely, @secondly, @weekdays and @weekends stand for a whole expression. The panel checks an expression as you type it, by the same rules.
Once per tick. Whatever the number of API Maker servers, an interval runs once : each scheduler belongs to one server, which holds it in Redis, and a scheduler already running is not started again.
Spread out. Every server knows the servers which are up (each one says so every few seconds), and the schedulers are spread over them evenly. A server which joins takes its share; the others keep theirs.
At once. A server starts its schedulers as soon as it starts.
Failover. When a server stops cleanly, it gives its schedulers back and the others take them at their next look, within five seconds ; when it dies, within about twenty seconds, when its hold runs out.
Which server. The Server column of the Schedulers page shows the server running each scheduler.
Safe deployments. A Git pull or a deployment does not interrupt a run in progress ; the next run uses the new code.
Runs on : Every server runs the scheduler on every server of the installation at each moment of its intervals, for work every server does for itself (its own files, its own cache) or work the servers share. Runs per server says how many runs each server starts :
Runs per server
At each moment
Once per server
One run on each server.
Once per runner
One run in each runner of the sandbox of the language of the code, on each server : as many runs as the server has processor cores (the runners a sandbox opens). The most work done at once. A runner which is not open yet opens, and runs the process initializers first. With Run Native : one run per server.
Every server starts the scheduler within a few seconds of a save, and a server which joins starts it too. The Server column of the Schedulers page and the Schedule panel of the form list the servers which run it ; each run on the panel says its server, and its runner.
Each run is told where it is among the runs of its moment, in g.req.scheduler :
Key
Meaning
worker, workers
This run among all the runs of the moment, from 0, and how many there are, on all the servers. The servers number the runs the same way, server after server in the order of their ids.
firedAt
The moment the runs were started for, in milliseconds since 1970 : the same in every run of that moment, on every server.
serverIndex, servers
This server among the servers which run the scheduler, and how many they are.
runnerIndex, runners
The runner of the run on its server, and how many runs this server started.
serverId, serverName, name, runOn
The server, the scheduler, and where it runs.
Share the work between the runs of a moment : each run takes its share by worker, and the lock of each item before it works on it, so an item a run of an earlier moment still works on is left alone.
Each run of the moment packs its share of the orders
import*asTfrom'types';asyncfunctionmain(g:T.IAMGlobal){const{worker,workers}=g.req.scheduler;constorders=awaitg.sys.db.query({instance:'mysql_8',database:'shop',collection:'orders',find:{status:'paid'},limit:500});letpacked=0;for(constorderoforders){// Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.if(order.id%workers!==worker)continue;// A run of an earlier moment may still work on an order : it holds its lock.constlock=awaitg.sys.system.getLock({name:`order-${order.id}`,ttlSeconds:120});if(!lock.acquired)continue;try{awaitg.sys.db.updateById({instance:'mysql_8',database:'shop',collection:'orders',id:order.id,updateData:{status:'packed'}});packed++;}finally{awaitg.sys.system.releaseLock(lock);}}return{worker,packed};}module.exports=main;
Each run of the moment packs its share of the orders
fromamimportGasyncdefmain(g:G):worker,workers=g.req.scheduler.worker,g.req.scheduler.workersorders=awaitg.sys.db.query('mysql_8','shop','orders',find={'status':'paid'},limit=500)packed=0fororderinorders:# Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.iforder.id%workers!=worker:continue# A run of an earlier moment may still work on an order : it holds its lock.lock=awaitg.sys.system.get_lock(f'order-{order.id}',ttl_seconds=120)ifnotlock.acquired:continuetry:awaitg.sys.db.update_by_id('mysql_8','shop','orders',order.id,update_data={'status':'packed'})packed+=1finally:awaitg.sys.system.release_lock(lock.token)return{'worker':worker,'packed':packed}
Each run of the moment packs its share of the orders
<?phpfunctionmain(AmG$g){$worker=$g->req->scheduler->worker;$workers=$g->req->scheduler->workers;$orders=$g->sys->db->query('mysql_8','shop','orders',find:['status'=>'paid'],limit:500);$packed=0;foreach($ordersas$order){// Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.if($order->id%$workers!==$worker){continue;}// A run of an earlier moment may still work on an order : it holds its lock.$lock=$g->sys->system->getLock("order-{$order->id}",ttlSeconds:120);if(!$lock->acquired){continue;}try{$g->sys->db->updateById('mysql_8','shop','orders',$order->id,updateData:['status'=>'packed']);$packed++;}finally{$g->sys->system->releaseLock($lock->token);}}return['worker'=>$worker,'packed'=>$packed];}
Each run of the moment packs its share of the orders
filestaticclassApi{publicstaticasyncTask<object?>Main(Gg){varworker=g.Req.Scheduler.GetInt64("worker");varworkers=g.Req.Scheduler.GetInt64("workers");varorders=awaitg.Sys.Db.Query(newQuery("mysql_8","shop","orders"){Find=newJsonObject{["status"]="paid"},Limit=500});varpacked=0;foreach(varorderinorders){varid=order.GetInt64("id");// Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.if(id%workers!=worker)continue;// A run of an earlier moment may still work on an order : it holds its lock.varheld=awaitg.Sys.System.GetLock(newGetLock($"order-{id}"){TtlSeconds=120});if(!held.Acquired)continue;try{awaitg.Sys.Db.UpdateById(newUpdateById("mysql_8","shop","orders",id){UpdateData=newJsonObject{["status"]="packed"}});packed++;}finally{awaitg.Sys.System.ReleaseLock(held.Token!);}}returnnew{worker,packed};}}
Each run of the moment packs its share of the orders
packagecodeimport("fmt""apimaker/am")funcMain(g*am.G)(any,error){worker,workers:=g.Req.Scheduler().Int("worker"),g.Req.Scheduler().Int("workers")orders,err:=g.Sys.Db.Query(am.Query{Instance:"mysql_8",Database:"shop",Collection:"orders",Find:am.M{"status":"paid"},Limit:500})iferr!=nil{returnnil,err}packed:=0for_,order:=rangeorders{id:=order.Int("id")// Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.ifid%workers!=worker{continue}// A run of an earlier moment may still work on an order : it holds its lock.lock,err:=g.Sys.System.GetLock(am.GetLock{Name:fmt.Sprintf("order-%d",id),TtlSeconds:120})iferr!=nil{returnnil,err}if!lock.Acquired{continue}_,err=g.Sys.Db.UpdateById(am.UpdateById{Instance:"mysql_8",Database:"shop",Collection:"orders",Id:id,UpdateData:am.M{"status":"packed"}})g.Sys.System.ReleaseLock(lock.Token)iferr!=nil{returnnil,err}packed++}returnam.M{"worker":worker,"packed":packed},nil}
Each run of the moment packs its share of the orders
useam::{Errors,G,GetLock,Query,UpdateById,Value,json};pubfnmain(g:&mutG)->Result<Value,Errors>{letworker=g.req.scheduler()["worker"].as_i64().unwrap_or_default();letworkers=g.req.scheduler()["workers"].as_i64().unwrap_or(1);letorders=g.sys.db.query(Query::new("mysql_8","shop","orders").find(json!({"status":"paid"})).limit(500))?;letmutpacked=0;fororderin&orders{letid=order["id"].as_i64().unwrap_or_default();// Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.ifid%workers!=worker{continue;}// A run of an earlier moment may still work on an order : it holds its lock.letlock=g.sys.system.get_lock(GetLock::new(format!("order-{id}")).ttl_seconds(120.0))?;if!lock.acquired{continue;}letupdated=g.sys.db.update_by_id(UpdateById::new("mysql_8","shop","orders",id).update_data(json!({"status":"packed"})));g.sys.system.release_lock(&lock.token)?;updated?;packed+=1;}Ok(json!({"worker":worker,"packed":packed}))}
Each run of the moment packs its share of the orders
importam.G;importam.GetLock;importam.JsonObject;importam.Lock;importam.Query;importam.UpdateById;importjava.util.List;importjava.util.Map;classApi{staticObjectmain(Gg){longworker=g.req.getScheduler().getLong("worker");longworkers=g.req.getScheduler().getLong("workers");List<JsonObject>orders=g.sys.db.query(newQuery("mysql_8","shop","orders").find(Map.of("status","paid")).limit(500));intpacked=0;for(JsonObjectorder:orders){longid=order.getLong("id");// Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.if(id%workers!=worker)continue;// A run of an earlier moment may still work on an order : it holds its lock.Locklock=g.sys.system.getLock(newGetLock("order-"+id).ttlSeconds(120));if(!lock.isAcquired())continue;try{g.sys.db.updateById(newUpdateById("mysql_8","shop","orders",id).updateData(Map.of("status","packed")));packed++;}finally{g.sys.system.releaseLock(lock.getToken());}}returnMap.of("worker",worker,"packed",packed);}}
Each run of the moment packs its share of the orders
importam.Gimportam.GetLockimportam.Queryimportam.UpdateByIdfunmain(g:G):Any?{valworker=g.req.scheduler.getLong("worker")valworkers=g.req.scheduler.getLong("workers")valorders=g.sys.db.query(Query("mysql_8","shop","orders").find(mapOf("status"to"paid")).limit(500))varpacked=0for(orderinorders){valid=order.getLong("id")// Every run of the moment takes its share : the orders whose number, divided by the runs, leaves its own.if(id%workers!=worker)continue// A run of an earlier moment may still work on an order : it holds its lock.vallock=g.sys.system.getLock(GetLock("order-$id").ttlSeconds(120.0))if(!lock.isAcquired)continuetry{g.sys.db.updateById(UpdateById("mysql_8","shop","orders",id).updateData(mapOf("status"to"packed")))packed++}finally{g.sys.system.releaseLock(lock.token)}}returnmapOf("worker"toworker,"packed"topacked)}
Once per moment, on all the servers : what must happen once (a summary, a mail), a run takes a lock named after the moment, report-${g.req.scheduler.firedAt}, with ttlSeconds longer than the work and no release : one run of all the servers gets it.
Overlapping moments : a moment does not wait for the runs of the one before. A run which may outlast the interval takes locks for its items, as above.
Without locks : worker and workers alone give each run its own items, as long as every run ends before the next moment.
A log profile records every run with its duration, and what it returned when Log Response is ticked for the scheduler.
Schedulers are files in Git (src/schedulers/<name>/) and deploy with a pull. Their active state and intervals travel with them.
A scheduler which needs more than its timeout should split the work : page through the rows and keep a cursor in the cache. Or run it on every server, in every runner, and share the rows between the runs.
No group opens a scheduler to an API user : a scheduler runs on its timer, or with Run now in the admin panel.