#locks #foundation

please

Foundation for implementing long-lived database locks

3 releases

0.1.2 Sep 30, 2018
0.1.1 Sep 30, 2018
0.1.0 Sep 30, 2018

#100 in Database implementations

Download history 9/week @ 2018-10-01

3 downloads per month

MIT/Apache

22KB
298 lines

Please

Crate for identifying and expiring long-running database activities. The core primitive provided by the crate is a PleaseHandle. These handles represent long-running operations, and can be used as the basis for implementing exclusive locking behaviour when a lock may be held for too long for transaction-level locking to be acceptable.

Documentation


lib.rs:

Crate for identifying and expiring long-running database activities. The core primitive provided by the crate is a PleaseHandle. These handles represent long-running operations, and can be used as the basis for implementing exclusive locking behaviour when a lock may be held for too long for transaction-level locking to be acceptable.

Setup

This crate requires that certain tables exist, and for this reason it exports its own migrations. To manage migrations of third-party crates you can install the diesel-setup-deps tool:

cargo install diesel-setup-deps
diesel setup
diesel-setup-deps

The necessary migrations will be automatically added to your diesel migrations directory, and these should be committed to version control along with your other migrations.

Usage

To begin with, you will typically have a long-running operation which you want to have exclusive access to some part of your database.

A good example might be generating a report: this operation will process large amounts of data, and then save the result to the output field in our reports table. We want the operation to have exclusive access to the output field, so that nobody else tries to generate the report whilst we are running, and we also may want to track whether a report is in progress or not.

This operation can be implemented as a single function:

fn generate_report(
    connection_pool: Arc,
    report_id: i32
) -> PleaseResult {

First we obtain a handle to represent our operation:

    let mut handle = PleaseHandle::new_with_cleanup(
        connection_pool, "generating report"
    )?;

Next we store our handle's ID in the reports table:

    handle.transaction(|conn, handle_id| {
        diesel::update(
            reports::table
                .filter(reports::id.eq(report_id))
                .filter(reports::operation_id.is_null())
        )
        .set(reports::operation_id.eq(Some(handle_id)))
        .execute(conn)
    })?;

Importantly, we fail if the operation ID is already set.

Now, we can perform whatever work is required to generate the report. If the report may take longer than the operation timout, then you can either increase the timeout, or call handle.refresh() every so often to ensure the timeout is never reached.

When we have our result, we simply save it back, and close the handle:

    handle.transaction(|conn, handle_id| {
        diesel::update(
            reports::table
                .filter(reports::id.eq(report_id))
        )
        .set(reports::output.eq(Some(result)))
        .execute(conn)
    })?;

    handle.close()?;
    Ok(())
})

The handle will be automatically closed if it is instead allowed to fall out of scope, but errors will be ignored.

Database Schema

In the above example, it is expected that the reports::operation_id column is a nullable integer column, and a foreign key into the please_ids table.

Ideally you should set up cascade rules such that when rows are deleted from the please_ids table, corresponding reports have their operation_id column set to null.

Operation Timeouts

If no activity happens on a PleaseHandle for longer than the operation timeout then the handle may expire. This will happen automatically whenever perform_cleanup or new_with_cleanup is called from another thread or another database client.

Calling the methods transaction or refresh are considered activity, and will prevent the handle from expiring, assuming it has not already expired. Both methods will fail-fast if the operation has already expired. In this case you should cancel any work you were doing as part of the operation.

The operation timeout is controlled by a database function: please_timeout(). To change the timeout, use a migration to alter this function and return a different value. It is not currently possible to change the timeout on a per-operation basis.

It is recommended to set the operation timeout to as short a time as possible, so that if your application crashes, is terminated unexpectedly, or simply loses connectivity to the database, any locks it might have held are released as soon as possible.

The operation timeout is by default set to two minutes.

Dependencies

~3.5MB
~64K SLoC