Application Bootstrap — From main() to a Running Tier¶
Overview and Motivation¶
Every Tentackle application — a database-backed console daemon, a TRIP application server, a JavaFX desktop client, a web application in a servlet container — starts the same way:
What differs between them is not the startup logic but the order and timing of a fixed set of steps. A console application runs them straight through. A server adds session pools and a TRIP listener. A desktop client cannot log in until a human has typed a password, so it suspends midway through and resumes on a JavaFX thread. A web application is a server that hands sessions to a servlet container.
Application captures what all of them
have in common: it is the process-wide singleton that owns the session, the root
domain context, the application's properties and its
lifecycle. AbstractApplication
implements the steps as overridable template methods; each tier's base class assembles
them into the sequence that tier needs.
This document describes that hierarchy, the template steps, and the four shapes.
The Hierarchy¶
Application (interface, tentackle-pdo)
│ SessionProvider + DomainContextProvider
AbstractApplication (abstract, tentackle-pdo)
│ template methods + start/stop
┌─────────────┴──────────────┐
AbstractClientApplication AbstractServerApplication (tentackle-persistence)
(tentackle-pdo) │ pools, container detection
│ ┌────────┴────────┐
ConsoleApplication ServerApplication WebApplication<T> (tentackle-web)
(tentackle-pdo) (TRIP server) (servlet container)
│
DesktopApplication<C> (tentackle-fx-rdc)
│
UpdatableDesktopApplication<C> (tentackle-fx-rdc-update)
| Class | Module | Shape |
|---|---|---|
Application |
tentackle-pdo | The contract and the singleton registry. |
AbstractApplication |
tentackle-pdo | The template: properties, session, start/stop. |
AbstractClientApplication |
tentackle-pdo | Adds updateSessionInfoAfterLogin(). |
ConsoleApplication |
tentackle-pdo | Non-interactive client: daemons, batch jobs, tools. |
AbstractServerApplication |
tentackle-persistence | Serves other tiers: pools, connection manager. |
ServerApplication |
tentackle-persistence | Adds the TRIP listener. |
WebApplication<T> |
tentackle-web | A server bridged to a web framework. |
DesktopApplication<C> |
tentackle-fx-rdc | Interactive JavaFX client. |
Note that DesktopApplication is a client, not a server, and WebApplication
is a server — a web application talks to browsers, but from Tentackle's point of
view it serves sessions to other code, exactly like a TRIP server.
Application — The Contract¶
Application extends SessionProvider and DomainContextProvider, which is what
makes it the root of context propagation: the application singleton holds the
context every PDO created without an explicit one inherits. See
The DomainContextProvider.
One per JVM. register() installs the instance and throws if a different one is
already registered; unregister() removes it. Application.getInstance() returns it
from anywhere — or null before startup:
The interface also exposes the application's identity (getName(), getVersion(),
getCreationTime()), its CommandLine and SessionInfo, property accessors, and
the lifecycle pair start(String[]) / stop(int, Throwable) with no-arg convenience
defaults.
One method has no default and no implementation in any framework class — every application must supply it:
The framework knows a user only as an id; mapping that id to the application's own user PDO is application knowledge. The generated project implements it as a cached lookup:
@Override
public User getUser(DomainContext context, long userId) {
return Pdo.create(User.class, context).selectCached(userId);
}
Two predicates let framework code branch on the tier without knowing the class:
isServer() (false by default, true in AbstractServerApplication) and
isInteractive() (false by default, true in DesktopApplication).
AbstractApplication — The Template¶
Construction¶
The constructor takes a name and a version, either of which may be null:
filterName() then falls back to the class's simple name — walking up past anonymous
inner classes, which is what makes unnamed applications in unit tests still report
something useful — and filterVersion() falls back to "1.0.0-SNAPSHOT". It also
records creationTime and installs a default uncaught-exception handler that logs
rather than letting threads die silently.
start(args)¶
@Override
public void start(String[] args) {
cmdLine = new CommandLine(args);
setProperties(cmdLine.getOptionsAsProperties());
try {
startup();
}
catch (RuntimeException e) {
stop(1, e); // a failed startup is a stop, not a stack trace
}
}
Command-line options become application properties.
CommandLine
parses --flag and --key=value; everything else is a plain argument. So
--url=... --user=scott --statistics arrives as three properties.
startup() is abstract — it is the one step AbstractApplication refuses to
guess, because the order is exactly what distinguishes the tiers.
The steps¶
| Step | Default behavior |
|---|---|
initialize() |
Applies properties, initializes scripting, logs the discovered module hooks. The first step, always. |
login() |
(subclass) Creates the SessionInfo, opens the session, makes it current, sets the root domain context. |
configure() |
Calls configureModificationTracker(), configurePreferences(), configureSecurityManager(). Runs after the connection exists. |
finishStartup() |
Activates statistics if asked, starts the modification tracker, registers its emergency-shutdown hook. |
cleanup() |
Terminates PDO helper threads and closes the session. |
The split between configure() and finishStartup() matters: configure() prepares
singletons that need a live session, finishStartup() starts the threads that use
them. Subclasses override individual steps rather than the sequence.
initializeScripting() is a small but load-bearing default: if no default scripting
language is set and exactly one provider is on the module path, it becomes the
default. Most applications therefore never configure
scripting at all.
Well-known properties¶
applyProperties() interprets a set of properties that need no application code:
| Property | Effect |
|---|---|
notracker |
Don't start the modification tracker — and disable PDO caching globally, since a cache without invalidation would go stale. |
nosecurity |
Disable the security manager. |
statistics |
Collect PDO/operation method invocation statistics and TRIP/SQL/Transaction statistics (if server). Recommended during development. |
scripting=<lang> |
Set the default scripting language. |
locale=<tag> |
Set the default locale; falls back to the user preference, and warns if unsupported. |
readonlyprefs |
Silently ignore preference writes. |
systemprefs |
System preferences only (forced on servers). |
noprefsync |
Turn off preference auto-sync between JVMs. |
Values are run through StringHelper.evaluate(...), so they may reference system
properties. A property named SYSTEM_foo or ^foo is set as system property foo
instead of being stored — the log deliberately records only the key at INFO level,
never the value, since it may be a password. Command-line properties take precedence
over properties from a file.
A small shell script src/scripts/collect_statistics.sh is provided to collect
statistics from log files.
stop(exitValue, throwable)¶
Guarded against re-entry by a stopping flag, so the shutdown path can be reached
from several directions at once without running twice. It logs, runs cleanup()
(catching and logging failures rather than letting them mask the exit), unregisters,
and finally calls System.exit(exitValue) — but only if
isSystemExitNecessaryToStop() says so. That hook is what lets a server running
inside a container shut down without killing the container's JVM.
Shape 1 — Console¶
ConsoleApplication is the
straight-through case, and the clearest illustration of the template:
@Override
protected void startup() {
register(); // only one application at a time
initialize(); // environment
login(); // connect to database or server
configure(); // tracker, preferences, security
finishStartup(); // start the tracker
}
Its login() builds a SessionInfo from --user / --password / --backend, or
takes --url, --user and --password directly from the command line when both a
URL and a user are given, bypassing the properties file. It then opens the session,
makes it current, and installs the root domain context.
Despite the name, ConsoleApplication is the base for anything non-interactive:
daemons, batch jobs, migration tools. The generated project's daemon extends it and
overrides two steps:
public class MyAppDaemon extends ConsoleApplication {
public MyAppDaemon() { super("MyAppDaemon", Version.RELEASE); }
@Override
protected void configurePreferences() {
super.configurePreferences();
User user = PdoUtilities.getInstance().getUser(getDomainContext());
PersistedPreferencesFactory.getInstance().setSystemOnly(user.isSystemPreferencesOnly());
}
@Override
protected void finishStartup() {
latestMessageId = on(Message.class).selectMaxId();
Pdo.listen(this::logMessages, Message.class);
super.finishStartup();
}
public static void main(String[] args) { new MyAppDaemon().start(args); }
}
Note the shape: call super, add your own work. configurePreferences() can already
reach the logged-in user, because configure() runs after login().
Shape 2 — Server¶
AbstractServerApplication
uses the same startup() order as the console case but hardens and extends the steps:
isServer()returns true, andsetSessionInfo()marks the info immutable and refuses to replace it once set — a running server must not have its identity changed underneath it.configurePreferences()forces system-only preferences;configureModificationTracker()drops the poll interval to 1000 ms.initializeScripting()additionally callsvalidateValidators(), which walks every PDO class and touches every validator, compiling all validation scripts at startup. A server pays that cost once, at a predictable moment, instead of making the first user of each entity wait.finishStartup()creates the pools: aMultiUserSessionPoolif the server is itself a client of another server (the cascade), otherwise its ownConnectionManagerand aSessionPool. The local pool hands out sessions with a wipedSessionInfo— no user id, no name, no password — forcing the caller to authenticate rather than inheriting the server's identity.- The constructor calls
detectContainer(), a JNDI lookup ofjava:comp/env. If it succeeds,isSystemExitNecessaryToStop()returns false andcleanup()also deregisters JDBC drivers loaded by the application's classloader, avoiding the classic container redeploy leak.
ServerApplication
adds remoting on top: it initializes the LockManager when connected directly to a
database, creates the TripServer in finishStartup(), starts it as the final act
of startup(), and on shutdown closes every open remote session before stopping the
listener. Its constructor takes the RemoteDbConnectionImpl subclass that defines the
server's remote API.
public class MyAppServer extends ServerApplication {
public MyAppServer() {
super("MyAppServer", Version.RELEASE, MyAppRemoteDbConnectionImpl.class);
}
@Override
protected void finishStartup() {
super.finishStartup();
createUpdateService();
}
}
Shape 3 — Desktop¶
Two different Application types¶
This is the one genuinely confusing part of the hierarchy, and worth stating plainly:
| Type | Really is | Role |
|---|---|---|
DesktopApplication<C> |
extends AbstractClientApplication |
The Tentackle application. The singleton, the session, the context. |
FxApplication |
extends javafx.application.Application |
The JavaFX application. The FX lifecycle entry point. |
FxApplication is not part of the Tentackle Application hierarchy at all — it is
JavaFX's. The two hold references to each other: FxApplication.start(stage) calls
DesktopApplication.setFxApplication(this), and FxApplication.stop() (which JavaFX
invokes when the last window closes) forwards to Application.getInstance().stop(...).
The interleaved startup¶
A desktop client cannot run the template straight through, because login() needs a
password that does not exist yet. So DesktopApplication.startup() runs only the
steps that need no connection, then hands control to JavaFX:
@Override
protected void startup() {
register();
initialize();
Application.launch(getApplicationClass(), getCommandLine().getArgs()); // javafx.application.Application
}
launch(...) blocks until the FX application exits, so startup() does not return
for the life of the client. The remaining steps run later, driven by the login view:
main()
└─ new MyAppFxClient().start(args)
└─ AbstractApplication.start → startup()
├─ register(), initialize()
└─ javafx.application.Application.launch(LoginApplication.class)
└─ [FX thread] FxApplication.start(stage)
├─ desktopApplication.setFxApplication(this)
├─ registerUncaughtExceptionHandler()
└─ LoginApplication.startApplication(stage) → shows the login view
└─ user authenticates
└─ [background thread] DesktopApplication.login(view, sessionInfo)
├─ createSession() / createDomainContext()
├─ configure() "CONFIGURE APPLICATION..."
├─ Fx.load(mainControllerClass) "LOADING GUI..."
└─ [FX thread] show main stage
└─ on WINDOW_SHOWN → finishStartup(), hide login view
Two details are worth pulling out. First, login() runs on a background service
thread, not the FX thread, so the login view can keep repainting its progress bar
while the connection is established — which is why DesktopApplication overrides
configurePreferences(), configureSecurityManager() and
configureModificationTracker() purely to report progress before delegating to
super. Second, finishStartup() fires on WINDOW_SHOWN of the main stage: the
login window is not dismissed until the real window is up, so a failure late in
startup still leaves the user somewhere sensible.
DesktopApplication also gives the modification tracker its own cloned session,
grouped with the main one, so tracker polling does not contend with the UI's session.
And via classMapped(...) it pre-compiles each PDO's validators in the background the
first time the class is used — the interactive counterpart to the server's eager
validateValidators().
getApplicationClass() returns the FxApplication to launch, defaulting to
LoginApplication;
override it to supply a custom login view. See
rdc.md → Application lifecycle
for the login package itself, and
fx-rdc-update.md
for UpdatableDesktopApplication, which adds self-update.
Shape 4 — Web¶
WebApplication<T>
extends AbstractServerApplication — inheriting the pools, the container detection
and the immutable session info — and adds a cache mapping the web framework's session
key (type T) to a Tentackle SessionInfo. It slots into the template at three
points: startup() creates the cache before super.startup(), finishStartup()
starts it, cleanup() terminates it. The request-scoped getSession(key) /
putSession(session) pair is the bridge itself. See
web.md.
Shutdown¶
stop() is reachable from several directions, and they converge:
- the application calls
stop()itself; - startup throws, and
start()callsstop(1, e); - JavaFX closes the last window →
FxApplication.stop()→Application.stop(...); - the modification tracker terminates — cleanly (
stop(0, null)) or unexpectedly, which logs*** emergency shutdown ***and exits with 2; - a server sends a
LogoutEvent, whose defaultLogoutEventHandlerterminates the modification tracker — and therefore, by the previous rule, the application.LogoutEventis aMasterSerialEventmarked@TripReferencable(NEVER); a client that needs to shut down differently replaces the handler.
Each cleanup() extends its parent's: the desktop client logs statistics and
terminates the FX toolkit, the TRIP server closes remote sessions and stops the
listener, the server shuts down pools and the connection manager, and
AbstractApplication closes the session last.
Writing Your Own¶
- Pick the base class from the table above.
- Provide a constructor passing a name and version (
nullfor defaults). - Implement
getUser(context, userId)— the one mandatory method. - Override individual template steps, always calling
super— notstartup(), unless you are defining a genuinely new tier shape. - Call
start(args)frommain().
For desktop clients, additionally implement getMainControllerClass() and,
optionally, getApplicationClass() and configureMainStage(...).
Summary¶
| Concern | How the hierarchy handles it |
|---|---|
| Singleton | register() / unregister() guarantee one application per JVM; getInstance() reaches it from anywhere. |
| Root context | Application is a SessionProvider + DomainContextProvider; every context-less PDO inherits from it. |
| Uniform steps | initialize → login → configure → finishStartup, overridden individually, assembled by startup(). |
| Configuration | Command-line options become properties; well-known keys need no code; SYSTEM_/^ set system properties. |
| Tier differences | Servers add pools and TRIP; desktop clients interleave with the FX lifecycle; web applications bridge to a container. |
| Shutdown | One re-entrant stop(); isSystemExitNecessaryToStop() keeps containers alive; cleanup chains through the hierarchy. |