6 releases
0.2.1 | Oct 4, 2018 |
---|---|
0.2.0 | Oct 4, 2018 |
0.1.3 | Oct 4, 2018 |
0.1.2 | Sep 30, 2018 |
#1617 in Database interfaces
831 downloads per month
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.
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<ConnectionPool>,
report_id: i32
) -> PleaseResult<ConnectionPoolError> {
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
~5MB
~100K SLoC