platform_core/app_starter.rs
1//
2// Copyright 2018-2026 Accenture Technology
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17//! Rust port of the Java application lifecycle — `AutoStart` + `AppStarter`
18//! (`org.platformlambda.core.system`) and the `EntryPoint` contract
19//! (`org.platformlambda.core.models.EntryPoint`).
20//!
21//! Startup order is the Java sequence exactly:
22//!
23//! 1. **Essential services** (the Java `EssentialServiceLoader`, sequence 0 —
24//! reserved for the framework): here, the elastic store's housekeeping
25//! (holding-area liveness marker, keep-alive refresh, expired-store scan).
26//! 2. **Before-application hooks** (`@BeforeApplication`), ordered by
27//! `sequence` (1–999, lower first; user code conventionally uses 3–999) —
28//! validation/compilation work before anything is registered or served
29//! (e.g. event-script's `CompileFlows` in Java). A failure **aborts
30//! startup** (Java parity: fatal).
31//! 3. **Preload** (`@PreLoad`): composable functions are registered and bound
32//! to their routes — callable via `PostOffice` from this point on.
33//! 4. The HTTP server would start here when `rest.automation=true` — REST
34//! automation is a later increment; the flag currently logs a notice.
35//! 5. **Main applications** (`@MainApplication`), ordered by `sequence`.
36//! A missing main application is an error (Java parity).
37//!
38//! Java discovers all five phases by classpath annotation scanning; Rust has
39//! no runtime scanning (design D6), so [`AppStarter`] is an explicit
40//! **builder** — a `#[preload]`-style proc-macro can add the ergonomic layer
41//! in a later increment. Java's `AutoStart` global run-once guard is not
42//! ported: the builder is consumed by [`run`](AppStarter::run), and the
43//! framework phases behind it are idempotent (deliberate divergence,
44//! test-friendly).
45
46use std::sync::Arc;
47
48use async_trait::async_trait;
49
50use crate::function::{AppError, ComposableFunction};
51use crate::platform::{FunctionOptions, Platform};
52use crate::util::app_config_reader::AppConfigReader;
53use crate::util::{elastic_queue, managed_cache};
54
55/// Highest allowed hook sequence (Java `MAX_SEQ`); larger values clamp to it.
56const MAX_SEQ: u32 = 999;
57
58/// The application entry-point contract (Java `EntryPoint`): implemented by
59/// both before-application hooks and main applications.
60#[async_trait]
61pub trait EntryPoint: Send + Sync {
62 async fn start(&self, args: &[String]) -> Result<(), AppError>;
63}
64
65/// The lifecycle builder (Java `AutoStart`/`AppStarter`, explicit-registration
66/// form). Build the application declaratively, then [`run`](Self::run) it:
67///
68/// ```ignore
69/// AppStarter::new()
70/// .before_application(5, Arc::new(CompileCheck))
71/// .preload("greeting.demo", TypedAdapter::arc(Greetings), 10)
72/// .main_application(1, Arc::new(MainApp))
73/// .run(std::env::args().collect())
74/// .await?;
75/// ```
76#[derive(Default)]
77pub struct AppStarter {
78 before: Vec<(u32, Arc<dyn EntryPoint>)>,
79 preloads: Vec<(String, Arc<dyn ComposableFunction>, usize, FunctionOptions)>,
80 mains: Vec<(u32, Arc<dyn EntryPoint>)>,
81}
82
83impl AppStarter {
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 /// Register a before-application hook (Java `@BeforeApplication`).
89 /// `sequence` orders execution (lower first, clamped to 999; 0 is reserved
90 /// for the framework's essential services — user code uses 1–999,
91 /// conventionally 3–999).
92 pub fn before_application(mut self, sequence: u32, hook: Arc<dyn EntryPoint>) -> Self {
93 self.before.push((sequence.min(MAX_SEQ), hook));
94 self
95 }
96
97 /// Register a composable function at startup (Java `@PreLoad`).
98 pub fn preload(
99 self,
100 route: &str,
101 function: Arc<dyn ComposableFunction>,
102 instances: usize,
103 ) -> Self {
104 self.preload_with_options(route, function, instances, FunctionOptions::default())
105 }
106
107 /// Register a composable function with explicit options (Java `@PreLoad`
108 /// combined with `@ZeroTracing` and/or `@EventInterceptor`).
109 pub fn preload_with_options(
110 mut self,
111 route: &str,
112 function: Arc<dyn ComposableFunction>,
113 instances: usize,
114 options: FunctionOptions,
115 ) -> Self {
116 self.preloads
117 .push((route.to_string(), function, instances, options));
118 self
119 }
120
121 /// Register a main application (Java `@MainApplication`); `sequence`
122 /// orders execution when there is more than one (lower first).
123 pub fn main_application(mut self, sequence: u32, entry: Arc<dyn EntryPoint>) -> Self {
124 self.mains.push((sequence.min(MAX_SEQ), entry));
125 self
126 }
127
128 /// Run the lifecycle (Java `AutoStart.main` → `AppStarter.main`).
129 /// Must be called within a Tokio runtime.
130 pub async fn run(self, args: Vec<String>) -> Result<(), AppError> {
131 // -Dkey=value runtime arguments (the JVM -D analog) become process
132 // overrides — idempotent, in case logging::init already loaded them
133 crate::util::overrides::load_runtime_args();
134 let config = AppConfigReader::get_instance();
135 log::info!(
136 "Starting application {} (platform-core v{})",
137 Platform::name(),
138 env!("CARGO_PKG_VERSION")
139 );
140 // 1. essential services (sequence 0, framework-reserved — the Java
141 // EssentialServiceLoader, which registers async.http.request (500),
142 // temporary.inbox (500) and distributed.tracing (1) at the HIGHEST
143 // startup priority, before any user hook or preload): elastic-store
144 // housekeeping, the RPC reply listener, the telemetry sink (a
145 // deliberate singleton — serializes trace-record processing to
146 // preserve ordering; the event system buffers bursts), the event
147 // API, actuators, no.op and the HTTP client (all idempotent)
148 elastic_queue::start_housekeeping();
149 managed_cache::start_housekeeping();
150 let platform = Platform::get_instance();
151 // the RPC reply listener is registered at Platform construction (so
152 // every registry has it from birth); assert it here like the Java
153 // loader does. Reply dispatch is DIRECT on the sender's runtime (see
154 // RESERVED_ENGINE_ROUTES), so the entry is the addressable identity
155 if !platform.has_route(crate::inbox::TEMPORARY_INBOX) {
156 if let Err(e) = platform.register_with_options(
157 crate::inbox::TEMPORARY_INBOX,
158 Arc::new(crate::inbox::TemporaryInbox),
159 500,
160 crate::platform::FunctionOptions {
161 zero_traced: true,
162 interceptor: false,
163 private: true,
164 },
165 ) {
166 if !platform.has_route(crate::inbox::TEMPORARY_INBOX) {
167 return Err(e);
168 }
169 }
170 }
171 if !platform.has_route(crate::telemetry::DISTRIBUTED_TRACING) {
172 if let Err(e) = platform.register_private(
173 crate::telemetry::DISTRIBUTED_TRACING,
174 std::sync::Arc::new(crate::telemetry::Telemetry::new(&platform)),
175 1,
176 ) {
177 // a concurrent lifecycle run may have won the race — benign;
178 // anything else is a real startup failure
179 if !platform.has_route(crate::telemetry::DISTRIBUTED_TRACING) {
180 return Err(e);
181 }
182 }
183 }
184 // the event-over-http service (Java EssentialServiceLoader): reached
185 // through REST automation's default /api/event route. Registered
186 // PRIVATE — it is never itself a remote target. 250 workers (Java
187 // parity: the endpoint fans out concurrent RPC forwards). An event
188 // INTERCEPTOR (Java @EventInterceptor parity): replies are sent
189 // manually, so the streaming relay can withhold its auto-reply.
190 if !platform.has_route(crate::automation::EVENT_API_SERVICE) {
191 let event_api_options = crate::platform::FunctionOptions {
192 zero_traced: false,
193 interceptor: true,
194 private: true,
195 };
196 if let Err(e) = platform.register_with_options(
197 crate::automation::EVENT_API_SERVICE,
198 Arc::new(crate::automation::EventApiService::new(&platform)),
199 250,
200 event_api_options,
201 ) {
202 if !platform.has_route(crate::automation::EVENT_API_SERVICE) {
203 return Err(e);
204 }
205 }
206 }
207 // actuator endpoints (Java EssentialServiceLoader parity): /info /env
208 // /health /livenessprobe, exposed via REST automation's default routes
209 if !platform.has_route(crate::actuator::INFO_ACTUATOR) {
210 use crate::actuator::{ActuatorContext, ActuatorKind, ActuatorServices};
211 let context = ActuatorContext::new(&platform);
212 let actuators = [
213 (crate::actuator::INFO_ACTUATOR, ActuatorKind::Info),
214 (crate::actuator::ROUTES_ACTUATOR, ActuatorKind::Routes),
215 (crate::actuator::ENV_ACTUATOR, ActuatorKind::Env),
216 (crate::actuator::HEALTH_ACTUATOR, ActuatorKind::Health),
217 (crate::actuator::LIVENESS_ACTUATOR, ActuatorKind::Liveness),
218 ];
219 // one ops knob for the whole family (see actuator_instances)
220 let instances = crate::actuator::actuator_instances(config);
221 for (route, kind) in actuators {
222 if let Err(e) = platform.register_private(
223 route,
224 Arc::new(ActuatorServices::new(kind, context.clone())),
225 instances,
226 ) {
227 if !platform.has_route(route) {
228 return Err(e);
229 }
230 }
231 }
232 }
233 // the no-op echo function (Java NoOpFunction, a platform built-in
234 // for event scripts): echoes headers and body; instance count is
235 // overridable via worker.instances.no.op (Java envInstances parity)
236 if !platform.has_route("no.op") {
237 let config = AppConfigReader::get_instance();
238 let no_op_instances = config
239 .get_property("worker.instances.no.op")
240 .and_then(|value| value.parse::<usize>().ok())
241 .unwrap_or(500);
242 if let Err(e) = platform.register_private(
243 "no.op",
244 Arc::new(crate::function::NoOpFunction),
245 no_op_instances,
246 ) {
247 if !platform.has_route("no.op") {
248 return Err(e);
249 }
250 }
251 }
252 // the Async HTTP client (Java EssentialServiceLoader parity);
253 // tolerate a concurrent registration like the actuators above
254 if !platform.has_route(crate::automation::ASYNC_HTTP_REQUEST) {
255 if let Err(e) = platform.register_with_options(
256 crate::automation::ASYNC_HTTP_REQUEST,
257 Arc::new(crate::automation::http_client::AsyncHttpClientService::new(
258 &platform,
259 )),
260 500,
261 FunctionOptions {
262 zero_traced: false,
263 interceptor: true,
264 // Java EssentialServiceLoader: registerPrivate
265 private: true,
266 },
267 ) {
268 if !platform.has_route(crate::automation::ASYNC_HTTP_REQUEST) {
269 return Err(e);
270 }
271 }
272 }
273 // 2. before-application hooks, ordered by sequence (stable sort keeps
274 // registration order within a sequence — Java scan order analog)
275 let mut before = self.before;
276 before.sort_by_key(|(sequence, _)| *sequence);
277 for (sequence, hook) in before {
278 hook.start(&args).await.map_err(|e| {
279 AppError::new(
280 e.status(),
281 format!(
282 "BeforeApplication (sequence {sequence}) failed: {}",
283 e.message()
284 ),
285 )
286 })?;
287 }
288 // 3. preload: bind composable functions to their routes
289 for (route, function, instances, options) in self.preloads {
290 platform.register_with_options(&route, function, instances, options)?;
291 log::info!(
292 "{route} with {instances} instance{} started",
293 if instances == 1 { "" } else { "s" }
294 );
295 }
296 // 4. REST automation: the HTTP protocol boundary. Websocket services
297 // from the `#[websocket_service]` inventory register their URL
298 // paths first (Java `prepareWebsocketServices`); the server starts
299 // when REST automation is enabled OR any websocket service exists
300 // (Java `startHttpServerIfAny` parity).
301 for entry in inventory::iter::<crate::registry::WsServiceEntry> {
302 // Java @OptionalService: skip a conditionally-registered websocket
303 // service when its configuration condition does not hold.
304 if !crate::util::feature::is_required(entry.optional_service, config) {
305 log::info!(
306 "Skip optional websocket service /{}/{} (condition: {})",
307 entry.namespace,
308 entry.name,
309 entry.optional_service.unwrap_or_default()
310 );
311 continue;
312 }
313 if validate_ws_service_name(entry.name) && validate_ws_service_name(entry.namespace) {
314 crate::automation::ws_server::register_ws_service_with_namespace(
315 entry.namespace,
316 entry.name,
317 entry.factory,
318 );
319 } else {
320 log::error!(
321 "Unable to load websocket service /{}/{} - not a valid service name",
322 entry.namespace,
323 entry.name
324 );
325 }
326 }
327 let serve_http = config.get_property_or("rest.automation", "false") == "true"
328 || crate::automation::ws_server::has_ws_services();
329 if serve_http {
330 crate::automation::start_http_server(&platform).await?;
331 }
332 // 5. main applications, ordered by sequence
333 if self.mains.is_empty() {
334 // Java: "Missing MainApplication ... Did you forget to annotate your main module?"
335 return Err(AppError::new(
336 400,
337 "Missing main application - did you forget to add one with main_application()?",
338 ));
339 }
340 let mut mains = self.mains;
341 mains.sort_by_key(|(sequence, _)| *sequence);
342 for (sequence, entry) in mains {
343 entry.start(&args).await.map_err(|e| {
344 AppError::new(
345 e.status(),
346 format!(
347 "MainApplication (sequence {sequence}) failed: {}",
348 e.message()
349 ),
350 )
351 })?;
352 }
353 Ok(())
354 }
355}
356
357/// The one-line application entry point (Java `AutoStart.main(args)`): loads
358/// the `-D` runtime overrides, installs the logger, **collects every
359/// annotated item from the link-time inventory** (`#[preload]`,
360/// `#[before_application]`, `#[main_application]` — the classpath-scanning
361/// analog), and runs the lifecycle. A standalone `fn main()` uses
362/// [`AutoStart::run`], which then keeps serving until Ctrl-C; an embedder
363/// (tests, an existing async context) calls [`AutoStart::main`], which returns
364/// once the app is booted (the HTTP/websocket accept loop runs in the
365/// background).
366pub struct AutoStart;
367
368impl AutoStart {
369 /// Build a Tokio runtime, boot the application, then — when serving (REST
370 /// automation on, or any websocket service registered) — stay alive until
371 /// Ctrl-C (Java: the JVM stays up on non-daemon threads). This is the whole
372 /// `fn main()` body; the `auto_start_main!` macro wraps exactly this plus
373 /// the app's resource root.
374 pub fn run() -> Result<(), AppError> {
375 let runtime = tokio::runtime::Runtime::new()
376 .map_err(|e| AppError::new(500, format!("Unable to start runtime: {e}")))?;
377 runtime.block_on(async {
378 Self::main(std::env::args().collect()).await?;
379 // HTTP/websocket serving: stay alive until Ctrl-C. This blocking
380 // wait lives here (the standalone-process entry point), NOT in
381 // `main`, so an embedder that awaits `main` gets control back once
382 // the app is booted instead of hanging on the signal.
383 let config = AppConfigReader::get_instance();
384 if config.get_property_or("rest.automation", "false") == "true"
385 || crate::automation::ws_server::has_ws_services()
386 {
387 log::info!("Application running - press Ctrl-C to stop");
388 let _ = tokio::signal::ctrl_c().await;
389 } else {
390 // give fire-and-forget telemetry a beat to be logged before exit
391 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
392 }
393 crate::util::elastic_queue::shutdown_cleanup();
394 Ok(())
395 })
396 }
397
398 /// The async lifecycle (must run within a Tokio runtime): run every
399 /// before-application hook, bind the preloads, start the HTTP/websocket
400 /// server when serving — then **return** (the accept loop keeps running as
401 /// a background task). Booting the engine hands control back to the caller;
402 /// a standalone process uses [`AutoStart::run`] to serve until Ctrl-C.
403 ///
404 /// Runs only once per process (Java parity: `AutoStart.started` is an
405 /// `AtomicBoolean`) — repeated execution is a no-op.
406 pub async fn main(args: Vec<String>) -> Result<(), AppError> {
407 static STARTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
408 if STARTED.swap(true, std::sync::atomic::Ordering::SeqCst) {
409 return Ok(());
410 }
411 crate::util::overrides::load_runtime_args();
412 crate::logging::init();
413 let config = AppConfigReader::get_instance();
414 let mut starter = AppStarter::new();
415 for entry in inventory::iter::<crate::registry::BeforeAppEntry> {
416 // Java @OptionalService: skip a conditionally-run before-application hook
417 if !crate::util::feature::is_required(entry.optional_service, config) {
418 log::info!(
419 "Skip optional before-application (condition: {})",
420 entry.optional_service.unwrap_or_default()
421 );
422 continue;
423 }
424 starter = starter.before_application(entry.sequence, (entry.factory)());
425 }
426 // Java yaml.preload.override: a config-driven transform over the
427 // collected #[preload] set, applied between inventory collection and
428 // registration — rename / fan out / re-tune instances without
429 // recompiling (the boot sequence's "override" step)
430 let preload_overrides = crate::preload_override::preload_override(config);
431 for entry in inventory::iter::<crate::registry::PreloadEntry> {
432 // Java @OptionalService: skip a conditionally-registered route when
433 // its configuration condition does not hold (Java `Feature`).
434 if !crate::util::feature::is_required(entry.optional_service, config) {
435 log::info!(
436 "Skip optional {} (condition: {})",
437 entry.route,
438 entry.optional_service.unwrap_or_default()
439 );
440 continue;
441 }
442 // Java envInstances: the instance count may come from configuration.
443 // Resolved BEFORE the override applies (Java processPreload order),
444 // so the resolved value is the "old" count in the override's log.
445 let instances = entry
446 .env_instances
447 .and_then(|key| config.get_property(key))
448 .and_then(|value| value.parse::<usize>().ok())
449 .unwrap_or(entry.instances);
450 let (routes, instances) =
451 crate::preload_override::apply(&preload_overrides, entry.route, instances);
452 // a comma-separated route value declares ALIASES: every name
453 // registers the SAME function object with the same instance
454 // count and visibility (Java AppStarter splits @PreLoad.route
455 // and registers one instance for all names) — an override's
456 // replacement route set fans out the same way
457 let function = (entry.factory)();
458 for route in &routes {
459 starter = starter.preload_with_options(
460 route,
461 function.clone(),
462 instances,
463 FunctionOptions {
464 zero_traced: entry.zero_tracing,
465 interceptor: entry.interceptor,
466 private: entry.is_private,
467 },
468 );
469 }
470 }
471 for entry in inventory::iter::<crate::registry::MainAppEntry> {
472 // Java @OptionalService: skip a conditionally-run main application
473 if !crate::util::feature::is_required(entry.optional_service, config) {
474 log::info!(
475 "Skip optional main-application (condition: {})",
476 entry.optional_service.unwrap_or_default()
477 );
478 continue;
479 }
480 starter = starter.main_application(entry.sequence, (entry.factory)());
481 }
482 starter.run(args).await?;
483 // The app is booted; the HTTP/websocket accept loop (if serving) runs
484 // as a background task. Return control to the caller — `AutoStart::run`
485 // is what blocks a standalone process alive until Ctrl-C.
486 Ok(())
487 }
488}
489
490/// Java `validServiceName` for websocket paths: lowercase letters, digits,
491/// '.', '-', '_'.
492fn validate_ws_service_name(name: &str) -> bool {
493 !name.is_empty()
494 && name.bytes().all(|b| {
495 b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'-' | b'_')
496 })
497}