also adds a "noSleep" option to the server command to avoid the 5 second sleep we introduced on startup for non-coordinator cluster nodes. The sleep doesn't seem to be needed in the tests and makes them much slower.
6.5 KiB
Initial Transaction Support
This is not full-featured transaction support with commit and rollback for now; this is a placeholder intended to allow us to solve shorter-term problems.
The primary purpose of this is to allow an exclusive transaction to block new ingest activity from starting, while permitting existing ingest operations to complete, even if a single ingest requires multiple operations. This allows users with cooperating ingest operations to ensure a stable state for the data on disk before triggering snapshots or other writes.
Overview: What transactions are
A transaction reflects an ongoing set of related operations that may be occurring in multiple or distinct messages. There is no support for rolling back a failed transaction. Transactions can coexist, and there's nothing controlling simultaneous access to fields.
However, a transaction can be exclusive. An exclusive transaction cannot start until other transactions complete, but no non-exclusive transaction can start while an exclusive transaction is waiting.
Transactions are holder-wide, not index-specific. Transactions are also presumably cluster-wide.
API Details
The base transaction endpoints are /transactions, for listing or creating
transactions, and /transaction/[id], for listing, creating, finishing, or
cancelling a transaction.
A POST to /transactions attempts to create a transaction, assigning it an
arbitrary ID that is not the ID of any existing transaction. A GET from
/transactions lists existing transactions.
A POST to /transaction/[id] tries to create a transaction with the given
ID, failing if it can't for any reason, including the reason "this ID is
already in use". A GET from /transaction/[id] retrieves information about
the transaction.
When creating a transaction, you may specify an options object:
```
{
"exclusive": true, // default is false
"timeout": 300 // in seconds, default is 300
}
```
For an exclusive transaction, you may also specify the optional parameter
"pause-snapshots" as a boolean. A true value indicates that the snapshot
queue should be paused once this transaction becomes active. Note that pausing
the snapshot queue can cause some write operations to block indefinitely.
If a transaction requests that the snapshot queue be paused, it will not
report itself "active" until the snapshot queue has completed any outstanding
snapshots and paused itself. The full sequence of events, then, is:
- Stop allowing new transactions to start.
- Wait for transactions to complete.
- Pause snapshot queue.
- Wait for snapshot queue to report that it's successfully paused.
- Transition to active state.
Exclusive transactions which pause the snapshot queue should not write to the database; this is used as a way to block activity so backups can be made.
When requesting information about a transaction, you get back an object:
```
{
"active": true,
"timeout": 300, // timeout time in seconds
"stats": {
"idle": 0, // time in seconds since last activity
"queries": 3, // queries submitted in this transaction
"errors": 0 // errors produced by queries
}
}
```
To mark a transaction as complete, you POST to /transaction/[id]/finish, and
get back the same information you'd have gotten from a GET for that transaction.
The finish request may block if any existing queries are running as part of
that transaction, but immediately prevents any new queries from starting for
that transaction.
Queries can be associated with a transaction by including
X-Pilosa-Transaction: [id] in their request headers. A transaction's idle
timer is reset by any query against it, even a query which doesn't write
anything.
When an exclusive transaction is created, it does not necessarily start out
in the active state. It immediately blocks the starting of new non-exclusive
transactions, but does not transiction to an active state until existing
transactions complete. During this time, a GET to it should return:
```
{
"active": false,
"blocked-by": [ "id" ]
}
```
where blocked-by is a list of the IDs of any transactions blocking the transition.
If multiple exclusive transactions are requested, they become active sequentially in the order the requests came in, and the snapshot queue and other transactions are not permitted to resume until the exclusive transactions all complete.
Implementation Notes
All requests go through coordinator.
When creating a new transaction, we'll create it on every node in the cluster and persist it to disk.
Only the coordinator will accept requests to start a transaction.
Timeouts only expire when there has been no activity on a transaction for the timeout duration. Any activity on the transaction may extend the deadline (unimplemented).
When finishing a transaction, we'll finish it on the coordinator and then broadcast the finish to the cluster before returning to the client.
When getting an exclusive transaction, if the transaction is active, we'll make sure that all nodes agree before returning it.
Coordinator forwards all requests to every other node so they can stay in sync. If the coordinator doesn't hear back from a node, the request fails. The coordinator only reaches out to active nodes, so if the cluster is in DEGRADED, things can still continue.
If an node is down and comes back up it needs to synchronize its state with the coordinator (unimplemented).
There is a separate TransactionManager and TransactionStore
The store is just responsible for persisting info about transactions. The manager handles all the logic (at the node level). Logic related to cluster and remote vs local node is handled by the Server. The Holder contains the TransactionManager, and the Server contains the logic for how to handle external vs intra cluster requests (remote==true).
There is intra-cluster messaging for transactions which is handled with the new TransactionMessage and goes through the usual SendMessage/Broadcaster stuff.
There is also external API which is handled by the HTTP handler and goes through API (and is passed directly to Server). (unimplemented)
TODO
- implement api layer and cluster logic, startup, etc.
- implement HTTP layer including header/transaction ID
- implement and use persistent transaction store rather than inmem.
- update go-pilosa/gpexp to actually USE transactions
- update IDK to use updated go-pilosa
Testing TransactionManager
- there should never be more than one Exclusive transaction
- if the Exclusive transaction is active, there should be no other transactions