# Clotho Transactions Clotho provides two levels of transactional control: 1. **Optimistic locking** via `@Version` — detects concurrent modifications on individual entities at write time. Does not require a database transaction. 2. **Transactional sessions** — wraps all reads and writes in a single ArcadeDB transaction, providing atomicity for multi-step operations. This document covers transactional sessions. --- ## Why transactions? Without a transaction: - Each Gremlin traversal is auto-committed by ArcadeDB immediately. - A multi-step save that fails midway leaves the graph in a partial state. - Two concurrent sessions can read and overwrite each other's changes without any isolation guarantee. With a transactional session, all operations share a single transaction scope. Either everything commits or nothing does. --- ## Manual transactional sessions ```java ClothoSession session = sessionFactory.openTransactionalSession(); try { Company acme = new Company(); acme.setName("Acme Corp"); Person alice = new Person(); alice.setName("Alice"); session.save(alice); session.save(acme); session.close(); // auto-save dirty entities + tx.commit() } catch (RuntimeException e) { session.rollback(); // tx.rollback(), cache cleared, session unbound throw e; } ``` Or use try-with-resources (preferred — `close()` commits on success, rolls back on exception): ```java try (ClothoSession session = sessionFactory.openTransactionalSession()) { session.save(alice); session.save(acme); } // ← tx.commit() fires here; RuntimeException triggers tx.rollback() ``` --- ## Spring `@Transactional` When using the Spring Boot starter, `ClothoTransactionManager` is registered automatically (conditional on no other `PlatformTransactionManager` bean being present). This wires Clotho into Spring's standard `@Transactional` machinery. ### Obtaining the session inside a transactional method **Option A — `openSession()` (recommended):** behaves correctly in and out of a transaction. Inside a `@Transactional` method the active session is borrowed (no new session created); outside, a new non-transactional session is opened. ```java @Service @RequiredArgsConstructor public class PersonService { private final ClothoSessionFactory sessionFactory; private final PersonRepository personRepo; @Transactional public void promote(ARID personId) { try (ClothoSession session = sessionFactory.openSession()) { // borrows tx session Person p = session.load(Person.class, personId); p.setRole("SENIOR"); // auto-saved on session close; tx committed by Spring tx manager } } } ``` **Option B — `getCurrentSession()`:** throws `ClothoException` if no session is active. Use when you explicitly require a session to already be bound (e.g. in a helper that must only be called from within a `@Transactional` context). ```java @Transactional public void doWork() { ClothoSession session = sessionFactory.getCurrentSession(); // ... } ``` ### Repository queries inside a transaction `ClothoRepository.execute(traversal)` (no-session overload) uses the factory's base `GraphTraversalSource` and is **not** part of the transaction. Use the session-aware overload instead: ```java @Transactional public void updateEngineers() { try (ClothoSession session = sessionFactory.openSession()) { List engineers = personRepo.execute(session, personRepo.allEntities(), t -> t.has("role", "Engineer")); for (Person p : engineers) { p.setRole("Senior Engineer"); // dirty tracking fires; auto-saved on session close } } } ``` --- ## Nested `@Transactional` methods Spring's default propagation is `REQUIRED`: an inner `@Transactional` method joins the outer transaction rather than starting a new one. Clotho honours this via the borrow counter: ```java @Transactional // outer — opens a transactional session public void outer() { inner(); // inner joins; no new session or commit // commit happens here when outer returns } @Transactional // inner — borrows the outer session public void inner() { // ... same transaction } ``` If you need the inner method to run in its own transaction, use `@Transactional(propagation = Propagation.REQUIRES_NEW)`. Clotho does not currently support REQUIRES_NEW natively — the inner `openSession()` will still borrow the outer session. If true nested transaction isolation is required, manage sessions manually with separate `openTransactionalSession()` calls. --- ## Apps with another transaction manager (JPA, R2DBC) `ClothoTransactionManager` registers with `@ConditionalOnMissingBean(PlatformTransactionManager.class)`. If your application already has a JPA or R2DBC transaction manager, the Clotho bean is skipped. To use Clotho transactions alongside another tx manager, declare the bean explicitly and qualify it: ```java @Bean public ClothoTransactionManager clothoTransactionManager(ClothoSessionFactory factory) { return new ClothoTransactionManager(factory); } ``` Then annotate service methods: ```java @Transactional("clothoTransactionManager") public void myGraphOperation() { ... } ``` Or declare Clotho's manager as `@Primary` if it is the dominant transaction manager in your application. --- ## Relationship between `@Version` and transactions `@Version` provides **optimistic** concurrency control — it detects conflicts at write time by checking the entity's application-managed version counter against what is stored in the database. It works independently of the transactional session API. Combining both: - The batch auto-save at `close()` provides atomic writing of all dirty entities in one Gremlin request (the only atomicity guarantee available over ArcadeDB's Gremlin WebSocket). - `@Version` prevents lost updates when two sessions modify the same entity concurrently, via a two-step check-then-write that fails the entire batch if any version is stale. A `@Version` check failure throws `OptimisticLockException` during the pre-flight step. Inside a transactional session, the exception propagates out of `close()`. Since no properties were written during the pre-flight, no rollback is necessary (but `close()` will attempt `g.tx().rollback()` regardless, which is a no-op with ArcadeDB Gremlin). --- ## Session auto-close behaviour in transactions | Scenario | What happens on `session.close()` | |---|---| | Non-transactional | Auto-saves dirty managed entities; clears cache; unbinds | | Transactional (new) | Auto-saves dirty managed entities; `tx.commit()`; clears cache; unbinds | | Transactional (borrowed) | Decrements borrow count; no-op until count reaches 0 | | Exception during auto-save | `tx.rollback()`; clears cache; unbinds; re-throws | Call `session.rollback()` explicitly to abort the transaction at any point without saving. `rollback()` always resets the borrow count to 0 and unbinds immediately. --- ## Batch auto-save and the safe session pattern When `session.close()` triggers auto-save, Clotho collects every dirty MANAGED entity from the session cache — vertices and edges — and persists them in **a single Gremlin traversal**: ``` inject(0).union( __.V(id1).property("name", "Alice"), // dirty vertex __.V(id2).property("dept", "Eng"), // dirty vertex __.E(eid).property("role", "Senior"), // dirty edge ... ).iterate() ``` This collapses N round-trips into one, making the auto-save as efficient as possible and treating all updates as a single ArcadeDB request. ### Recommended session pattern Load everything you intend to modify at the start of the session, make your changes, then let `close()` flush them together: ```java try (ClothoSession session = sessionFactory.openSession()) { Person alice = session.load(Person.class, aliceId); // load upfront Person bob = session.load(Person.class, bobId); Company acme = session.load(Company.class, acmeId); // ... fetch any read-only helper data here too ... alice.setEmail("alice@new.example.com"); // dirty tracking fires bob.setRole("Senior Engineer"); acme.setName("Acme Corp International"); } // ↑ single batch traversal flushes all three changes here ``` The more entities you modify before closing, the more work is consolidated into that one round-trip. ### Atomicity — what "single request" means here ArcadeDB auto-commits each incoming Gremlin request as one database operation. Because the auto-save batch is one request, updates to entities that **pass** their checks are committed together. Any interruption (network failure, server error) affects the entire request. However, there are two important caveats: **Explicit `session.save()` calls mid-session are NOT batched.** Each call to `session.save(entity)` issues an immediate individual traversal. Only the auto-save triggered by `session.close()` uses the batch path. **`@Version` conflicts fail the whole batch.** The batch auto-save runs in two Gremlin round-trips: 1. **Pre-flight** — one request checks that every `@Version`-guarded entity still has its expected version in the database. If any version is stale, `OptimisticLockException` is thrown immediately and no properties are written. 2. **Batch update** — one request writes all dirty properties for all entities, plus the incremented version for each `@Version` entity. Concretely: if session has dirty entities A (no `@Version`), B (`@Version`, stale), and C (`@Version`, current), the outcome is: - Pre-flight detects B's stale version → `OptimisticLockException` thrown - Neither A, B, nor C has been written yet There is a narrow TOCTOU window between the pre-flight and the write (a concurrent session could change B's version in that gap), but within that window only already-confirmed entities would be overwritten. ### `@Version` — application-managed OCC `@Version` does **not** rely on ArcadeDB's internal `@version` property (which is not accessible via the Gremlin WebSocket interface). Instead, Clotho manages the counter as a regular graph property: - **On create**: the version property is initialised to `0` in the graph; the in-memory field is set to `0`. - **On load**: the version is read like any other property. - **On update**: the pre-flight filter is `.has(versionProp, currentVersion)`. If matched, the new version `currentVersion + 1` is written atomically in the same traversal step. The in-memory field is updated after a successful write. The property name defaults to `"_version"` and can be overridden via `@Version("myProp")`. ### ArcadeDB Gremlin transaction limitations ArcadeDB's Gremlin WebSocket interface returns a transaction-bound `GraphTraversalSource` from `g.tx().begin()`, but subsequent Gremlin bytecode operations on that source fail at runtime. `openTransactionalSession()` therefore uses the base traversal source — the same one used by non-transactional sessions. The practical consequences: - Each Gremlin request is auto-committed by ArcadeDB immediately. - `g.tx().commit()` / `g.tx().rollback()` (called by `close()` / `rollback()`) are effective no-ops in this mode. - The **batch auto-save** at `close()` is still a single Gremlin request, so it is atomic at the request level. - The **borrow counter** and **Spring `@Transactional` bridge** (`ClothoTransactionManager`) work correctly and provide the expected API surface. True multi-request atomicity requires ArcadeDB's native Java or HTTP API directly. ### OrientDB Gremlin transaction limitations OrientDB with `orient-transactional=false` (required so that `addV` returns real record IDs immediately rather than temporary `#-1:-2` IDs) rejects `g.tx()` calls entirely, throwing `Graph does not support transactions`. At startup, `ClothoSessionFactory` probes transaction support by calling `g.tx().rollback()`. If the graph rejects the call with the "does not support transactions" message, all `g.tx().commit()` and `g.tx().rollback()` calls are skipped for every session. The `transactional` flag still drives the batch auto-save path and the borrow/Spring `@Transactional` semantics — only the explicit `g.tx()` calls are suppressed. - Each Gremlin request auto-commits individually; the batch auto-save in `close()` provides request-level atomicity (one request = all dirty entity writes). - True multi-request transactions require OrientDB's native Java or binary protocol. --- ## Limitations - `ClothoRepository.execute(traversal)` without a session is NOT part of the current transaction. Always use `execute(session, traversal)` for transactional repository queries. - `ClothoSession` is NOT thread-safe. One session per thread / virtual thread. - ArcadeDB requires the schema (vertex and edge types) to exist before any write. Clotho does not manage schema creation.