Skip to content

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's ServiceFinder/ServiceFactory SPI, 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 on tentackle-sql or tentackle-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 CredentialsProvider obtains the credentials before the session is opened — interactively if the method requires it, using the UI-agnostic CredentialsPrompt callbacks. Providers are MappedServices registered by method name via @CredentialsProviderService and selected through the authentication key in backend.properties; the default is password, served by the built-in non-interactive PasswordCredentialsProvider.
  • On the server, the AuthenticationManager drives a priority-ordered chain of AuthenticationProvider @Services. Each one either returns an Authentication (name, claims, roles), abstains with null, or vetoes by throwing an AuthenticationException. Authentication is deny by default: without a provider, every login is refused.
  • An optional UserResolver maps the proven identity onto the application's user entity, yielding the userId/userClassId that are written back onto the SessionInfo and drive DomainContext and the SecurityManager.

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 a SessionInfo (create(), create(info)), plus a pool-internal create(pool, info). It also manages global close handlers applied to every session.
  • SessionPool — a pool of sessions for one SessionInfo, letting a single user run several sessions in parallel. Built via SessionPoolFactory.
  • MultiUserSessionPool — a pool spanning multiple SessionInfos, so different users can each have parallel sessions (typical for a server).
  • SessionPoolProvider — abstracts whether a pool is local or remote.
  • SessionPooledExecutor — runs Suppliers 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 variant submit(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 a SessionTaskDispatcher and an ExclusiveSessionProvider). Code calls countModification(session, name) when it modifies data, and addDeletion(...) for deletes; listeners are registered via addModificationListener(...). It exposes the current getIdSerialForClass(...) / getIdSerialForName(...), can invalidate() itself, and reports isLocalClientMode() (clients directly on the DB, no middle tier). Convenience statics submit(...) / 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). Beyond dataChanged(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. ModificationListenerAdapter is 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:

  • MasterSerial wraps the long serial the tracker of a remote client periodically polls from the server.
  • MasterSerialEvent is an application-defined event (extending the serial with arbitrary payload). Each event class is paired with a Consumer handler annotated with @MasterSerialEventService — a MappedService that 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.
  • MasterSerialListEvent bundles several pending events for a client. Its constructor, add(...) and addAll(...) honor the override-older-events rule; the positional mutators inherited from ArrayList (add(int, …), addFirst, set, …) do not.
  • MasterSerialEventHandlerFactory (default DefaultMasterSerialEventHandlerFactory) resolves handlers, one stateless singleton per event type, or null if the client has no handler for it. The annotation is verified at compile time by MasterSerialEventServiceAnnotationProcessor (Consumer implemented, no-args constructor present).

Session task dispatching

  • SessionTaskDispatcher — a TaskDispatcher bound to a Session, executing submitted tasks serially on one exclusive connection (default impl DefaultSessionTaskDispatcher, with SessionTaskDispatcherLock). 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 a SessionDependable).
  • SessionKeepAliveDaemon / SessionKeepAliveTask — periodically setAlive() a session (pinging asynchronously so a slow remote line cannot block the daemon), honoring each session's keepAliveInterval (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 a TrackedList'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; Validateable and a ScopeConfigurator, with a symbolic name and a persisted flag.
  • BackendConfiguration — the connection target: URL, options, user, password (stored encrypted if a Cryptor is configured) and a reference to a DriverConfiguration. 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, carrying comment, component, parent, embedded and an ordinal. The ordinal encodes the field/method order from the model definition. embedded marks 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 (a MappedService).
  • @TableName — the database table name for a PDO, with build-time options mapSchema (replace dots by underscores) and prefix (configurable via the Wurbelizer plugin's wurbletProperties, 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 @TableName and @ClassId mapped services (getTableName, getClassName, getClassId, getClassNames), plus determineTablename/determineClassId reading the annotations of a class directly.
  • Keep-alive — keepAliveIntervalChanged(session) starts the SessionKeepAliveDaemon on 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), isSilentRollbackSufficient and createLazyExceptionMessage, used by PersistenceException. 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 MasterSerialEvent and a Consumer handler annotated with @MasterSerialEventService(YourEvent.class). The server stays oblivious to handlers; clients bind their own.
  • Custom session info: provide a SessionInfo/SessionInfoFactory implementation via the SPI to carry extra per-session attributes between client and server.
  • Custom authentication: register an @Service AuthenticationProvider to verify a new kind of credentials on the server, and a @CredentialsProviderService CredentialsProvider to obtain them on the client — see Authentication.
  • Reacting to session lifecycle: register a SessionCloseHandler (per session, or globally via the SessionFactory) — but keep it non-blocking, as it runs during close.
  • Custom session utilities: replace SessionUtilities via @Service(SessionUtilities.class) — usually by extending DbSessionUtilities — 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 it tentackle-common): TRIP serialization, tasks/dispatchers, validation, binding, reflection and service discovery.
  • It exports org.tentackle.session and org.tentackle.session.auth, and opens both to org.tentackle.core for TRIP, because SessionInfo and its credentials, modification and master-serial events travel between JVMs.
  • It provides a ModuleHook service (org.tentackle.session.service.Hook) for i18n bundle resolution.
  • It depends on neither tentackle-sql nor tentackle-database; those (and the remoting modules) depend on it and supply the concrete local/remote Session implementations.
  • Backend Properties — the reference of all session configuration keys (url, credentials, timeouts, server endpoints, …).
  • Authentication — the credentials a SessionInfo carries 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 SessionInfo between JVMs.
  • Locking — token locks, whose lockLingerEnabled flag is carried on the SessionInfo.
  • Database — the module providing the concrete local Session implementation.
  • Exceptions and Error Handling — PersistenceException and the session it points at, lazy messages, and the SessionUtilities alignment hooks.