Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Locks

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.

Schedulers on Every Server and Distributed Locks with Redis10 chapters and the transcript
Take getLock('name'), or getLock({ name, ttlSeconds, waitSeconds, tries, retryDelayMs, maxHolders, token, throwIfNotAcquired })
Give back releaseLock(lock), or releaseLock(lock.token)
Frees itself after ttlSeconds, 60 by default : a run which crashes never blocks the others
Waits not at all by default ; with waitSeconds it gets the lock the moment it is given back
A few at a time maxHolders: 3 : three runs hold it at once, the fourth waits
Where the Redis of your installation, reached by the agent of each runner itself : the call never goes through the server
Code only in every kind of code of your account ; no system API answers it over HTTP

Take a lock, give it back

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.

Send the invoice of an order once
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    const 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.
    const lock = await g.sys.system.getLock({ name: `invoice-${id}`, ttlSeconds: 30 });
    if (!lock.acquired) return { sent: false };
    try {
        await g.sys.db.updateById({ instance: 'mysql_8', database: 'shop', collection: 'orders', id, updateData: { invoice_sent: true } });
        return { sent: true };
    } finally {
        await g.sys.system.releaseLock(lock);
    }
}
module.exports = main;
Send the invoice of an order once
from am import G


async def main(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 = await g.sys.system.get_lock(f'invoice-{order_id}', ttl_seconds=30)
    if not lock.acquired:
        return {'sent': False}
    try:
        await g.sys.db.update_by_id('mysql_8', 'shop', 'orders', order_id, update_data={'invoice_sent': True})
        return {'sent': True}
    finally:
        await g.sys.system.release_lock(lock.token)
Send the invoice of an order once
<?php
function main(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);
    }
}
Send the invoice of an order once
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var id = 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.
        var held = await g.Sys.System.GetLock(new GetLock($"invoice-{id}") { TtlSeconds = 30 });
        if (!held.Acquired) return new { sent = false };
        try
        {
            await g.Sys.Db.UpdateById(new UpdateById("mysql_8", "shop", "orders", id) { UpdateData = new JsonObject { ["invoice_sent"] = true } });
            return new { sent = true };
        }
        finally
        {
            await g.Sys.System.ReleaseLock(held.Token!);
        }
    }
}
Send the invoice of an order once
package code

import (
    "fmt"

    "apimaker/am"
)

func Main(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})
    if err != nil {
        return nil, err
    }
    if !lock.Acquired {
        return am.M{"sent": false}, nil
    }
    defer g.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}})
    if err != nil {
        return nil, err
    }
    return am.M{"sent": true}, nil
}
Send the invoice of an order once
use am::{Errors, G, GetLock, UpdateById, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let id = 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.
    let lock = g.sys.system.get_lock(GetLock::new(format!("invoice-{id}")).ttl_seconds(30.0))?;
    if !lock.acquired {
        return Ok(json!({ "sent": false }));
    }
    let updated = 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 }))
}
Send the invoice of an order once
import am.G;
import am.GetLock;
import am.Lock;
import am.UpdateById;
import java.util.Map;

class Api {
    static Object main(G g) {
        long id = 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.
        Lock lock = g.sys.system.getLock(new GetLock("invoice-" + id).ttlSeconds(30));
        if (!lock.isAcquired()) return Map.of("sent", false);
        try {
            g.sys.db.updateById(new UpdateById("mysql_8", "shop", "orders", id).updateData(Map.of("invoice_sent", true)));
            return Map.of("sent", true);
        } finally {
            g.sys.system.releaseLock(lock.getToken());
        }
    }
}
Send the invoice of an order once
import am.G
import am.GetLock
import am.UpdateById

fun main(g: G): Any? {
    val id = 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.
    val lock = g.sys.system.getLock(GetLock("invoice-$id").ttlSeconds(30.0))
    if (!lock.isAcquired) return mapOf("sent" to false)
    try {
        g.sys.db.updateById(UpdateById("mysql_8", "shop", "orders", id).updateData(mapOf("invoice_sent" to true)))
        return mapOf("sent" to true)
    } finally {
        g.sys.system.releaseLock(lock.token)
    }
}

Wait for a turn, a few at a time

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.

Three calls of the payment service at a time
import * as T from 'types';

