You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Proposal: multi-request transaction support for Fuseki, built on TDB2
I've hoped for this for a long time, now it might be within grasp.
Yes, AI wrote this. I haven't dug into the Jena/Fuseki code, but I did review the proposal (and made minor edits).
Motivation
SPARQL 1.1/1.2 Protocol only defines atomic, single-shot query and update operations, there's no notion of a transaction spanning more than one HTTP request. This has come up repeatedly outside this project too: w3c/sparql-dev#83 is a long-running discussion of exactly this gap, and @afs already sketched an approach there:
An alternative is an addition query string parameter and use the usual endpoint for query/update etc. Given that security may apply on some endpoints based on URL (e.g. different URLs for query and update), using the same endpoints for operations with transactions and single operations would be helpful. It is also helpful to client libraries as the sequence "begin"-any existing code-"commit" works. /transactions/<id> is for transaction control.
This issue proposes building that out for Fuseki, on top of TDB2's existing internal ACID transactions (Transactional/TxnType, Serializable isolation). The TDB2 storage engine already does the hard part, what's missing is a way for a client to hold one of those transactions open across several HTTP requests.
Other triple stores have shipped support: RDF4J's REST API has a dedicated transaction resource, Blazegraph had a Transaction-Location header proposal over its REST Transaction API), Allegrograph supports transactions with session ports.
/ds/query and /ds/update stay exactly as they are for ordinary, non-transactional requests — zero behavior change for existing clients.
New control endpoint, /ds/transactions (a new Operation, dispatched the same way Query/Update/GSP_* are today via OperationRegistry/DataService):
POST /ds/transactions?type={read|write|read-promote|read-committed-promote} — begins a transaction (TxnType maps directly onto the existing enum in org.apache.jena.query.TxnType). Response: Location: /ds/transactions/{txnId}, txnId a server-generated opaque token (UUID).
POST /ds/transactions/{txnId}?action=commit — commit.
POST /ds/transactions/{txnId}?action=abort — abort/rollback.
GET /ds/transactions/{txnId} — status (active/idle-time remaining), mainly for diagnostics.
A transaction-scoped request is just an ordinary /ds/query or /ds/update call carrying an SPARQL-Transaction: {txnId} request header. No other change to the query/update request shape.
Mapping onto the existing Jena/Fuseki internals
(Filed against current main; class/line references may drift.)
HttpAction (jena-fuseki2/jena-fuseki-core/.../servlets/HttpAction.java) currently assumes one begin/end pair per request, on one thread (isInActionTxn, activeDSG are set up in begin(TxnType) and torn down in endInternal() within the same servlet call — see SPARQLQueryProcessor.execute() and SPARQL_Update.execute()). A held, cross-request transaction can't just reuse this path unmodified, since a transaction begun on one request's thread must be resumed on a different request's thread.
TDB2 already has the primitive this needs: DatasetGraphTDB's TransactionalSystem (org.apache.jena.dboe.transaction.txn.TransactionalBase, in jena-dboe-transaction) implements
which suspends a transaction off its current thread and resumes it on another. It's just not exposed above DatasetGraphTDB today — reaching it means unwrapping to the TDB2-specific type. Proposal: expose a narrow SPI (e.g. DetachableTransactional with default no-op detach()/attach(), implemented for real by TDB2's DatasetGraphTDB) so Fuseki can ask a DatasetGraph "can you hand a transaction across threads?" generically, and reject/501 the new endpoint for datasets that answer no.
TDB1 has no equivalent.DatasetGraphTransaction binds via a plain ThreadLocal<DatasetGraphTxn> with no suspend/resume API. Same for in-memory and other non-TDB2 DatasetGraphs. v1 of this proposal is TDB2-only; other backends would report "not detachable" and a POST /ds/transactions against them would fail cleanly rather than half-working.
A new server-side registry (per DataService, alongside its existing activeTxn/totalTxn counters in DataService.java) holds {txnId -> HeldTransaction}, where HeldTransaction wraps the detached TransactionCoordinatorState, the TxnType, and a last-touched timestamp. On a query/update request carrying SPARQL-Transaction, Fuseki looks up the entry, attach()s it for the duration of that one request, executes normally, then detach()s again in finally — explicit commit()/abort() only happen via the control endpoint.
Abandoned-transaction handling is not optional. A client that opens a write transaction and disappears would otherwise block every other writer indefinitely, and a leaked read transaction on the fallback lock path (TransactionalLock/LockMRSW, for non-TDB2 datasets, though those are out of scope here) can walk into the ReentrantReadWriteLock "maximum lock count exceeded" failure Jena has hit before (e.g. java error Maximum lock count exceeded #1499, java.lang.Error: Maximum lock count exceeded #2584). Proposal: an idle-timeout reaper thread per dataset that force-aborts held transactions past a configurable deadline, plus a configurable cap on concurrently-held transactions per dataset.
Multi-instance deployments are explicitly out of scope for v1. A held transaction is in-process state on one Fuseki instance; it has no meaning across a farm of Fuseki instances behind a load balancer without a shared coordinator. This should be documented as a hard limitation, not silently broken — an operator running Fuseki behind a non-sticky LB needs to know this feature won't work as expected there.
Suggest packaging this as an optional FusekiModule (jena-fuseki-main's pluggable module mechanism) rather than baking it into fuseki-core/fuseki-main unconditionally, so operators who don't want the operational risk of server-held locks (timeouts, cap tuning, the multi-instance caveat above) can simply not load it, and the core query/update path is untouched either way.
Non-goals
Distributed/XA transactions spanning multiple datasets or multiple Fuseki instances.
Any change to the semantics or wire format of a plain, non-transactional /query or /update request.
TDB1 or non-TDB2 backend support (v1 rejects cleanly; could be revisited later if there's demand).
Does packaging as a FusekiModule (opt-in) vs. a built-in fuseki-core feature match how you'd want this maintained?
Is TDB2-only for v1 acceptable, or is TDB1 parity a hard requirement before this would be considered?
Would the maintainers be open to a PR along these lines? Happy to adjust the design based on feedback before investing in an implementation — in particular I'd rather settle the API shape and TDB1-scope questions above first.
Are you interested in contributing a solution yourself? Yes, pending agreement on the approach above.
Are you interested in contributing a solution yourself?
Version
latest
Feature
Proposal: multi-request transaction support for Fuseki, built on TDB2
I've hoped for this for a long time, now it might be within grasp.
Yes, AI wrote this. I haven't dug into the Jena/Fuseki code, but I did review the proposal (and made minor edits).
Motivation
SPARQL 1.1/1.2 Protocol only defines atomic, single-shot query and update operations, there's no notion of a transaction spanning more than one HTTP request. This has come up repeatedly outside this project too: w3c/sparql-dev#83 is a long-running discussion of exactly this gap, and @afs already sketched an approach there:
This issue proposes building that out for Fuseki, on top of TDB2's existing internal ACID transactions (
Transactional/TxnType, Serializable isolation). The TDB2 storage engine already does the hard part, what's missing is a way for a client to hold one of those transactions open across several HTTP requests.Other triple stores have shipped support: RDF4J's REST API has a dedicated transaction resource, Blazegraph had a
Transaction-Locationheader proposal over its REST Transaction API), Allegrograph supports transactions with session ports.Proposed API
Following @afs's sketch:
/ds/queryand/ds/updatestay exactly as they are for ordinary, non-transactional requests — zero behavior change for existing clients./ds/transactions(a newOperation, dispatched the same wayQuery/Update/GSP_*are today viaOperationRegistry/DataService):POST /ds/transactions?type={read|write|read-promote|read-committed-promote}— begins a transaction (TxnTypemaps directly onto the existing enum inorg.apache.jena.query.TxnType). Response:Location: /ds/transactions/{txnId},txnIda server-generated opaque token (UUID).POST /ds/transactions/{txnId}?action=commit— commit.POST /ds/transactions/{txnId}?action=abort— abort/rollback.GET /ds/transactions/{txnId}— status (active/idle-time remaining), mainly for diagnostics./ds/queryor/ds/updatecall carrying anSPARQL-Transaction: {txnId}request header. No other change to the query/update request shape.Mapping onto the existing Jena/Fuseki internals
(Filed against current
main; class/line references may drift.)HttpAction(jena-fuseki2/jena-fuseki-core/.../servlets/HttpAction.java) currently assumes one begin/end pair per request, on one thread (isInActionTxn,activeDSGare set up inbegin(TxnType)and torn down inendInternal()within the same servlet call — seeSPARQLQueryProcessor.execute()andSPARQL_Update.execute()). A held, cross-request transaction can't just reuse this path unmodified, since a transaction begun on one request's thread must be resumed on a different request's thread.DatasetGraphTDB'sTransactionalSystem(org.apache.jena.dboe.transaction.txn.TransactionalBase, injena-dboe-transaction) implementsDatasetGraphTDBtoday — reaching it means unwrapping to the TDB2-specific type. Proposal: expose a narrow SPI (e.g.DetachableTransactionalwith default no-opdetach()/attach(), implemented for real by TDB2'sDatasetGraphTDB) so Fuseki can ask aDatasetGraph"can you hand a transaction across threads?" generically, and reject/501 the new endpoint for datasets that answer no.DatasetGraphTransactionbinds via a plainThreadLocal<DatasetGraphTxn>with no suspend/resume API. Same for in-memory and other non-TDB2DatasetGraphs. v1 of this proposal is TDB2-only; other backends would report "not detachable" and aPOST /ds/transactionsagainst them would fail cleanly rather than half-working.DataService, alongside its existingactiveTxn/totalTxncounters inDataService.java) holds{txnId -> HeldTransaction}, whereHeldTransactionwraps the detachedTransactionCoordinatorState, theTxnType, and a last-touched timestamp. On a query/update request carryingSPARQL-Transaction, Fuseki looks up the entry,attach()s it for the duration of that one request, executes normally, thendetach()s again infinally— explicitcommit()/abort()only happen via the control endpoint.TransactionalLock/LockMRSW, for non-TDB2 datasets, though those are out of scope here) can walk into theReentrantReadWriteLock"maximum lock count exceeded" failure Jena has hit before (e.g. java error Maximum lock count exceeded #1499, java.lang.Error: Maximum lock count exceeded #2584). Proposal: an idle-timeout reaper thread per dataset that force-aborts held transactions past a configurable deadline, plus a configurable cap on concurrently-held transactions per dataset.FusekiModule(jena-fuseki-main's pluggable module mechanism) rather than baking it intofuseki-core/fuseki-mainunconditionally, so operators who don't want the operational risk of server-held locks (timeouts, cap tuning, the multi-instance caveat above) can simply not load it, and the core query/update path is untouched either way.Non-goals
/queryor/updaterequest.Open questions for maintainers
SPARQL-Transactionvs. a query parameter (both were discussed in Multi-request transaction support in the SPARQL protocol w3c-cg/sparql-dev#83) is worth a decision before implementation starts.FusekiModule(opt-in) vs. a built-in fuseki-core feature match how you'd want this maintained?Would the maintainers be open to a PR along these lines? Happy to adjust the design based on feedback before investing in an implementation — in particular I'd rather settle the API shape and TDB1-scope questions above first.
Are you interested in contributing a solution yourself?
Yes