Skip to main content

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}