> For the complete documentation index, see [llms.txt](https://docs.drizz.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.drizz.dev/writing-tests/which-variable/lockable-pools.md).

# Lockable pools

A **lockable pool** is a dataset of equal-length lists. Each index across the lists is one row, and a running test leases one row for the length of the run.

|               |                                                                        |
| ------------- | ---------------------------------------------------------------------- |
| **Platforms** | Android · iOS                                                          |
| **A row**     | The i-th value of every column, leased as one unit                     |
| **Rule**      | Every column must be a list, and all columns the same length           |
| **Scope**     | Leases are global — honored across every plan, run and tab at once     |
| **Watch out** | Pool size caps parallelism. A 4-row pool means at most 4 tests at once |

## Prerequisites

* Drizz desktop app installed and signed in
* A dataset whose every column is a list of the same length
* A test plan, to lock the variables on

## Copy this

The dataset — `shopease_logins`, with **Lockable pool** ticked:

```yaml
phone: ["9000000001", "9000000002", "9000000003", "9000000004"]
otp:   ["123456", "123456", "123456", "123456"]
```

That is a pool of **4 rows**. Row 0 is `{phone: 9000000001, otp: 123456}`.

The test that leases from it:

```
# ShopEase — login with a leased account

OPEN_APP com.shopease.android
Wait Until 5 Seconds

Type {{phone}} in the mobile number field
Tap on Get OTP
Wait Until 2 Seconds

Type {{otp}} in the OTP field
Tap on Continue
Wait Until 5 Seconds

Validate that the home screen is visible
```

The script is identical to a normal dataset-driven test. The difference is in how the plan resolves `{{phone}}` and `{{otp}}`.

## Data that can't be shared

| Data                            | What happens without a pool                     |
| ------------------------------- | ----------------------------------------------- |
| Login credentials               | A second login can invalidate the first session |
| Phone numbers waiting on an OTP | Two runs race for the same message              |
| Accounts mid-flow               | One run's cart is the other run's mystery bug   |

Ten parallel tests against one account produce ten flaky results. A pool gives each run its own row.

## Set up a pool

1. Write every column as a list, all the same length.

   ```yaml
   phone: ["9000000001", "9000000002", "9000000003", "9000000004"]
   otp:   ["123456", "123456", "123456", "123456"]
   ```
2. Tick **Lockable pool** in the dataset editor.
3. Save the dataset.
4. Open the plan's data step and set each pooled variable's source to **lock to a pool**.
5. Run the plan and open the **Locks** panel to confirm rows are leased and released.

Save rejects a pool that isn't shaped right:

| Problem                      | Message on save                             |
| ---------------------------- | ------------------------------------------- |
| A column isn't a list        | Rejected — it names the offending variables |
| Columns of different lengths | Rejected — it lists the lengths             |
| An empty pool                | Rejected — a pool needs at least one row    |

Single-quoted inline lists (`['a', 'b']`) don't parse as a list and are caught by the not-a-list check. Use double quotes, or block `-` items.

A variable that only a pool provides is locked automatically. A variable that both a pool and a normal dataset provide is left to be chosen — nothing is selected silently. Because a row is paired, locking one column also leases the pool's other referenced columns.

## Lease behavior

<table data-search="false"><thead><tr><th>Stage</th><th>Behavior</th></tr></thead><tbody><tr><td>Acquire</td><td>The test leases the lowest free row when it starts</td></tr><tr><td>Binding</td><td>Every column in that row binds into the run. <code>{{phone}}</code> and <code>{{otp}}</code> come from the same index, always paired</td></tr><tr><td>Hold</td><td>The row is held for the whole run, by at most one run at a time</td></tr><tr><td>Release</td><td>The row is released when the run finishes — passed, failed or timed out</td></tr><tr><td>Scope</td><td>Leases are global. A plan, a second plan, and a single test run from the editor draw from one registry, so no two get the same row</td></tr><tr><td>Exhaustion</td><td>The next run waits rather than failing, with <code>pool_exhausted</code> as the wait reason. It gives up after a timeout and fails with "no free value in pool"</td></tr><tr><td>Stale recovery</td><td>Leases older than the stale threshold are reclaimed the next time anything tries to acquire a row</td></tr></tbody></table>

## Sizing

| Pool rows vs parallelism   | Result                                                                                             |
| -------------------------- | -------------------------------------------------------------------------------------------------- |
| Rows ≥ maximum parallelism | Every run leases immediately                                                                       |
| Rows < maximum parallelism | The extra runs queue. The suite still passes; it takes as long as running at the lower concurrency |
| Rows > maximum parallelism | No cost. Spare rows sit unleased                                                                   |

A plan that runs 10-wide needs a pool of 10 rows or more.

## Several pools at once

A test can lease from more than one pool — credentials from one, phone numbers from another. Lock each variable to its pool.

Rows are acquired all-or-nothing. If one pool is full, whatever was already acquired is released and the run retries, which is what stops two runs deadlocking while each holds half of what it needs.

Each pool independently caps concurrency at its own size. A 2-row credentials pool plus a 2-row phone pool, with six cases needing both, runs at most 2 at a time.

## Rules and limits

* **All columns must be the same length**, or the pool can't be locked or used at all.
* **A `SET` in the test wins** over a leased value for the same variable. A stray `SET phone = "9000000000"` silently defeats the pool.
* **Locking gives&#x20;*****different*****&#x20;values, not the same one.** Two runs that must use an identical value take a fixed value from a normal dataset instead.
* **Data-driven looping and lockable pools can't be combined** on one plan.
* **Deleting or renaming a pool a saved plan uses** stops that plan running until the plan is reopened and pools are chosen again. It never silently leases from somewhere else.
* **Two selected pools providing the same variable** is flagged as a conflict and blocks save and run. Untick all but one.

## When rows get stuck

A run that dies without releasing — a closed tab, a crashed session — leaves its row held. Two mechanisms recover it:

* Leases older than the stale threshold are reclaimed automatically the next time anything tries to acquire a row.
* The **Locks** panel has **Clear stale** and **Release all**, plus a per-row release.

Releasing a row that's still running frees it for reuse while the run continues, so two runs can then share that value. The panel marks running rows and asks for confirmation.

## Common mistakes

<table data-search="false"><thead><tr><th>What you write</th><th>What happens</th></tr></thead><tbody><tr><td><code>phone</code> as a list, <code>otp</code> as a single value</td><td>The pool can't be locked. Every column must be a list</td></tr><tr><td>Columns of different lengths</td><td>Rejected on save. Even a one-row difference blocks the pool</td></tr><tr><td><code>phone: ['9000000001']</code> — single quotes inline</td><td>Doesn't parse as a list. Use double quotes or block <code>-</code> items</td></tr><tr><td>A 4-row pool with concurrency set to 10</td><td>Six runs sit waiting. You'll see <code>pool_exhausted</code></td></tr><tr><td><code>SET phone = "9000000000"</code> in a test that leases <code>phone</code></td><td>The <code>SET</code> wins and every parallel run uses the same number</td></tr><tr><td>Expecting two runs to get the same leased row</td><td>They never will. Use a fixed value instead</td></tr><tr><td>Renaming a pool a saved plan uses</td><td>The plan won't run until you reopen it and pick pools again</td></tr></tbody></table>

## Next

* [Datasets & test data](/writing-tests/which-variable/datasets.md)
* [Test plans](/running-tests/test-plans.md)
* [Devices: local, cloud & private](/running-tests/devices.md)

***

*Last updated: 6 August 2026*