async function main(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.
    const lock = await g.sys.system.getLock({ name: 'payment-service', maxHolders: 3, waitSeconds: 10, ttlSeconds: 20, throwIfNotAcquired: true });
    try {
        const resp = await g.sys.system.callExternalApi({ url: 'https://payments.example.com/charges', method: 'POST', body: { order: g.req.body.id } });
        return resp.body;
    } finally {
        await g.sys.system.releaseLock(lock);
    }
}
module.exports = main;
Three calls of the payment service at a time
from am import G


async def main(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 = await g.sys.system.get_lock('payment-service', max_holders=3, wait_seconds=10, ttl_seconds=20, throw_if_not_acquired=True)
    try:
        resp = await g.sys.system.call_external_api('https://payments.example.com/charges', method='POST', body={'order': g.req.body.id})
        return resp.body
    finally:
        await g.sys.system.release_lock(lock.token)
Three calls of the payment service at a time
<?php
function main(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);
    }
}
Three calls of the payment service at a time
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // The payment service takes three calls at a time : a fourth run waits for a turn, ten seconds at most, then fails.
        var held = await g.Sys.System.GetLock(new GetLock("payment-service") { MaxHolders = 3, WaitSeconds = 10, TtlSeconds = 20, ThrowIfNotAcquired = true });
        try
        {
            var resp = await g.Sys.System.CallExternalApi(new CallExternalApi("https://payments.example.com/charges")
            {
                Method = "POST",
                Body = new JsonObject { ["order"] = g.Req.Body.GetInt64("id") },
            });
            return resp.Body;
        }
        finally
        {
            await g.Sys.System.ReleaseLock(held.Token!);
        }
    }
}
Three calls of the payment service at a time
package code

import "apimaker/am"

func Main(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})
    if err != nil {
        return nil, err
    }
    defer g.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")},
    })
    if err != nil {
        return nil, err
    }
    return resp.Body, nil
}
Three calls of the payment service at a time
use am::{CallExternalApi, Errors, G, GetLock, Value, json};

pub fn main(g: &mut G) -> 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.
    let lock = 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),
    )?;
    let order = g.req.body()["id"].clone();
    let resp = 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)
}
Three calls of the payment service at a time
import am.CallExternalApi;
import am.G;
import am.GetLock;
import am.Lock;
import java.util.Map;

class Api {
    static Object main(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 lock = g.sys.system.getLock(new GetLock("payment-service").maxHolders(3).waitSeconds(10).ttlSeconds(20).throwIfNotAcquired(true));
        try {
            return g.sys.system.callExternalApi(new CallExternalApi("https://payments.example.com/charges")
                .method("POST")
                .body(Map.of("order", g.req.getBody().getLong("id")))).getBody();
        } finally {
            g.sys.system.releaseLock(lock.getToken());
        }
    }
}
Three calls of the payment service at a time
import am.CallExternalApi
import am.G
import am.GetLock

fun main(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.
    val lock = g.sys.system.getLock(GetLock("payment-service").maxHolders(3).waitSeconds(10.0).ttlSeconds(20.0).throwIfNotAcquired(true))
    try {
        val resp = g.sys.system.callExternalApi(
            CallExternalApi("https://payments.example.com/charges").method("POST").body(mapOf("order" to g.req.body.getLong("id"))),
        )
        return resp.body
    } finally {
        g.sys.system.releaseLock(lock.token)
    }
}

Keep a lock longer

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
const lock = await g.sys.system.getLock({ name: 'nightly-export', ttlSeconds: 60 });
for (let page = 0; lock.acquired && page < pages; page++) {
    await exportPage(page);
    // Sixty seconds more for the next page.
    await g.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.

What getLock takes

Key Default Meaning
name required 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 getLock answers

Key Meaning
acquired Whether your code holds the lock now.
name The name of the lock.
token 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.

In a scheduler of every server

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.

How it works

  • 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.

Good to know

  • The names are of your account : another account has locks of the same names of its own.
  • Choose ttlSeconds longer than the work it guards ; for long work, keep the lock longer at each step.
  • A test case mocks g.sys.system.getLock as any other call.
  • The debugger shows the answer of getLock among the variables : a run which waits on a breakpoint holds its lock, until its ttlSeconds are over.