Hi All,
I thought I'd kick-start the conversation of what the remote transaction
flow could look like and how it would tie in with the TinkerPop 4 HTTP API.
I added some diagrams to
https://github.com/apache/tinkerpop/blob/remote-tx/docs/src/dev/future/http-api.md
which I'll refer to in this post.
Much of this design is borrowed from the current WebSocket implementation
and so many of the decisions made were done so to minimize changes needed
when moving from WebSockets to HTTP. The basic idea is that transactions
will be bound using a Transaction Id rather than a Session over a WebSocket
connection. The transaction will be controlled using the standard Gremlin
language syntax for transaction control ("g.tx().begin()",
"g.tx().commit()", "g.tx().rollback()") and once a begin() is issued, the
client must send all subsequent requests involved in that transaction to
the same endpoint. The server is responsible for tracking the state of the
transaction and executing each transaction against the appropriate Graph.
On receipt of a begin(), the server will generate the Transaction Id and
return it in the response to the client, and the client should attach that
Id to both the header and body for requests involved in that transaction.
Take a look at the "Protocol Flow" to see how this would work end to end.
Summary of Key Points
The "Request Format Specification" contains the expected fields that will
be used in the request and responses. Remember that there are other fields
that can be specified in the HTTP API but they aren't shown in those charts.
1. Single new field: Only transactionId is added to the request body
2. Dual transmission: Transaction ID must appear in both X-Transaction-Id
header AND transactionId body field
3. Header for routing: The header enables load balancer sticky sessions
without body parsing
4. Body for processing: The body field is used by the server to lookup
transaction state
5. Gremlin-based control: Transaction lifecycle uses standard Gremlin
syntax ("g.tx().begin()", "g.tx().commit()", "g.tx().rollback()")
6. Client-based affinity: Client binds to a single endpoint and all
transactions issued to the same endpoint.
7. Backward compatible: Requests without transactionId continue to work as
non-transactional requests
The protocol deliberately places host affinity responsibility on the client
side rather than requiring server-side coordination. The client is
responsible for routing all requests in a transaction to the same server
instance (via the X-Transaction-Id header for load balancer sticky
routing). This is very similar to the current WebSocket session design
where the client binds to one connection. The difference is that we are
binding to one endpoint instead in HTTP.
This design is intentionally server topology agnostic and works identically
whether the backend is a single server, a cluster with a load balancer, a
managed service, or a serverless deployment. The server implementation only
needs to manage local transaction state. Servers don't need to share
transaction state or implement distributed locking. Each server manages
only its own transactions, which should simplify implementations for
providers. Providers can deploy behind any load balancing strategy without
protocol changes. The X-Transaction-Id header provides a standard mechanism
for any infrastructure to achieve affinity.
Transaction control uses Gremlin script syntax ("g.tx().begin()",
"g.tx().commit()", "g.tx().rollback()") rather than dedicated REST
endpoints (e.g., "POST /transaction/begin", "DELETE /transaction/{id}").
This approach was chosen for several reasons:
-Consistency with existing API: All operations flow through the same
/gremlin endpoint with the same request format. GLVs/drivers/clients don't
need to implement multiple endpoint patterns or handle different
request/response structures for transaction control vs. query execution.
-Script and Traversal compatible: This allows for scripts to have the same
functionality as traversals.
Please respond if you have any questions or comments.
Thanks,
Ken