{"id":16675865,"url":"https://github.com/mpyw/laravel-database-advisory-lock","last_synced_at":"2025-10-16T17:47:55.813Z","repository":{"id":44847959,"uuid":"512590133","full_name":"mpyw/laravel-database-advisory-lock","owner":"mpyw","description":"Advisory Locking Features for Postgres/MySQL/MariaDB on Laravel","archived":false,"fork":false,"pushed_at":"2025-03-04T04:06:56.000Z","size":115,"stargazers_count":23,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-01T23:37:07.813Z","etag":null,"topics":["advisory-locks","databases","laravel","mutex","mysql","postgresql"],"latest_commit_sha":null,"homepage":"https://github.com/mpyw/laravel-database-advisory-lock/wiki","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mpyw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-11T02:22:06.000Z","updated_at":"2025-03-04T04:06:10.000Z","dependencies_parsed_at":"2024-10-12T13:08:23.349Z","dependency_job_id":"267a273e-879d-4d91-97ed-4a904bc045b0","html_url":"https://github.com/mpyw/laravel-database-advisory-lock","commit_stats":{"total_commits":24,"total_committers":1,"mean_commits":24.0,"dds":0.0,"last_synced_commit":"d8fe3c04d732027e67265cdb6d68827ab1cbd757"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Flaravel-database-advisory-lock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Flaravel-database-advisory-lock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Flaravel-database-advisory-lock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpyw%2Flaravel-database-advisory-lock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mpyw","download_url":"https://codeload.github.com/mpyw/laravel-database-advisory-lock/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247773721,"owners_count":20993639,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["advisory-locks","databases","laravel","mutex","mysql","postgresql"],"created_at":"2024-10-12T13:08:14.182Z","updated_at":"2025-10-16T17:47:50.771Z","avatar_url":"https://github.com/mpyw.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Laravel Database Advisory Lock [![Build Status](https://github.com/mpyw/laravel-database-advisory-lock/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/mpyw/laravel-database-advisory-lock/actions) [![Coverage Status](https://coveralls.io/repos/github/mpyw/laravel-database-advisory-lock/badge.svg?branch=master)](https://coveralls.io/github/mpyw/laravel-database-advisory-lock?branch=master)\n\nAdvisory Locking Features of Postgres/MySQL/MariaDB on Laravel\n\n## Requirements\n\n| Package | Version                               | Mandatory |\n|:--------|:--------------------------------------|:---------:|\n| PHP     | \u003ccode\u003e^8.2\u003c/code\u003e                     |     ✅     |\n| Laravel | \u003ccode\u003e^11.0 \u0026#124;\u0026#124; ^12.0\u003c/code\u003e |     ✅     |\n| PHPStan | \u003ccode\u003e\u0026gt;=2.0\u003c/code\u003e                 |           |\n\n\u003e [!NOTE]\n\u003e Older versions have outdated dependency requirements. If you cannot prepare the latest environment, please refer to past releases.\n\n| RDBMS    | Version                   |\n|:---------|:--------------------------|\n| Postgres | \u003ccode\u003e\u0026gt;=9.1.14\u003c/code\u003e  |\n| MySQL    | \u003ccode\u003e\u0026gt;=5.7.5\u003c/code\u003e   |\n| MariaDB  | \u003ccode\u003e\u0026gt;=10.0.15\u003c/code\u003e |\n\n## Installing\n\n```\ncomposer require mpyw/laravel-database-advisory-lock:^4.4\n```\n\n## Basic usage\n\n\u003e [!IMPORTANT]\n\u003e The default implementation is provided by `ConnectionServiceProvider`, however, **package discovery is not available**.\n\u003e Be careful that you MUST register it in **`config/app.php`** by yourself.\n\n```php\n\u003c?php\n\nreturn [\n\n    /* ... */\n\n    'providers' =\u003e [\n        /* ... */\n\n        Mpyw\\LaravelDatabaseAdvisoryLock\\ConnectionServiceProvider::class,\n\n        /* ... */\n    ],\n\n];\n```\n\n```php\n\u003c?php\n\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Database\\ConnectionInterface;\n\n// Session-Level Locking\n$result = DB::advisoryLocker()\n    -\u003eforSession()\n    -\u003ewithLocking('\u003ckey\u003e', function (ConnectionInterface $conn) {\n        // critical section here\n        return ...;\n    }); // no wait\n$result = DB::advisoryLocker()\n    -\u003eforSession()\n    -\u003ewithLocking('\u003ckey\u003e', function (ConnectionInterface $conn) {\n        // critical section here\n        return ...;\n    }, timeout: 5); // wait for 5 seconds or fail\n$result = DB::advisoryLocker()\n    -\u003eforSession()\n    -\u003ewithLocking('\u003ckey\u003e', function (ConnectionInterface $conn) {\n        // critical section here\n        return ...;\n    }, timeout: -1); // infinite wait (except MariaDB)\n\n// Postgres only feature: Transaction-Level Locking (no wait)\n$result = DB::transaction(function (ConnectionInterface $conn) {\n    $conn-\u003eadvisoryLocker()-\u003eforTransaction()-\u003elockOrFail('\u003ckey\u003e');\n    // critical section here\n    return ...;\n});\n```\n\n## Advanced Usage\n\n\u003e [!TIP]\n\u003e You can extend Connection classes with `AdvisoryLocks` trait by yourself.\n\n```php\n\u003c?php\n\nnamespace App\\Providers;\n\nuse App\\Database\\PostgresConnection;\nuse Illuminate\\Database\\Connection;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass DatabaseServiceProvider extends ServiceProvider\n{\n    public function register(): void\n    {\n        Connection::resolverFor('pgsql', function (...$parameters) {\n            return new PostgresConnection(...$parameters);\n        });\n    }\n}\n```\n\n```php\n\u003c?php\n\nnamespace App\\Database;\n\nuse Illuminate\\Database\\PostgresConnection as BasePostgresConnection;\nuse Mpyw\\LaravelDatabaseAdvisoryLock\\AdvisoryLocks;\n\nclass PostgresConnection extends BasePostgresConnection\n{\n    use AdvisoryLocks;\n}\n```\n\n## Implementation Details\n\n### Key Hashing Algorithm\n\n```sql\n-- Postgres: int8\nhashtext('\u003ckey\u003e')\n```\n\n```sql\n-- MySQL/MariaDB: varchar(64)\nCASE WHEN CHAR_LENGTH('\u003ckey\u003e') \u003e 64\nTHEN CONCAT(SUBSTR('\u003ckey\u003e', 1, 24), SHA1('\u003ckey\u003e'))\nELSE '\u003ckey\u003e'\nEND\n```\n\n- Postgres advisory locking functions only accept integer keys. So the driver converts key strings into 64-bit integers through `hashtext()` function.\n  - An empty string can also be used as a key.\n- MySQL advisory locking function accepts string keys but their length are limited within 64 chars. When key strings exceed 64 chars limit, the driver takes first 24 chars from them and appends 40 chars `sha1()` hashes.\n  - MariaDB's limit is actually 192 bytes, unlike MySQL's 64 chars. However, the key hashing algorithm is equivalent.\n  - MariaDB accepts an empty string as a key, but does not actually lock anything. MySQL, on the other hand, raises an error for empty string keys.\n- With either hashing algorithm, collisions can theoretically occur with very low probability.\n\n### Locking Methods\n\n|                           | Postgres  | MySQL/MariaDB |\n|:--------------------------|:---------:|:-------------:|\n| Session-Level Locking     |     ✅     |       ✅       |\n| Transaction-Level Locking |     ✅     |       ❌       |\n\n- Session-Level locks can be acquired anywhere.\n  - They can be released manually or automatically through a destructor.\n  - For Postgres, there was a problem where the automatic lock release algorithm did not work properly, but this has been fixed in version 4.0.0. See [#2](https://github.com/mpyw/laravel-database-advisory-lock/pull/2) for details.\n- Transaction-Level locks can be acquired within a transaction.\n  - You do not need to and cannot manually release locks that have been acquired.\n\n### Timeout Values\n\n|                                            |    Postgres     | MySQL | MariaDB |\n|:-------------------------------------------|:---------------:|:-----:|:-------:|\n| Timeout: `0` (default; immediate, no wait) |        ✅        |   ✅   |    ✅    |\n| Timeout: `positive-int`                    |        ✅        |   ✅   |    ✅    |\n| Timeout: `negative-int` (infinite wait)    |        ✅        |   ✅   |    ❌    |\n| Timeout: `float`                           |        ✅        |   ❌   |    ❌    |\n\n- Postgres does not natively support waiting for a finite specific amount of time, but this is emulated by looping through a temporary function.\n- MariaDB does not accept infinite timeouts. very large numbers can be used instead.\n- Float precision is not supported on MySQL/MariaDB.\n\n## Caveats about Transaction Levels\n\n### Recommended Approach\n\nWhen transactions and advisory locks are related, either locking approach can be applied. \n\n\u003e [!TIP]\n\u003e **For Postgres, always prefer Transaction-Level Locking.**\n\n\u003e [!NOTE]\n\u003e **Transaction-Level Locks:**  \n\u003e Ensure the current context is \u003cins\u003einside the transaction\u003c/ins\u003e, then rely on automatic release mechanisms.\n\u003e\n\u003e ```php\n\u003e if (DB::transactionLevel() \u003c 1) {\n\u003e     throw new LogicException(\"Unexpectedly transaction is not active.\");\n\u003e }\n\u003e\n\u003e DB::advisoryLocker()\n\u003e     -\u003eforTransaction()\n\u003e     -\u003elockOrFail('\u003ckey\u003e');\n\u003e // critical section with transaction here\n\u003e ```\n\n\u003e [!NOTE]\n\u003e **Session-Level Locks:**  \n\u003e Ensure the current context is \u003cins\u003eoutside the transaction\u003c/ins\u003e, then proceed to call `DB::transaction()` call.\n\u003e\n\u003e ```php\n\u003e if (DB::transactionLevel() \u003e 0) {\n\u003e     throw new LogicException(\"Unexpectedly transaction is already active.\");\n\u003e }\n\u003e\n\u003e $result = DB::advisoryLocker()\n\u003e     -\u003eforSession()\n\u003e     -\u003ewithLocking('\u003ckey\u003e', fn (ConnectionInterface $conn) =\u003e $conn-\u003etransaction(function () {\n\u003e         // critical section with transaction here\n\u003e     }));\n\u003e ```\n\n\u003e [!WARNING]\n\u003e When writing logic like this, [`DatabaseTruncation`](https://github.com/laravel/framework/blob/87b9e7997e178dfc4acd5e22fa8d77ba333c3abd/src/Illuminate/Foundation/Testing/DatabaseTruncation.php) must be used instead of [`RefreshDatabase`](https://github.com/laravel/framework/blob/87b9e7997e178dfc4acd5e22fa8d77ba333c3abd/src/Illuminate/Foundation/Testing/RefreshDatabase.php).\n\n### Considerations\n\n\u003e [!CAUTION]\n\u003e **Session-Level Locks:**  \n\u003e Don't take session-level locks in the transactions when the content to be committed by the transaction is related to the advisory locks.\n\u003e\n\u003e What would happen if we released a session-level lock within a transaction? Let's verify this with a timeline chart, assuming a `READ COMMITTED` isolation level on Postgres. The bank account X is operated from two sessions A and B concurrently.\n\u003e\n\u003e | Session A                                                          | Session B                                                                                             |\n\u003e |:-------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------|\n\u003e | `BEGIN`                                                            |                                                                                                       |\n\u003e | ︙                                                                  | `BEGIN`                                                                                               |\n\u003e | `pg_advisory_lock(X)`                                              | ︙                                                                                                     |\n\u003e | ︙                                                                  | `pg_advisory_lock(X)`                                                                                 |\n\u003e | Fetch balance of User X\u003cbr\u003e(Balance: 1000 USD)                     | ︙                                                                                                     |\n\u003e | ︙                                                                  | ︙                                                                                                     |\n\u003e | Deduct 800 USD if balance permits\u003cbr\u003e(Balance: 1000 USD → 200 USD) | ︙                                                                                                     |\n\u003e | ︙                                                                  | ︙                                                                                                     |\n\u003e | `pg_advisory_unlock(X)`                                            | ︙                                                                                                     |\n\u003e | ︙                                                                  | Fetch balance of User X\u003cbr\u003e**(Balance: 1000 USD :heavy_exclamation_mark:)**                           |\n\u003e | ︙                                                                  | ︙                                                                                                     |\n\u003e | ︙                                                                  | Deduct 800 USD if balance permits\u003cbr\u003e**(Balance: 1000 USD → 200 USD :bangbang:)**                     |\n\u003e | `COMMIT`                                                           | ︙                                                                                                     |\n\u003e | ︙                                                                  | `pg_advisory_unlock(X)`                                                                               |\n\u003e | Fetch balance of User X\u003cbr\u003e(Balance: 200 USD)                      | ︙                                                                                                     |\n\u003e |                                                                    | `COMMIT`                                                                                              |\n\u003e |                                                                    | ︙                                                                                                     |\n\u003e |                                                                    | Fetch balance of User X\u003cbr\u003e(**Balance: \u003cins\u003e-600 USD\u003c/ins\u003e** :interrobang::interrobang::interrobang:) |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpyw%2Flaravel-database-advisory-lock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmpyw%2Flaravel-database-advisory-lock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpyw%2Flaravel-database-advisory-lock/lists"}