g.sys.system.getLock takes a lock, g.sys.system.releaseLock gives it back. A lock is a name of your account : while a run of your code holds it, the other runs which ask for the same name do not get it, on every server of the installation, in every sandbox and in every runner, whatever their language. Schedulers which run on every server share their work this way : each run takes the lock of an item before it works on it, and leaves the items another run holds.
The name says what the lock guards : the invoice of an order, a report, a sync with a partner. A run which does not get the lock answers at once (acquired is false) : it leaves the work to the run which holds it. Give the lock back in a finally, so a run which fails gives it back too.
import*asTfrom'types';asyncfunctionmain(g:T.IAMGlobal){constid=g.req.body.id;// One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.constlock=awaitg.sys.system.getLock({name:`invoice-${id}`,ttlSeconds:30});if(!lock.acquired)return{sent:false};try{awaitg.sys.db.updateById({instance:'mysql_8',database:'shop',collection:'orders',id,updateData:{invoice_sent:true}});return{sent:true};}finally{awaitg.sys.system.releaseLock(lock);}}module.exports=main;
fromamimportGasyncdefmain(g:G):order_id=g.req.body.id# One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.lock=awaitg.sys.system.get_lock(f'invoice-{order_id}',ttl_seconds=30)ifnotlock.acquired:return{'sent':False}try:awaitg.sys.db.update_by_id('mysql_8','shop','orders',order_id,update_data={'invoice_sent':True})return{'sent':True}finally:awaitg.sys.system.release_lock(lock.token)
<?phpfunctionmain(AmG$g){$id=$g->req->body->id;// One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.$lock=$g->sys->system->getLock("invoice-{$id}",ttlSeconds:30);if(!$lock->acquired){return['sent'=>false];}try{$g->sys->db->updateById('mysql_8','shop','orders',$id,updateData:['invoice_sent'=>true]);return['sent'=>true];}finally{$g->sys->system->releaseLock($lock->token);}}
filestaticclassApi{publicstaticasyncTask<object?>Main(Gg){varid=g.Req.Body.GetInt64("id");// One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.varheld=awaitg.Sys.System.GetLock(newGetLock($"invoice-{id}"){TtlSeconds=30});if(!held.Acquired)returnnew{sent=false};try{awaitg.Sys.Db.UpdateById(newUpdateById("mysql_8","shop","orders",id){UpdateData=newJsonObject{["invoice_sent"]=true}});returnnew{sent=true};}finally{awaitg.Sys.System.ReleaseLock(held.Token!);}}}
packagecodeimport("fmt""apimaker/am")funcMain(g*am.G)(any,error){id:=g.Req.Body().Int("id")// One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.lock,err:=g.Sys.System.GetLock(am.GetLock{Name:fmt.Sprintf("invoice-%d",id),TtlSeconds:30})iferr!=nil{returnnil,err}if!lock.Acquired{returnam.M{"sent":false},nil}deferg.Sys.System.ReleaseLock(lock.Token)_,err=g.Sys.Db.UpdateById(am.UpdateById{Instance:"mysql_8",Database:"shop",Collection:"orders",Id:id,UpdateData:am.M{"invoice_sent":true}})iferr!=nil{returnnil,err}returnam.M{"sent":true},nil}
useam::{Errors,G,GetLock,UpdateById,Value,json};pubfnmain(g:&mutG)->Result<Value,Errors>{letid=g.req.body()["id"].as_i64().unwrap_or_default();// One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.letlock=g.sys.system.get_lock(GetLock::new(format!("invoice-{id}")).ttl_seconds(30.0))?;if!lock.acquired{returnOk(json!({"sent":false}));}letupdated=g.sys.db.update_by_id(UpdateById::new("mysql_8","shop","orders",id).update_data(json!({"invoice_sent":true})));g.sys.system.release_lock(&lock.token)?;updated?;Ok(json!({"sent":true}))}
importam.G;importam.GetLock;importam.Lock;importam.UpdateById;importjava.util.Map;classApi{staticObjectmain(Gg){longid=g.req.getBody().getLong("id");// One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.Locklock=g.sys.system.getLock(newGetLock("invoice-"+id).ttlSeconds(30));if(!lock.isAcquired())returnMap.of("sent",false);try{g.sys.db.updateById(newUpdateById("mysql_8","shop","orders",id).updateData(Map.of("invoice_sent",true)));returnMap.of("sent",true);}finally{g.sys.system.releaseLock(lock.getToken());}}}
importam.Gimportam.GetLockimportam.UpdateByIdfunmain(g:G):Any?{valid=g.req.body.getLong("id")// One run at a time works on an order : a run which asks meanwhile finds the lock taken, and leaves it.vallock=g.sys.system.getLock(GetLock("invoice-$id").ttlSeconds(30.0))if(!lock.isAcquired)returnmapOf("sent"tofalse)try{g.sys.db.updateById(UpdateById("mysql_8","shop","orders",id).updateData(mapOf("invoice_sent"totrue)))returnmapOf("sent"totrue)}finally{g.sys.system.releaseLock(lock.token)}}
waitSeconds waits while others hold the lock : the call gets it as soon as a holder gives it back (Redis tells every server at once, the call does not wait for its next try), or when a hold runs out. maxHolders lets several runs hold the lock together : a service which takes three calls at a time gets three, never a fourth. throwIfNotAcquired turns a lock which was not taken into an error, so the code after the call only runs with the lock.
import*asTfrom'types';asyncfunctionmain(g:T.IAMGlobal){// The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.constlock=awaitg.sys.system.getLock({name:'payment-service',maxHolders:3,waitSeconds:10,ttlSeconds:20,throwIfNotAcquired:true});try{constresp=awaitg.sys.system.callExternalApi({url:'https://payments.example.com/charges',method:'POST',body:{order:g.req.body.id}});returnresp.body;}finally{awaitg.sys.system.releaseLock(lock);}}module.exports=main;
fromamimportGasyncdefmain(g:G):# The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.lock=awaitg.sys.system.get_lock('payment-service',max_holders=3,wait_seconds=10,ttl_seconds=20,throw_if_not_acquired=True)try:resp=awaitg.sys.system.call_external_api('https://payments.example.com/charges',method='POST',body={'order':g.req.body.id})returnresp.bodyfinally:awaitg.sys.system.release_lock(lock.token)
<?phpfunctionmain(AmG$g){// The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.$lock=$g->sys->system->getLock('payment-service',maxHolders:3,waitSeconds:10,ttlSeconds:20,throwIfNotAcquired:true);try{$resp=$g->sys->system->callExternalApi('https://payments.example.com/charges',method:'POST',body:['order'=>$g->req->body->id]);return$resp->body;}finally{$g->sys->system->releaseLock($lock->token);}}
filestaticclassApi{publicstaticasyncTask<object?>Main(Gg){// The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.varheld=awaitg.Sys.System.GetLock(newGetLock("payment-service"){MaxHolders=3,WaitSeconds=10,TtlSeconds=20,ThrowIfNotAcquired=true});try{varresp=awaitg.Sys.System.CallExternalApi(newCallExternalApi("https://payments.example.com/charges"){Method="POST",Body=newJsonObject{["order"]=g.Req.Body.GetInt64("id")},});returnresp.Body;}finally{awaitg.Sys.System.ReleaseLock(held.Token!);}}}
packagecodeimport"apimaker/am"funcMain(g*am.G)(any,error){// The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.lock,err:=g.Sys.System.GetLock(am.GetLock{Name:"payment-service",MaxHolders:3,WaitSeconds:10,TtlSeconds:20,ThrowIfNotAcquired:true})iferr!=nil{returnnil,err}deferg.Sys.System.ReleaseLock(lock.Token)resp,err:=g.Sys.System.CallExternalApi(am.CallExternalApi{Url:"https://payments.example.com/charges",Method:"POST",Body:am.M{"order":g.Req.Body().Int("id")},})iferr!=nil{returnnil,err}returnresp.Body,nil}
useam::{CallExternalApi,Errors,G,GetLock,Value,json};pubfnmain(g:&mutG)->Result<Value,Errors>{// The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.letlock=g.sys.system.get_lock(GetLock::new("payment-service").max_holders(3).wait_seconds(10.0).ttl_seconds(20.0).throw_if_not_acquired(true),)?;letorder=g.req.body()["id"].clone();letresp=g.sys.system.call_external_api(CallExternalApi::new("https://payments.example.com/charges").method("POST").body(json!({"order":order})),);g.sys.system.release_lock(&lock.token)?;Ok(resp?.body)}
importam.CallExternalApi;importam.G;importam.GetLock;importam.Lock;importjava.util.Map;classApi{staticObjectmain(Gg){// The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.Locklock=g.sys.system.getLock(newGetLock("payment-service").maxHolders(3).waitSeconds(10).ttlSeconds(20).throwIfNotAcquired(true));try{returng.sys.system.callExternalApi(newCallExternalApi("https://payments.example.com/charges").method("POST").body(Map.of("order",g.req.getBody().getLong("id")))).getBody();}finally{g.sys.system.releaseLock(lock.getToken());}}}
importam.CallExternalApiimportam.Gimportam.GetLockfunmain(g:G):Any?{// The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.vallock=g.sys.system.getLock(GetLock("payment-service").maxHolders(3).waitSeconds(10.0).ttlSeconds(20.0).throwIfNotAcquired(true))try{valresp=g.sys.system.callExternalApi(CallExternalApi("https://payments.example.com/charges").method("POST").body(mapOf("order"tog.req.body.getLong("id"))),)returnresp.body}finally{g.sys.system.releaseLock(lock.token)}}
A lock frees itself ttlSeconds after it was taken : give a time longer than the work, or keep the lock longer while the work goes on. getLock with the token of a lock your code holds keeps that hold for ttlSeconds more, with the same token, and takes nothing else.
Long work : keep the lock at every page
constlock=awaitg.sys.system.getLock({name:'nightly-export',ttlSeconds:60});for(letpage=0;lock.acquired&&page<pages;page++){awaitexportPage(page);// Sixty seconds more for the next page.awaitg.sys.system.getLock({name:'nightly-export',token:lock.token,ttlSeconds:60});}
releaseLock answers false when the hold had ended before : the time was over, and another run may have held the lock meanwhile. That says the work took longer than its ttlSeconds.
The name of the lock : any text of up to 256 characters, of your account only. Give the name alone for the defaults : getLock('orders').
ttlSeconds
60
The lock frees itself this long after it was taken, or kept longer : even when the code which holds it never gives it back. Fractions are allowed, up to 30 days.
waitSeconds
0
How long to wait while others hold it. 0 answers at once.
tries
1
How many times to ask at most, retryDelayMs apart. With waitSeconds : as many as the wait leaves room for.
retryDelayMs
100
The pause between two tries. A release wakes a waiting call at once, whatever the pause.
maxHolders
1
How many runs hold the lock at once. Give the same number in every place which asks for the name.
token
The token of a lock you hold : keeps it for ttlSeconds more.
throwIfNotAcquired
false
A lock which was not taken is an error (status 400), not an answer.
What says that your code holds it : releaseLock and a getLock which keeps the lock longer take it. Empty when the lock was not taken.
expiresAt
When the hold ends, in milliseconds since 1970. When the lock was not taken : when the first hold of the others ends.
ttlSeconds
How long the lock is held.
holders, maxHolders
How many hold it now, your code among them when it took it, and how many may.
tries, waitedMs
How many times it was asked for, and how long the call waited.
releaseLock takes the lock or its token, and answers true when your code held the lock until then. A lock which was not taken gives nothing back (false). Both take a last true (or g.sys.system.full) for the whole answer, as every call does.
A scheduler which runs on every server, or in every runner of every server, starts several runs at the same moment. g.req.scheduler says which one a run is : worker among workers, the same moment firedAt on every server. Two ways to share the work, often together :
Each run its share : the items whose number divided by workers leaves worker. No call is needed to know whose item it is.
A lock per item : a run takes the lock of an item before it works on it ; an item whose lock is taken is someone else's. A run of an earlier moment which still works on an item keeps it.
Once per moment : a lock named after the moment (report-${g.req.scheduler.firedAt}, ttlSeconds longer than the work) is taken by one run of all the servers : that run alone does what must happen once.
A lock is a small sorted set in the Redis of the installation : the tokens of its holders, each with the moment its hold ends, by the clock of Redis itself. No two servers disagree on the time, and a hold of a server which went away ends by itself.
Taking a lock, keeping it longer and giving it back are one script of Redis each : one trip, a fraction of a millisecond on the network of the servers.
The agent of each runner, in the sandbox or in the process of the server's machine, talks to Redis on its own connection : a call of getLock never goes through the server. A call which waits is woken the moment the lock is given back, on any server : Redis publishes the release to the servers which have a call waiting.
A server whose settings name no Redis is alone : it keeps its locks in its process.