# Locks

> g.sys.system.getLock and g.sys.system.releaseLock in API Maker - one run at a time (or a few) does what a lock guards, on every server and in every runner, in all eight languages. Kept in Redis by the agent of each runner, freed by itself when its time is over, waited for or given up at once.

Source: https://docs.apimaker.dev/v4/docs/features/locks.html

`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](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.html#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.

- Video: [Schedulers on Every Server and Distributed Locks with Redis](https://apimaker.dev/videos/schedulers-every-server-and-locks) (6 min)

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

**TypeScript**

**Send the invoice of an order once**

```typescript
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;
```

**Python**

**Send the invoice of an order once**

```python
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)
```

**PHP**

**Send the invoice of an order once**

```php
<?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);
    }
}
```

**C#**

**Send the invoice of an order once**

```csharp
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!);
        }
    }
}
```

**Go**

**Send the invoice of an order once**

```go
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
}
```

**Rust**

**Send the invoice of an order once**

```rust
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 }))
}
```

**Java**

**Send the invoice of an order once**

```java
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());
        }
    }
}
```

**Kotlin**

**Send the invoice of an order once**

```kotlin
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.

**TypeScript**

**Three calls of the payment service at a time**

```typescript
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;
```

**Python**

**Three calls of the payment service at a time**

```python
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)
```

**PHP**

**Three calls of the payment service at a time**

```php
<?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);
    }
}
```

**C#**

**Three calls of the payment service at a time**

```csharp
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!);
        }
    }
}
```

**Go**

**Three calls of the payment service at a time**

```go
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
}
```

**Rust**

**Three calls of the payment service at a time**

```rust
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)
}
```

**Java**

**Three calls of the payment service at a time**

```java
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());
        }
    }
}
```

**Kotlin**

**Three calls of the payment service at a time**

```kotlin
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**

```typescript
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`](https://docs.apimaker.dev/v4/docs/apis-all/schedulers/user-created-schedulers-api.html#run-on-every-server) 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](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html) mocks `g.sys.system.getLock` as any other call.
- The [debugger](https://docs.apimaker.dev/v4/docs/features/debugging.html) shows the answer of `getLock` among the variables : a run which waits on a breakpoint holds its lock, until its `ttlSeconds` are over.
