Tentackle Session — The Persistence and Security Context Layer¶
Overview and Motivation¶
The tentackle-session module defines the session abstraction: the runtime context that ties a user, a
connection target, and a transaction together. A Session is the
single object every higher layer talks to in order to start transactions, run database work, share state, and be
notified about changes — regardless of whether the database is connected directly to the current JVM or reached
through a remote Tentackle server (or even a chain of middle-tiers).
The module sits between tentackle-core (its only dependency) and the persistence
stack (tentackle-database, tentackle-persistence,
tentackle-pdo). It deliberately contains only interfaces, abstract bases,
annotations, events, and a handful of small helper classes — the concrete Session implementation lives in
tentackle-database, which covers both the local (JDBC) and the remote (TRIP) case. This keeps the contract that the whole
framework programs against free of any JDBC, SQL-dialect, or wire-protocol detail.
A central design goal is location transparency. The same Session API is used whether persistence runs
locally or on a middle-tier server. A session knows whether it is local (the physical database connection lives
in this JVM) or remote (calls are routed via TRIP to a server), and application code does not have to care.
Design principles¶
- Interface-only contract. Everything that the framework above needs is expressed as an interface
(
Session,SessionInfo,SessionPool,ModificationTracker, …) located through a factory. Implementations are injected via Tentackle'sServiceFinder/ServiceFactorySPI, so the same calling code binds to a local or remote implementation at runtime. - No dependency on the database or SQL layer. Notably, even the connection/driver configuration classes
(
BackendConfiguration,DriverConfiguration) live here and explicitly avoid depending ontentackle-sqlortentackle-database, so UI and configuration code can manipulate connection settings without dragging in the backend machinery. - Location transparency.
isRemote()/getRemoteSession()expose locality when needed, but the transaction, property, and change-notification APIs behave identically for local and remote sessions. - Thread affinity via thread-locals. The "current" session is held in a
ThreadLocal, so domain and persistence code can reach it without threading it through every method signature.
Key Concepts¶
Session¶
Session is the heart of the module — a persistence and security
context. Its responsibilities group into:
| Group | Methods | Purpose |
|---|---|---|
| Current-session management | getCurrentSession, setCurrentSession, getSession, makeCurrent, isCurrent, clearCurrent |
Maintain and reach the thread-local "current" session. |
| Lifecycle | close, reOpen, clone, registerCloseHandler, unregisterCloseHandler, isPooled |
Open/close/clone sessions and react to closing. |
| Transactions | begin, commit, rollback, rollbackSilently, transaction(...), isTxRunning, getTxName, getTxNumber, getTxLevel, setConstraintsDeferred, isConstraintsDeferred |
Start and finish (possibly nested) transactions; defer deferrable constraint checks to commit time. |
| Savepoints | setSavepoint, rollback(SavepointHandle), releaseSavepoint |
Fine-grained rollback within a transaction. |
| Pre/post-commit hooks | preCommit, postCommit |
Run logic immediately before, or right after, the physical commit (e.g. firing events post-commit). |
| Grouping | groupWith, getSessionId, getSessionGroupId |
Tie several sessions of the same URL into a group that shares e.g. "same user". |
| Locality | isRemote, getRemoteSession, getUrl |
Discover whether the backend is local or reached via a server. |
| Keep-alive | setAlive, isAlive, setKeepAliveInterval, getKeepAliveInterval |
Prevent idle remote sessions from being timed out by the server. |
| State sharing | setProperty, getProperty, clearProperties |
Application state that, for remote sessions, is mirrored to the server-side session. |
| Misc | getDispatcher, setOwnerThread, getInstanceNumber, getName, getSessionInfo, preloadRemoteDelegates, applyTo(...) |
Async task dispatch, accidental-cross-thread detection, remote startup tuning, propagating the session onto SessionDependables. |
The static getCurrentSession()/setCurrentSession() pair backs the thread-local model; getSession() asserts
one is present and throws a PersistenceException
otherwise.
Transactions¶
begin() returns a transaction voucher (a long). Only the call that actually started the transaction
receives a non-zero voucher; nested begin()s return 0 and do nothing. The matching commit(voucher) /
rollback(voucher) only commit or roll back if the voucher is the one that opened the transaction — this makes
correct nesting trivial: every method may begin()/commit() defensively, and only the outermost actually
commits. The transaction(...) convenience methods wrap a lambda in this protocol and are the recommended
alternative to the @Transaction interceptor when the holder is not an Interceptable.
A local session can also take part in a global (JTA/XA) transaction coordinated by a transaction manager; the voucher protocol stays the same. See Global Transactions — JTA and XA.
TransactionIsolation and
TransactionWritability are type-safe enums over the
java.sql.Connection isolation constants and read-only/read-write mode, used when a transaction needs something
other than the default.
SessionInfo¶
SessionInfo is the serializable descriptor of
who and what a session represents: user id/name/class-id, the
Credentials proving that identity, application name/id,
locale, timezone, client version, host/OS/VM info, and the connection properties/propertiesName. It travels to
the server via TRIP when a remote session logs in, and the server may write values back (e.g., the resolved
user-id) for the client to read.
A session info is mutable until it is made Immutable (server applications do this with their own info);
after that, setters throw an ImmutableException. Equality is by username alone, so a clone, the original
and an info created just to name a user are interchangeable as keys — MultiUserSessionPool depends on it.
clone() copies the credentials physically, clears the session name, and enables lockLingerEnabled.
The connection properties are loaded lazily from getPropertiesName() (default backend) on first access.
The credentials default to
PasswordCredentials, so getUserName(),
getPassword(), setPassword(char[]) and clearPassword() simply read, write and wipe the default credentials
underneath. setCredentials(...) replaces them by another kind — a bearer token, for instance — which is how
single sign-on plugs in; see Authentication.
SessionInfos are created by the SessionInfoFactory
singleton (default implementation
DefaultSessionInfoFactory producing
DefaultSessionInfo), which can build one from a
username/password, from a properties name, or from an
EncryptedProperties object — honoring
Cryptor-encrypted credentials. It also
carries flags such as lockLingerEnabled (whether token-locks survive a session close) and the client version,
which the server checks against its own. The client in turn checks the server's version in
checkServerVersion(serverVersion), a hook that does nothing by default and throws a
VersionIncompatibleException if the versions don't fit.
Authentication¶
Verifying the credentials is a concern of its own, described in full in
Authentication. In outline, and living entirely in org.tentackle.session.auth:
- On the client, a
CredentialsProviderobtains the credentials before the session is opened — interactively if the method requires it, using the UI-agnosticCredentialsPromptcallbacks. Providers areMappedServices registered by method name via@CredentialsProviderServiceand selected through theauthenticationkey inbackend.properties; the default ispassword, served by the built-in non-interactivePasswordCredentialsProvider. - On the server, the
AuthenticationManagerdrives a priority-ordered chain ofAuthenticationProvider@Services. Each one either returns anAuthentication(name, claims, roles), abstains withnull, or vetoes by throwing anAuthenticationException. Authentication is deny by default: without a provider, every login is refused. - An optional
UserResolvermaps the proven identity onto the application's user entity, yielding theuserId/userClassIdthat are written back onto theSessionInfoand driveDomainContextand theSecurityManager.
Confidential values are held in a Secret, which keeps them
encrypted in memory — and hence also on the wire — whenever a
Cryptor is configured.
Note that this applies to sessions reaching the database through a Tentackle server. A direct two-tier session
(jdbc:/jndi: URL) authenticates at the database itself and never runs the provider chain.
Session creation, factory, and pooling¶
SessionFactory— the singleton that creates open sessions from aSessionInfo(create(),create(info)), plus a pool-internalcreate(pool, info). It also manages global close handlers applied to every session.SessionPool— a pool of sessions for oneSessionInfo, letting a single user run several sessions in parallel. Built viaSessionPoolFactory.MultiUserSessionPool— a pool spanning multipleSessionInfos, so different users can each have parallel sessions (typical for a server).SessionPoolProvider— abstracts whether a pool is local or remote.SessionPooledExecutor— runsSuppliers in parallel, each on its own pooled session (as the thread-local session), for background work. A success or fail handler is invoked once the session is back in the pool. The batch variantsubmit(Collection, …)does not block: a separate waiter thread collects the results (and failures, including a timeout) and then invokes the handlers.
Modification tracking¶
The change-notification subsystem lets any JVM learn that data changed, even when the change happened on another client through the server. This section is the overview — see Modification Tracking for the full story: the modification table, counting and rollback correction, operation modes, the poll loop, delayed listeners and master serial events.
ModificationTracker— the singleton that tracks global PDO changes. It maintains its own session (and is both aSessionTaskDispatcherand anExclusiveSessionProvider). Code callscountModification(session, name)when it modifies data, andaddDeletion(...)for deletes; listeners are registered viaaddModificationListener(...). It exposes the currentgetIdSerialForClass(...)/getIdSerialForName(...), caninvalidate()itself, and reportsisLocalClientMode()(clients directly on the DB, no middle tier). Convenience staticssubmit(...)/transaction(...)schedule work on the tracker's own session.ModificationListener— listens for changes by name (usually a table name, but any application name is allowed;null= listen to everything). BeyonddataChanged(event), it controls ordering (getPriority) and batching (getTimeFrame,getTimeDelay) so bursts of events can be coalesced and fired at a randomized time to spread server load.ModificationListenerAdapteris a convenience base.ModificationEvent/ModificationEventDetail— events come in three flavors: a master event (any change, no detail), a single named event, or a multi named event carrying one detail per name.
Master serial events¶
On top of the bare modification serial, the module supports pushing richer, application-specific events from server to client:
MasterSerialwraps thelong serialthe tracker of a remote client periodically polls from the server.MasterSerialEventis an application-defined event (extending the serial with arbitrary payload). Each event class is paired with aConsumerhandler annotated with@MasterSerialEventService— aMappedServicethat maps an event type to its handler. This means the server need not know the handlers at all, and different clients can react differently to the same server-side event.MasterSerialListEventbundles several pending events for a client. Its constructor,add(...)andaddAll(...)honor the override-older-events rule; the positional mutators inherited fromArrayList(add(int, …),addFirst,set, …) do not.MasterSerialEventHandlerFactory(defaultDefaultMasterSerialEventHandlerFactory) resolves handlers, one stateless singleton per event type, ornullif the client has no handler for it. The annotation is verified at compile time byMasterSerialEventServiceAnnotationProcessor(Consumerimplemented, no-args constructor present).
Session task dispatching¶
SessionTaskDispatcher— aTaskDispatcherbound to aSession, executing submitted tasks serially on one exclusive connection (default implDefaultSessionTaskDispatcher, withSessionTaskDispatcherLock). The dispatcher thread owns the session and makes it its thread-local session; a pooled session is refused. It can keep its session alive while idle, terminates when the session gets closed, and optionally closes the session on termination.AbstractSessionTask— base for tasks that need the dispatcher's session (it is aSessionDependable).SessionKeepAliveDaemon/SessionKeepAliveTask— periodicallysetAlive()a session (pinging asynchronously so a slow remote line cannot block the daemon), honoring each session'skeepAliveInterval(at least 1 second). After more than 10 failed pings in a row, the session is closed and it is up to the application to re-open it. A negative interval requests a non-blocking close.
Session providers and dependables¶
SessionProvider— anything that can hand out a session.ExclusiveSessionProvider— provides exclusive, reentrant access to a session: once acquired, no other thread gets it until released the matching number of times.SessionDependable— an object carrying a session;Session.applyTo(...)propagates a session onto such objects (and collections of them, including aTrackedList's removed objects), guarding against cycles.SessionHolder/ThreadLocalSessionHolder— a serializable holder of a session; the thread-local variant is handy where no domain context is available.
Connection configuration¶
These two classes describe how to connect and are persisted through the standard java.util.prefs.Preferences
API. Importantly, they depend on neither tentackle-sql nor tentackle-database, so configuration UIs (e.g.,
in tentackle-fx-rdc) can edit them without the backend on the classpath.
AbstractSessionConfiguration— shared base;Validateableand aScopeConfigurator, with a symbolicnameand apersistedflag.BackendConfiguration— the connection target: URL, options, user, password (stored encrypted if aCryptoris configured) and a reference to aDriverConfiguration. Provides static lookup of all configurations / the default for an application from user or system preferences.DriverConfiguration— how to load the JDBC driver (driver class name + URL to load it from).
Database-centric 2-tier applications, such as BundleMonkey, use this to configure the database connections. It is rarely used in a client-server application.
Annotations describing the persistent model¶
These annotations let the persistence layer examine the generated model by reflection:
@Persistent— marks persistent fields, methods or types, carryingcomment,component,parent,embeddedand anordinal. The ordinal encodes the field/method order from the model definition.embeddedmarks the getter of a relation to an embedded entity, whose columns live in the owner's table.@ClassId— the numeric class id of a persistent object (aMappedService).@TableName— the database table name for a PDO, with build-time optionsmapSchema(replace dots by underscores) andprefix(configurable via the Wurbelizer plugin'swurbletProperties, e.g.tablePrefix), to retarget schemas/tables without touching the model. See Table Names for the full picture, including the corresponding maven parameters.
SessionValidationUtilities (a @Service
overriding the core ValidationUtilities) uses the @Persistent ordinal so validators fire in priority plus
source-code order.
Session utilities¶
SessionUtilities is a replaceable @Service singleton
(the database layer replaces it by DbSessionUtilities) bundling the module's infrastructure:
- Model lookup — maps between class names, table names and class ids, built once from the
@TableNameand@ClassIdmapped services (getTableName,getClassName,getClassId,getClassNames), plusdetermineTablename/determineClassIdreading the annotations of a class directly. - Keep-alive —
keepAliveIntervalChanged(session)starts theSessionKeepAliveDaemonon demand. Should the daemon have died, the next call starts a new one and hands all registered sessions over to it.terminateHelperThreads()stops it. - Exception hooks —
alignExceptionCause(replace driver-specific causes a remote JVM cannot deserialize),alignTemporaryExceptionStatus(flag transient database errors as temporary),isSilentRollbackSufficientandcreateLazyExceptionMessage, used byPersistenceException. See Exceptions and Error Handling.
Exceptions¶
A family of (mostly persistence-related) exceptions is defined here so the upper layers share one vocabulary:
PersistenceException (the root),
SessionClosedException,
LoginFailedException,
AlreadyLoggedInException,
VersionIncompatibleException,
NotFoundException,
NotRemovableException and
ConstraintException.
The authentication failures live next to their SPI in org.tentackle.session.auth:
AuthenticationException and its subclass
CredentialsExpiredException, which tells
the client that refreshing the credentials and retrying is worth a try.
Package Layout¶
| Package | Contents |
|---|---|
org.tentackle.session |
The whole public API: Session, SessionInfo, the factories and pools, the modification-tracking and master-serial types, the task-dispatcher types, the configuration classes, the model annotations, the Default* helper implementations and the exceptions. |
org.tentackle.session.auth |
Authentication: the Credentials types and their Secret, the server-side AuthenticationProvider/AuthenticationManager/UserResolver SPI, the client-side CredentialsProvider/CredentialsPrompt SPI, and the authentication exceptions. |
org.tentackle.session.service |
The Hook (ModuleHook) placing the module in the module hierarchy and providing resource-bundle/i18n access. |
org.tentackle.session.apt |
The annotation processor MasterSerialEventServiceAnnotationProcessor for @MasterSerialEventService. |
How It Fits Together¶
Obtaining and using a session¶
Application code rarely constructs sessions directly; it usually receives one from a pool or the current domain context. At the lowest level, however, the flow is:
// describe who/what (the username/password form creates PasswordCredentials;
// setCredentials(...) would swap in a token instead)
SessionInfo info = SessionInfoFactory.getInstance().create("scott", password, "myapp");
// create an open session (local or remote, decided by the configuration/URL)
Session session = SessionFactory.getInstance().create(info);
session.makeCurrent(); // becomes the thread-local current session
// transactional work — only the outermost begin() commits
long tx = session.begin("doStuff");
try {
// ... persistence operations run against Session.getSession() ...
session.commit(tx);
}
catch (RuntimeException ex) {
session.rollback(tx);
throw ex;
}
// or, equivalently, with the lambda form:
session.transaction("doStuff", () -> {
// ... work ...
return null;
});
Local vs. remote¶
The very same code runs unchanged whether session.isRemote() is true or false. For a remote session, the
SessionFactory binds a remoting implementation that routes begin/commit/query calls over TRIP to the server,
where a mirror session does the actual JDBC work; getRemoteSession() exposes server-side helpers (e.g.,
server-side logging). For a local session, the implementation in tentackle-database talks to JDBC directly.
The difference shows at login: a remote session sends its SessionInfo — credentials included — to the server,
which runs the authentication provider chain and writes the resolved user back onto the info,
whereas a local session authenticates at the database itself.
Reacting to changes across clients¶
A client registers a ModificationListener with the ModificationTracker. The tracker of a remote client
periodically polls the server's master serial. When it advances, the tracker reads the serials of the tracked
names and the registered listeners receive ModificationEvents for the names they care about — so a change
committed by one user can refresh the screens of another. Independently of that, any MasterSerialEvents the
server queued for the client ride along with the polled master serial, and the matching
@MasterSerialEventService handlers run on the client.
Extending the Module¶
- Custom server-to-client events: implement
MasterSerialEventand aConsumerhandler annotated with@MasterSerialEventService(YourEvent.class). The server stays oblivious to handlers; clients bind their own. - Custom session info: provide a
SessionInfo/SessionInfoFactoryimplementation via the SPI to carry extra per-session attributes between client and server. - Custom authentication: register an
@ServiceAuthenticationProviderto verify a new kind of credentials on the server, and a@CredentialsProviderServiceCredentialsProviderto obtain them on the client — see Authentication. - Reacting to session lifecycle: register a
SessionCloseHandler(per session, or globally via theSessionFactory) — but keep it non-blocking, as it runs during close. - Custom session utilities: replace
SessionUtilitiesvia@Service(SessionUtilities.class)— usually by extendingDbSessionUtilities— e.g. to align further exception types.
Module Dependencies¶
tentackle-session sits low in the stack and is intentionally thin:
- It requires transitive
tentackle-core(and through ittentackle-common): TRIP serialization, tasks/dispatchers, validation, binding, reflection and service discovery. - It exports
org.tentackle.sessionandorg.tentackle.session.auth, and opens both toorg.tentackle.corefor TRIP, becauseSessionInfoand its credentials, modification and master-serial events travel between JVMs. - It provides a
ModuleHookservice (org.tentackle.session.service.Hook) for i18n bundle resolution. - It depends on neither
tentackle-sqlnortentackle-database; those (and the remoting modules) depend on it and supply the concrete local/remoteSessionimplementations.
Related Documentation¶
- Backend Properties — the reference of all session configuration keys (
url, credentials, timeouts, server endpoints, …). - Authentication — the credentials a
SessionInfocarries and the provider chain verifying them at login. - Modification Tracking — the
ModificationTracker, its listeners and the master serial events, in depth. - Tentackle SQL — the database backend layer beneath the local session implementation.
- PDO / Operations — the persistent domain objects that run on top of sessions.
- Services / ServiceFinder — the SPI mechanism behind the session, info, and tracker factories.
- TRIP — the remoting protocol that carries a remote session's calls and its
SessionInfobetween JVMs. - Locking — token locks, whose
lockLingerEnabledflag is carried on theSessionInfo. - Database — the module providing the concrete local
Sessionimplementation. - Exceptions and Error Handling —
PersistenceExceptionand the session it points at, lazy messages, and theSessionUtilitiesalignment hooks.