Skip to content

Commit 65ae4d8

Browse files
shai-almogclaude
andauthored
Backend: Spring-style beans, transactions, scheduling, metrics and MCP, resolved at build time (#5908)
* Backend: Spring-style beans, transactions, scheduling, metrics and MCP, resolved at build time The backend gains Spring's programming model under com.codename1.backend.annotations -- @Service/@Component/@Repository, @Autowired, @Value, @ConfigurationProperties, @Bean, scopes, profiles and conditions, @Transactional, @Async, @Scheduled, @ManagedResource/@Timed/@Counted and @McpTool -- with every decision made by the build: a generated BackendWiring constructs and injects the beans with plain code, and aspects are woven into the annotated methods. No container, scan, proxy or reflection at run time. Runtime: thread-bound transactions that the pool, daos and sessions join, a cron and fixed-rate scheduler with an optional database lock, task executors including fd-less virtual-thread tasks with a per-host wake pipe, HTTP sessions (memory or JDBC store), OTLP metrics and management endpoints, and an MCP endpoint with development tools. Also fixes the sticky virtual-thread yield reason (yieldNow from a handler hung), a handler taking HttpServer.Request being refused, and adds the backend and full-stack references to the generated agent skill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: fix review findings in sessions, tasks, MCP, metrics and wiring - Sessions: the session cookie goes on a copy of the handler's Response, never into it (it may be a shared constant); cn1.session.secure refuses anything but auto/true/false. - TaskExecutor: virtual submissions are refused after shutdown and counted active before the hand-off; @Async accessors re-fetch a shut-down executor. - MCP: only loopback or listed origins pass (the Host match let DNS rebinding through); byte/short tool arguments are range-checked; backend_call uses https against a TLS server. - Request metrics are recorded in a finally, so failures count and the route label is cleared. - Factory beans inherit their configuration class's @Profile and @ConditionalOnProperty; request-scoped beans' destroyMethod runs. - Scheduler lock: an INSERT failure with no existing row is rethrown. - OTLP histogram bucket_counts stays fixed64 (per metrics.proto), now held by a test decoding with the generated opentelemetry-proto classes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: per-server sessions and registries, bean lifecycle fixes; split the guide Review fixes: - Sessions, MCP tools and managed beans are per server (Backend.getSessions, Environment.registerTool/registerManaged, Builder.mcpTool); two servers in one process no longer share cookies, sessions or tools. - @SessionScope beans are kept by the server for the session's lifetime and destroyed on invalidate, expiry and stop; a negative session timeout fails. - Transactions: no process-wide default pool; a NESTED method before the first statement sets its savepoint after BEGIN. setRollbackOnly in the method that began the transaction rolls back without throwing. - Backend.stop runs destroy callbacks once; a failed start destroys built beans; management routes precede application handlers; session-store failures are logged as 500s; metrics shutdown is bounded by its timeout. - Weaving keeps synchronized on the body, so a synchronized @Async method holds its monitor where it runs. - Injection points and lifecycle methods inherited from base classes are wired; @ConditionalOnMissingBean matches the bean's exposed types and takes explicit ones. Docs: the backend chapter is split into nine chapters (web, beans, data and transactions, sessions, scheduling, observability, MCP, operations) with diagrams and compiled samples. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: per-server executors, bounded shutdown, lifecycle and DST fixes - Executors belong to the server that opened them: request, task, scheduler and start-up threads carry it, stopping one server no longer shuts down another's, and generated @Async code looks the executor up per call. At the shutdown deadline queued tasks are dropped rather than run against destroyed beans, and running ones are interrupted; a virtual task a host cannot run falls back to its own executor. - Every server applies cn1.session.* (handler-only ones sent a TLS session cookie without Secure); a failing request still stores its session so its session beans are kept or destroyed. - Request beans are destroyed while their request is still current; @PreDestroy runs subclass before superclass; factory beans run inherited lifecycle methods. - Health reports STARTING until the start-up hook returns. - A cron time inside a DST gap is skipped instead of firing an hour late. - The metrics exporter can be reopened; histograms copy and validate their bounds and labels. - A managed resource must be a singleton and cannot overload operations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: drain virtual tasks at shutdown, harden JDBC sessions and wiring - A stopping host keeps resuming its background virtual threads through the drain window instead of exiting with them yielded; stop() waits for that (except on the host running a handler that called stop), and a task that overruns is freed and reported. - JDBC sessions: a stale copy of an invalidated session is not written back; the stored last use is refreshed at min(1 min, timeout/4) and expiry allows that much grace; session-scoped beans are shared per session id across copies, created under one lock. - NESTED setRollbackOnly rolls back only its savepoint. - Built-in server gauges sum over running servers and drop stopped ones. - Generated wiring clears every bean field before a restart; factory beans step aside with a @ConditionalOnMissingBean configuration; scoped-bean stand-ins forward methods inherited from library classes; a managed-resource-only module gets an application. - Runtime cron parsing refuses dates no allowed month has; MCP long arguments are range-checked; unconfirmed backend_sql runs in an enforced read-only transaction, one statement, no value-setting pragmas. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: finish abandoned tasks, order request beans before the session save - A virtual task freed at shutdown now releases its executor's active count and fails its Future, instead of leaving both hanging. - Request-scoped beans are destroyed before the session is stored, so a @PreDestroy that changes or starts the session is saved and its cookie sent. - Loading a session refreshes its shared beans' last use, so a purge cannot destroy beans a long request is still using. - Scheduler.trigger refuses after stop and restores the job if the executor rejects it. - backend_sql runs only allow-listed read pragmas unconfirmed (either spelling), and closes a connection whose rollback failed. - The build warns about public methods a class-level @Transactional or @Async class inherits, which the annotation does not cover (as in Spring). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: roll back failed statements as Spring does; per-server logs and metrics - Database failures are DataAccessException (an IOException), and the default @Transactional rule rolls back for it beside RuntimeException and Error: in Spring a failed statement is an unchecked DataAccessException and rolls back. Other checked exceptions still commit, as Spring's rule says. - JDBC sessions save by optimistic merge: a versioned row, only this request's changed attributes applied, last use only moving forward; the purge no longer overflows PostgreSQL INTEGER for long timeouts. - Request logs are per server; request and job metrics are recorded only by servers that measure. - Duplicate @McpTool and @ManagedResource names are refused at build time (and at start-up for conditional beans). - Cron searches a full 400-year cycle; fixed-rate catch-up is arithmetic. - Managed operations answer 400 for bad JSON or rejected arguments and 404 only for an unknown bean or operation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: per-server tracers and gauges, dependency-ordered destruction - Each server keeps its own tracer until it stops: a later server's start no longer retires a live one's, request spans use the tracer of the server they reached and child spans their parent's, and the global slot passes to another live server's tracer when its own stops. - Managed-resource gauges are sources of a shared gauge, added when their server starts and removed when it stops. - Lazy singletons are destroyed in dependency order with the eager ones; request and session beans dependents first. - An @Async call dropped at the shutdown deadline fails its Future. - Counters export through OTLP as_int and print exactly in Prometheus. - Jobs whose classes share a simple name take qualified names. - Virtual tasks run only on their own server's hosts. - MCP enum arguments match constant names; Prometheus name collisions are refused when the second instrument is created. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: safer start-up failure, init order, session replacement - Any failure after the listener binds, before a Backend owns it, stops the listener, and gauges a failed start added are removed. - @PostConstruct follows every injection, fields and setters included. - A connection whose ROLLBACK fails in Database.transaction is closed, so the pool discards it instead of lending it to a borrower that would hang. - The generated application binds its scheduler to the server before starting it: job metrics are recorded when the server measures, and job spans go to the server's own tracer. - After invalidate(), getSession(false) answers null and getSession(true) starts a new session in the same request; both are finished at its end. - @Async on a @RequestScope bean, and @Async with @Scheduled, are build errors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: clean up after Errors at start-up; tighten sessions, MCP, metrics - Start-up clean-up runs through finally blocks, so an Error from a bean (a failed static initializer, a missing class) closes the pool, stops the executors and listener, destroys what was built and rolls the tracer back. - Every session a request invalidates is finished, not only the last; a new session is not stored when no response can carry its cookie; the session cookie name must be an HTTP token. - @McpTool with @Async, and duplicate or empty tool/operation parameter names, are build errors; float arguments are range-checked. - A @WebSocketMapping path with a percent escape is a build error. - A refused shared gauge leaves no entry behind; counters refuse to overflow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: guard post-start hooks, protect in-use session beans, scope rules - Every hook after the Backend exists (management, MCP attach, the application's started()) runs under one guard that stops the server if any fails. - Session beans are not expired while a request is using their session: each server counts requests per session id until the request ends. - Histograms keep a null first label apart from an empty one. - A @SessionScope bean cannot inject HttpSession (a per-request copy with the database store), and a websocket endpoint cannot inject request- or session-scoped beans; both are build errors. - @Scheduled, @McpTool and managed methods inherited from a superclass are registered. - The request-metrics switch is an AtomicBoolean, visible to running workers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: test that expiry spares session beans a request is using The purge is package-private so a test can run it at a chosen time instead of waiting for the next once-a-minute lookup; the test fails with the in-use check removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: database-time locks, atomic session rotation, per-server callbacks - Scheduler lock leases use the database server's clock (PostgreSQL, MySQL), so a replica running ahead cannot take a held lock. - A JDBC session rotation is one transaction that locks the old row and is refused when it is gone, so a stale copy cannot undo a logout. - Session in-use counts follow the HttpSession object, covering replaced and rotated sessions; a bare HttpServer refuses sessions it could never store. - WebSocket callbacks carry their own server's executors. - as_int encodes as sfixed64, so a negative up-down counter exports. - Virtual-task submission rechecks, under the inbox lock, that shutdown has not already drained the host. - Unknown cron zones are refused; an Error from a lifecycle hook no longer abandons shutdown; a second metrics exporter with another identity is refused, since metrics are per process. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: spare in-use sessions in the store, finish sessions on Errors - The store purge (memory and JDBC) skips sessions a running request uses. - A handler that throws an Error still has its invalidated and changed sessions stored. - @Async on a @SessionScope bean is a build error, as on a request bean. - Task queue-depth points are summed per executor name; histogram label keys that collide in Prometheus, or are "le", are refused. - The management endpoint's backend and the request-log switch are volatile, so running workers see them. - Managed-resource names must be one URL segment; duplicate programmatic job names are refused; the stdio bridge answers under the top-level id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: contain tracer Errors, honour lost rotations and leases, check tokens - Every hook into the tracer now catches Throwable: an AssertionError or LinkageError out of a tracer escaped request start-up, and since the tracer stays installed, every later request failed the same way. - A database session rotation that finds its old row gone (another request of the client rotated it first) is recorded on the session, and the request sends no Set-Cookie: the id it would announce has no row. - A scheduler claim writes a lease unique to that run, and release matches it, so a run that outlived lockAtMostFor cannot free the claim a later run (of any job sharing the lock name) took after it expired. - Prometheus label names are folded without the colon a metric name may carry; histogram label collisions use the same fold. - An executor shut down from one of its own tasks (an @Async method that stops the server) no longer waits out the timeout for that task, nor interrupts it. - A double of exactly -2^63 is refused as a long argument, like 2^63. - Config.getHeaderSecret refuses a bearer token no request could carry, for the MCP and management tokens as well as the OTLP relay's. - Three doc comments the /// conversion had merged into their neighbours are back on their own members, or gone where the member no longer exists. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: spare in-use sessions on lookup, check the whole bean graph off-request - A lookup no longer expires a session another request is still using: the database store writes the last use when a request ends, so a long request made the stored time look expired and the next request deleted the row and the beans under the first. The purge already spared these ids. - A class a @Bean method builds keeps its @Scheduled jobs, @McpTool methods and @ManagedResource surface; only its lifecycle methods were collected. - Websocket endpoints, beans with @Scheduled jobs and @ManagedResource beans are checked through their whole dependency graph for request- and session-scoped beans, and the error names the path. All three run with no request current; only an endpoint's direct injections were checked. - A prototype controller, or a prototype with MCP tools or managed attributes, is built once for the router: the reference was evaluated in the null test and again for the router, building a second instance. - A @Bean method that returns null stops the start, naming the method, instead of handing null to what injects it. - MCP: an explicit "id": null is a request and is answered; only an absent id is a notification. - A number argument given as "NaN" or "Infinity" is refused. - OTLP/JSON metrics write non-finite doubles as the protobuf JSON strings ("Infinity", "-Infinity", "NaN") rather than null. - Websocket callbacks report to their own server's tracer, through a per-thread owner the callbacks bind, rather than to whichever server installed its tracer last. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: undo a stale memory rotation, finish every session, bound the bridge - The memory store shares one HttpSession between concurrent requests, so a second request rotating it after the first had rotated and answered would delete the id the first response carries, and signed the client out if that response arrived last. A request now records the id it found each session under; a rotation whose replaced id is not that one is undone and not announced, as the database store's conditional move already settled. - A request that ends several sessions finishes each one on its own: a store failure on one no longer skips the deletes, saves and bean clean-up of the others. - The stdio bridge sets connect and read timeouts (CN1_MCP_TIMEOUT_MS, two minutes by default), so a backend that accepts and stalls is reported under the request's id instead of blocking every later message. - Gauge callbacks and the metrics export contain any Throwable: an Error from an application's gauge used to end the exporter's only thread. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: forget found session ids, run notifications, check executor kinds - The ids a request found its sessions under are cleared with its other session state, when it ends and when a pooled request is recycled: a keep-alive connection's request otherwise held every session it had served, attributes and all. - An MCP notification runs its method; only the answer is withheld. An id-less tools/call used to be acknowledged and never run. - cn1.task.executor.<name>.kind is trimmed, and anything but platform or virtual is refused: at start-up for the kinds the files set, and when the executor is created for one set only in the environment. A typo used to fall back silently to the annotation's kind. - An Error that fails the work of inSpan or inBackground -- custom spans, @Async calls, scheduled runs -- is recorded on the span before it is rethrown, rather than the span exporting as a success. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: histogram shapes, empty batches, untraced servers, one metrics exporter - A histogram registered again under an existing name must have the same bucket boundaries and label keys; a different shape is refused rather than silently recording against the first one's buckets and labels. - MCP: an empty JSON-RPC batch is answered with Invalid Request, not the 202 an all-notification batch gets. - A backend with tracing off passes an untraced marker instead of null, to its listener, its request handling, websocket callbacks, scheduler and @Async calls. Null means "whatever tracer is installed", so another backend in the process installing one exported this server's requests and jobs under its own service name. - Only the first open metrics exporter of an identity exports; a second one with the same service and endpoint waits and takes over when the first stops, instead of sending every point twice. The same exporter opened twice is refused. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: tokenless MCP on loopback, JSON-RPC 2.0 only, DST folds, bridge ids - A development server whose MCP endpoint has no token binds 127.0.0.1 when no address was chosen, and refuses to start when one was chosen that is not loopback. The development tools write SQL and call every handler, and the listener used to bind every interface. Documented in the guide and the skill, with how to test from another machine. - MCP refuses a request whose jsonrpc is not exactly "2.0" before its method runs. - A cron time the clocks pass twice (01:30 on a fall-back night) fires at the first occurrence, as Spring and Quartz do; the second only when the search starts after the first. It used to take the second. - The stdio bridge answers an HTTP refusal -- a 401 for a missing token, most often -- under the host's own request id. The server's error came before it read the body, so it carried a null id and the host never saw its request complete. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: spare pre-rotation ids, answer malformed MCP, drain before metrics off - A session rotated by a request still running is spared under the id its row and cookie still have, as well as its new one: a purge or another request's lookup used to see only the new id as busy and delete the row the running request was about to move. - MCP: an id-less object that is not a valid request (no method, or not JSON-RPC 2.0) is answered with Invalid Request under a null id; only a valid notification goes unanswered. A response object is ignored. - The stdio bridge answers a batch the backend refused or never received with one error per request id in it. - A request handler, websocket callback or executor task that calls Backend.stop() while another stop is under way returns instead of waiting: waiting held the very work the first stop's drain waited for. - Server metrics are turned off after the background-task drain, so a scheduled run that ends during it still records its duration. - A start that fails after the listener bound closes the session-scoped beans a request may already have built. - A float argument too small for a float is refused rather than made 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: strict loopback, JSON-RPC id types, @Async scope, savepoint failures - A tokenless MCP listener's address must be loopback by definition: localhost, ::1, or a numeric 127/8 literal. A prefix test took a name such as 127.backend.example for loopback wherever it resolved. - The MCP start-up line advertises the address the listener is bound to (bracketed for IPv6), not 127.0.0.1 regardless. - MCP refuses a request whose id is an object, array or boolean, with a null id and before its method runs. - Beans with @Async methods join the scoped-dependency check: their tasks run on an executor with no request current, so a request- or session-scoped bean reached from one throws on first use. - A savepoint that cannot be set marks the outer transaction rollback-only, so a caller that catches the failure cannot commit half of a flush or report success for work the server rolled back. - A metrics exporter hand-over starts the successor only while it is still open and first in line, under the lock its own shutdown takes; a successor that stopped meanwhile used to get a thread nothing stopped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: retire shared session beans, drain gauge reads, bound-address calls - An invalidation no longer destroys session-scoped beans another request still holds -- a database store hands each request its own copy. The beans are retired: that request keeps using them rather than building new ones under the deleted id, and the last one to leave destroys them. - Removing a shared gauge source waits (bounded) for reads of it already running, and a read skips a source removed since its list was copied; the server's own exporter now shuts down before its beans are destroyed and the pool closed, not after. A gauge used to be called during its bean's @PreDestroy, or against a closed pool. - backend_call sends its request to the address the listener is bound to (Backend.getListenAddress), not 127.0.0.1 regardless. - A histogram decides whether to export its unlabelled series under the lock that adds labelled ones, so a racing first observation no longer adds a transient zero series. - A task virtual thread abandoned at the drain deadline before its first turn has its token removed from VIRTUAL_TASKS, which otherwise kept the task and everything it captured for the life of the process. - An executor whose virtual hand-off is refused wakes a shutdown() waiting on the count it just gave back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: per-source gauge drains, executor kinds, unannounced rotations, params - Shared-gauge reads are counted per source, so a gauge stuck in its callback delays only its own removal rather than every gauge a stopping server removes, two seconds each. - One named executor asked for two kinds of thread is refused: at build time between @Async and @Scheduled declarations, and at run time for executors asked for in code. Whichever ran first used to decide, so a PLATFORM database method could run on the virtual hosts. AUTO agrees with either, and configuration still overrides. - A gauge without a name is refused, as counters and histograms are; it rendered a nameless Prometheus sample the scraper rejected. - A rotation a failed request cannot announce is undone on the session itself, and the request's other changes are saved under the id the client still has. Only the beans used to move back. - MCP params that are not an object are Invalid params, not treated as an empty call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: one cron run per repeated time, strict MCP args, masking, DevTools reuse - A cron time the clocks pass twice fires once, at the first occurrence, from whichever side of the change the search starts. It used to skip to the next day from the old offset and return the repeat from the new one, so the answer depended on which side of the change the search began. After a run at the first, the scheduler searches from just past it, where returning the repeat would run a daily job twice. The search steps past the repeat within the day, so the other times of that day still count. Documented in the scheduling chapter. - tools/call arguments that are not an object are Invalid params; the tool used to run with none. - backend_config masks keys naming headers or auth, and values carrying a credential as name=value -- cn1.otel.headers=api-key=..., a datasource URL's ?password=... -- whatever their key. - One DevTools instance given to two servers no longer points the first server's tools at the second: a later installation gets its own instance. - The MCP and management paths are canonicalized as request targets are (Config.getRoutePath), so /%6dcp is served at /mcp instead of never. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: tear down after an in-flight stop caller, one server per builder - A stop() called from a request, a websocket callback or a task drains as before -- discounting its caller -- but defers the teardown (beans destroyed, pool closed) to a thread that waits, up to the shutdown timeout, for that caller to leave. Requests and callbacks are counted in and out for it. The caller used to go on running against destroyed beans and a closed pool, and its own request stored its session into them. The tracer's shutdown is arranged on the caller's request span first, so that request still exports its trace. - A histogram ignores values past its label keys when telling series apart; a one-label histogram recorded with two second values exported two series under one label set. - A new session whose first save throws has its session-scoped beans destroyed and any half-written row removed: no cookie was sent, so nothing could reach it again. - Backend.Builder.sessionStore installs a store before the server listens, and Sessions.setStore refuses once a session has been looked up: a store set after start() lost the sessions already made. - A builder refuses to start a second server while its first is running: both shared the generated application's beans and scheduler, and stopping the first destroyed the second's. It may start again after. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: one transaction one pool, unwind overrunning tasks, session bean edges - A @Transactional method bound to one pool that touches another is refused (TransactionException.IllegalState), naming REQUIRES_NEW and NOT_SUPPORTED. The second pool used to hand out an ordinary connection that committed each statement, so a failure rolled back only the first database. - A virtual task still running at the drain deadline is no longer freed: free() does not unwind, so its finally blocks and monitor exits never ran and a lock it held stayed owned. A task that never started is still dropped; one that started runs on to completion past the deadline, as an overrunning platform task does, while the stop proceeds. - A host's wake byte is written, and its wake pipe closed, under the inbox lock, so a submitter cannot write into a descriptor shutdown has closed and the process has reused. - A request- or session-scoped bean inheriting an @Async method is refused like one declaring it. - A copy that rotates after another request invalidated its session uses the retired beans rather than building a set under the new id. - A histogram recorded with no label values goes to its unlabelled series rather than a second one with the same empty labels. - A database session's bean holder gets the touch-interval grace its row gets, so a request in that window does not find its beans destroyed and build a second set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: conditional @Primary fallbacks, scoped factory owners, metric labels - A conditional @Primary no longer discards the other candidates: the wiring uses it when its condition holds and the active alternative when not (Wiring.preferred). A prod-only primary beside a dev bean used to make the dev profile fail to start. More than one unconditional alternative is refused at build time. - A non-static @Bean method on a request- or session-scoped configuration that builds a bean of another scope is refused at build time: it is called through the scoped stand-in at start-up, with no request to find it in, and the server refused to start. - Histogram label values are widened before choosing a series (an Integer and a Long 200 export alike), the overflow label key is reserved -- also in the spelling Prometheus folds it to -- and Gauge.point refuses an empty label key. Each produced duplicate or nameless samples that make a scraper reject the whole exposition. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: joined failures mark rollback-only, AUTO decided per task, napping waiters, exporter hand-off - A joined DataSource.inTransaction or EntityManager.transaction body that throws now marks the outer transaction rollback-only, as Spring's participating transactions do; an outer caller that swallowed the exception used to commit the helper's half-done writes. - An AUTO executor decides virtual-or-platform per submission, not at creation, so one first requested during start-up (before the hosts exist) no longer stays on platform threads for good. - A virtual thread waiting cooperatively on an @Async Future naps on its host with a deadline (VirtualThread.current + HttpServer.napUntil) instead of being re-queued at once; the host loop polls with the earliest nap as its timeout rather than spinning a core. - A replacement OTLP metric exporter's thread joins its predecessor before its first export, so two exports of one registry never overlap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: namespaced JDBC sessions, exporter transport check, start-up drain - JDBC session rows carry a namespace (cn1.session.namespace, by default the generated application's package) and every query is limited to the store's own, so two different servers on one host sharing a database no longer accept each other's session cookie. Replicas share a namespace. - A second metrics exporter with different headers or protocol is refused like one with a different service or endpoint: only one exports, so its credentials were silently replaced by the first's. Header values are never printed. - A failed start drains the tasks @PostConstruct started with the configured shutdown timeout before destroying their beans, instead of interrupting them and tearing down at once. - A metric reader that answered false from open() is neither kept nor shut down; a reader whose shutdown throws no longer stops the teardown. - A class-level @Async counts only for the methods its class declares, as the weaver applies it, in the @Scheduled, @McpTool and scoped-bean checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: align with Spring on handler Errors, scope checks and shared sessions; review fixes Spring Boot behaviour where nothing costs more to match it: - A handler's Error is answered 500, as Spring Boot's Tomcat answers it, instead of dropping the connection; only a VirtualMachineError other than a stack overflow is rethrown, as Tomcat rethrows it. - A websocket endpoint, job, managed bean or @Async bean that can reach a request- or session-scoped bean, and @Async on a scoped bean, build with a warning rather than an error: Spring starts them and the scoped stand-in throws IllegalStateException only when used with no request current. - JDBC sessions default to one shared namespace, as Spring Session shares its table; cn1.session.namespace keeps two projects apart. - A metric registered again in another unit keeps the first, as Micrometer does, and is warned about once. Review fixes: - An AUTO executor is pinned to the kind a later caller asks for by name, so the result no longer depends on call order. - The task queue gauge reads only the servers that measure. - An exporter opened a second time is refused before its settings change, and a server whose reader's open() threw no longer shuts it down. - A string tool argument refuses an array or object. - A request leaves its sessions before restoring the task and trace context, so a retired session's @PreDestroy runs as its own server's. - A timed Future.get saturates its deadline instead of overflowing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: undo failed rotations, text-keyed histogram series, early JSON, optional conditionals - A session-id rotation whose save fails is undone -- the id, and the beans a lookup after the rotation moved -- so the client's old cookie still finds them instead of a second set being built. - Histogram series are told apart by the text Prometheus prints, so Boolean.TRUE and "true", or 1L and "1", are one series, not duplicate samples; the first value's type is kept for OTLP. - respondJson's body is serialised before request-scoped beans are destroyed, as Spring MVC writes the body first; only when there are beans to end, so the zero-copy write is unchanged otherwise. - An optional field or setter whose candidates are all conditional is injected only when one is active, keeping the field's initializer as Spring does; @Autowired(required = false) on a method now makes its arguments optional, as in Spring. - A route returning a Future (an @Async route) is refused with a message that says why: the client would get the pending task, not the result. - Caller-supplied waits saturate their deadline in every shutdown and borrow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: saturated schedules, active-only routes, one setter per property, partial metric success - Fixed-delay, fixed-rate and initial-delay schedules and lock leases saturate their timestamps: a period of Long.MAX_VALUE wrapped into the past and ran the job back to back. - The route listing (backend_routes) names only the controllers and endpoints this start registered; a conditional one that is off serves 404. - @ConfigurationProperties binds one setter per property, as Spring Boot's binder does: the overload matching the getter's type, else the first. - The OTLP metric exporter reads partial_success and reports rejected data points instead of counting the export healthy. - The websocket wrapper forwards getSubprotocols(), so an endpoint's protocols are negotiated. - A metric name used by both @Timed and @Counted is a build error, rather than a registration that throws after the body has run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: non-blocking chained futures, handshake stop, tracer before accept, Future results; fix TelemetryTest flake - An @Async call returning another's pending AsyncTask completes when that one does instead of blocking its worker, which deadlocked a one-thread executor (and any pool under enough concurrent outer calls). Any other Future is still awaited, as Spring's interceptor does. - The websocket handshake (router, getSubprotocols, onOpen) is marked as serving work: a stop() from it discounts its own connection and defers the teardown until it returns; the fallback router counts in flight too. - A server's tracer, including the untraced marker, is set before any worker accepts, so a request accepted meanwhile cannot report to another server's tracer. - An @McpTool or @ManagedOperation returning a Future is a build error, as a route's is: the caller would get the pending task, not the result. - TelemetryTest drains the network queue before each test and waits for its own span: a previous test's late export landed in the collector mock and failed build-test (21) intermittently. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: only the rotating request announces a session rotation; exporter reopen waits; Future attributes - Memory-store sessions: a request records the id it looked the session up by (its cookie), not the shared object's current id, and only the request that called changeSessionId() stores and announces the rotation. A request arriving mid-login used to undo the login's rotation -- or complete it and send the new id to whoever held the old cookie -- defeating the fixation defence. - A reopened OTLP metric exporter waits for its previous thread's last export before applying the new configuration, instead of starting a second loop beside it; refused if that export outlasts a minute. - An @ManagedAttribute getter returning a Future, or woven @Async, is a build error, as operations and tools already are. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: Prometheus-folded aspect metric names, replaced shared gauges, prototype jobs warned - @Timed/@Counted names are checked at build time the way the runtime claims them for Prometheus: two names that fold alike (latency.ms, latency_ms), or a counter's _total against a histogram's series, are refused, instead of throwing in the woven finally after the body ran. - A gauge that replaces one addSource() built drops the stale shared registration, so the old server's last removeSource no longer deletes it. - A prototype bean with @Scheduled methods builds with a warning, as Spring runs it: one instance runs its jobs for the server's life and a prototype is never destroyed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: old session id retired at rotation, request beans drained fully, interface aspects refused - The memory store answers a lookup only when the session still has that id: a login's changeSessionId() retires the old, possibly planted id at once, as a servlet container does, instead of when the login request saves. An undone rotation restores the id and the entry answers again. - Request-scoped beans that a @PreDestroy builds are destroyed however deep the chain goes (bounded), not just one pass further. - @Transactional, @Async, @Timed or @Counted on a project interface -- on a default method, an abstract one, or the type -- is a build error: the build weaves classes, and the annotation was silently applied to nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: one backend per process; conditional deps checked at start as in Spring - Backend.start() refuses a second live Backend in the same process (a stopping one is waited for, so stop-then-start still works). Scaling out is more processes -- sessions in the JDBC store, job locks in the database -- and the runtime's process-wide state (tracer, metrics and exporter, default executors, virtual-thread hosts) assumes one server. The tests that exercised two concurrent servers are removed; one test pins the rule. - A required dependency whose candidates are all conditional is checked at start, as Spring checks it (Wiring.single names the injection point); the developer guide no longer claims the build proves coverage. - A @Scheduled method returning a Future builds with a warning: its return is ignored, as in Spring, and the run ends when the method returns. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: class-specific woven bodies, virtual-task stop, per-bean job names, full termination; build on master - Moved method bodies are named per declaring class, so a same-package subclass's woven override no longer overrides the base class's body: an explicit super.foo() runs the base body instead of recursing. - stop() called from a virtual @Async task no longer waits out the drain for its own host: each virtual task records the host it runs on. - Two beans of one class with @Scheduled methods (two @Bean factories) get their jobs named by bean, as Spring schedules each, instead of refusing the start with a duplicate name. - awaitTermination() also waits for a deferred teardown, so a process does not exit while @PreDestroy and the pool close are still running. - The wake-pipe native reads the array payload through CN1_ARRAY_DATA: master's 4-byte header (#5903) removed the data pointer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: injective support names, discarded file responses closed, restart cursor, slot released - Woven body names carry the declaring class's own name, not its hash, and support-class names encode `_` and `$` injectively: two classes whose names shared a hash (Aa, BB), or Outer_Inner and Outer$Inner, no longer get one name -- a recursing super call, or one helper replacing the other. - A file response replaced by a 500 because the session could not be stored closes its descriptor; only the writer closed it before. - The virtual-task host cursor is reduced to the current server's host count, so a restart with fewer hosts does not index past its array. - A stopped Backend frees the process slot, so its destroyed beans and sessions are not kept reachable until the next start. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend build: injective support names, per-bean stand-ins, lifecycle runs synchronously - Support-class names escape `_` as `__` and `$` as `_S`, so no two class names meet (A$_B and A_$B collided under the previous scheme). - Scoped and lazy stand-ins are named per bean: two @Bean methods of one class, both request-scoped, each get their own instead of one source replacing the other. - A stand-in whose bean's constructor calls an overridable method runs the bean's own code during construction rather than dereferencing a scope that is not assigned yet. - @Async on @PostConstruct or @PreDestroy is not applied, with a warning, as Spring calls lifecycle methods on the bean itself: the server no longer becomes ready before initialisation, and a @PreDestroy body still runs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend review fixes; recalibrate the Windows AMD perf row; GcSteadyState fault twin may wedge Review: - A request's scoped beans are destroyed even when serialising the deferred JSON body throws: the array was taken first, and every later pass found nothing to destroy. - @Profile("!") (an empty negated name) is a build error; it activated the bean under every profile. - A @PostConstruct or @PreDestroy returning a Future builds with a warning: the return is ignored, as in Spring, and the server goes on when the method returns. Gates: - perf-baseline.json: the windows-x64 AMD family 25 model 1 rows are recalibrated with calibrate-perf-baseline.py from the six calibration runs of #5903 plus this branch's run, which changes nothing in the VM. Its arrayRandom rounds spanned 1.06-1.49 inside one run (median 1.38 against a 0.97 baseline) -- runner noise the row's 25% tolerance could not absorb; the row is now 1.061 with 45%, the script's own rule for the observed spread. - GcSteadyStateIntegrationTest scenario 4 builds the reserve OUT (-DCN1_PACING_NO_RESERVE) to prove scenario 3 can fail. That deliberately broken build can wedge on the admission margin instead of finishing, and the gate required it to finish, so it failed on a slow runner (footprint 745MB of a 768MB budget, no cycles, no reserve parks). A wedge is now accepted as the demonstration, checked from the per-second probe: headroom under the threshold and no reserve parks. A finishing run is held to the same assertions as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend docs: drop unsupported hand-written mains, promote Future, db session store - Remove every hand-wired server from the guide (ServerSnippets, raw HttpServer.start/serveForever, installByHand, mcpTool builder); the build writes the entry point and that is the only supported shape. - Promote java.util.concurrent Future/ExecutionException/TimeoutException/ CancellationException/TimeUnit (and Properties) to documented backend API: rendered in the backend javadoc, gated by check-backend-jdk-surface.py (which also scans the guide's demos), and exercised on ParparVM by SelfTest.futures(). - Session store value "jdbc" -> "db"; diagrams clarified (no bare "null", fixedRate vs fixedDelay with varied run lengths). - ParparVM virtual threads are not Java 21 ones; say so. - Correct doc claims the audit found false: CN1_ prefixed env names, multi-server passages (one backend per process), @Scheduled scope rules, management "always on in dev", injected DataSource instead of DataSource.open(getenv), PushFeedback on the backend Json and injection. - Document the missing cn1.otel.* keys and CN1_HTTP_MAX_UPLOAD_MB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: management and MCP linked only on request; settings as annotations - Backend no longer names Management or McpServer. Only the builder's management() and mcp() do, and the generated entry point calls them only when the build asked (@EnableManagement / @EnableMcpServer / an @McpTool / a literal cn1.*.enabled=true in any application*.properties, or a dev build). The translator drops the uncalled builder method and the classes with it; BackendOtelTest now asserts from nm that a server that never asked carries neither, against a control that links both. - Typed settings annotations -- @ServerConfig, @SessionConfig, @DataSourceConfig, @StaticFilesConfig, exporter settings on @OpenTelemetry, paths on the Enable* ones -- compiled in as the bottom configuration layer (Config.withCompiledDefaults), under the files and the environment. Values the runtime would refuse, and two classes disagreeing, are build errors naming the class. - OtlpTracerTest.partialSuccessIsCounted waited on a flush that could run before the request's span was queued (the span ends after the response is written); it now waits for the span first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: controllers return and accept the application's own classes as JSON A route may now return an entity or DTO (and collections or String-keyed maps of them) and take one as @RequestBody, as a Spring controller does through Jackson. The build writes a <Name>Cn1Json codec per class -- plain code, no reflection -- in the app's @Mapped JSON form, so a class shared by app and backend round-trips: fields (public directly, others via bean accessors), @JsonProperty/@JsonIgnore (now shared with the backend), Date as epoch millis (read from millis or ISO-8601), byte[] base64, enums by name, subclasses written as themselves, unknown members ignored. - A body the codec refuses is a 400 naming the path ($.lines[0].quantity: expected ...); a response nesting past 64 objects (a cycle) is a 500 pointing at @JsonIgnore. - Generic type-variable fields, body classes without a no-arg constructor, interfaces, arrays other than byte[], and runtime types such as HttpServer.Response inside a list stay build errors, with the reason. - HttpServer renders a deferred JSON body before the write, into the same buffer, so a throw there (a Writable, a codec) is answered 500 and recorded as the handler's failure instead of dropping the connection unanswered. - The guide's order example is compiled from the guide's own files and run end to end by BackendBeansTest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: outbound I/O parks a virtual thread instead of blocking its host A virtual thread used to park only on the socket it served. An outbound read -- a PostgreSQL or MySQL query, a Web call, a TLS handshake -- did a blocking recv() on the host pthread, and with one host per core that many slow calls stopped the server answering anything. - Native: a per-virtual-thread wait record (descriptors, events, timeout) and cn1BackendVtWait/cn1BackendAwaitFd; resume answers WAITING (3) for it. Outbound Tcp descriptors stay non-blocking for life and every wait (connect, read, write, TLS handshake/read/write) parks on a virtual thread and polls anywhere else, so platform threads behave as before. SO_RCVTIMEO/SO_SNDTIMEO become the wait's deadline. Web drives libcurl through the multi interface on a virtual thread. - Host: WAITING registers the descriptors with the host's own poller for exactly the wait, wakes on readiness or deadline, and the stop drain keeps pumping tasks that wait on outbound I/O. - Docs/javadoc/skill: network databases, Web and TLS are fine on VIRTUAL; SQLite, file access, host-name resolution and Object.wait still block. - Tests: SelfTest (Tcp, Web, TLS handshake, TLS read, read deadline) and DbCheck (pg_sleep / SLEEP against a one-host server) prove another request is served while the call waits; both fail with the old blocking wait. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: clear the PMD report; native checks for the JSON codec runtime - The seven AvoidUsingHardCodedIP findings are loopback and wildcard literals that recognise or bind the local interface and dial nothing; each carries a //NOPMD with that reason, the convention ServerSocket and Credentials already use. The OwnRoute overrides get @Override. - SelfTest exercises JsonCodec on the translated runtime: range and fraction refusals with their paths, dates from millis and ISO-8601 with offsets and fractions, an impossible date refused, base64, writeDate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend review: codec value dispatch, TreeSet, long bounds, MCP CORS and paths - An Object or raw Map/List field is written by its run-time class through a generated cn1app.JsonValues: a class the build writes goes through its codec, Date as millis, enums by name, JDK shapes through Json; any other class is a 500 instead of its toString(). - A TreeSet body needs a Comparable element; refused at build time otherwise. - readLong bounds a fractional-free double by 2^63 before converting, so 9223372036854775808.0 is refused instead of clamped to Long.MAX_VALUE. - MCP and management match the canonical path (pathFrom), as the routers, static files and relay do: /%6dcp is /mcp. - MCP answers an allowed origin's CORS preflight before authentication and names the origin on every reply to it (new Response.header). - The guide's order example uses one source block per include, which the snippet validator requires. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend review: VT pinning, pool opens outside the monitor, MCP codecs, lease check - PINNING. ParparVM identifies a monitor's owner by its OS thread, which every virtual thread on a host shares, so a virtual thread that parked inside `synchronized` let the next one on that host into the same critical section. With outbound I/O now parking, that became likely. A virtual thread now counts the monitors it holds (one TLS load per monitorEnter/ Exit, zero off a virtual thread) and never switches out while the count is above zero: every park -- socket wait, collector handshake, nap -- asks cn1BackendCanPark/cn1VirtualThreadPinned and waits like a platform thread instead. SelfTest reproduces the interleave on one host: two requests waiting on a slow socket inside one monitor; with pinning disabled it sees two inside at once. - DataSource reserves pool capacity under its monitor and opens the connection outside it, and a virtual thread waits for a free connection by napping rather than wait(); DbCheck runs concurrent lazy opens on one host against real PostgreSQL. - @McpTool parameters other than scalars are read through the shared JSON codecs (List<Integer> holds Integers, DTOs are built, a bad value is a tool error naming its path), results are written through them, and shapes they cannot serve are build errors. The codec set is shared between the bean and router processors. - A scheduler claim that lands after its lease ran out is not run. - Response.header no longer fully qualifies an imported class (forbidden PMD). - The DB session purge's cross-replica limit is recorded as a Spring-matching decision. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend: a connection gate instead of Database's monitor, so queries stay parkable With virtual threads pinned while they hold a monitor, Database's synchronized methods pinned every query, and a PostgreSQL or MySQL wait held its host again -- DbCheck's "another request is served while a query waits" failed on one host. The exclusion a connection needs (one operation at a time, a transaction reserved to its caller) is now a reentrant gate: a flag taken and returned under a monitor held only for that instant. A virtual thread waiting for it naps; anything else waits on the monitor as before. Transaction ownership is keyed by the virtual thread, not Thread.currentThread(), which every virtual thread on a host shares, and a caller waiting on another's transaction gives the gate up meanwhile, as wait() gave up the monitor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend review: float range on body reads, built-in metric names reserved - A body value read into float or Float is refused (400) when a float cannot hold it, instead of narrowing 1e100 to infinity or 1e-100 to zero; the same rule MCP float arguments already had. - @Timed/@Counted names are checked against the server's own instruments at build time, by name and by Prometheus series. Taken at run time, the clash surfaced in the woven finally after the body's work had committed. - An Object or raw collection field holding a class with no codec stays a run-time 500 by design; the reasoning is in BackendJsonCodecs.check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend build: fail fast on generated-name collisions; two dispatch/entry fixes - Two classes whose JSON codec names fold together (Outer_Inner and Outer.Inner) are a build error instead of one codec silently replacing the other. - Every class the bean processor generates -- aspects, proxies, tool and managed adapters, codecs -- is checked against the project's own classes before it is compiled; an existing class of that name is a build error instead of being overwritten in the output directory. - Subclass dispatch walks ancestors on the compile classpath too, so a project subclass reaching the declared type through a dependency's class is written with its own fields. - A module whose only request is @EnableManagement gets an entry point. Each has a test asserting the failure or the outcome; the dispatch and entry-point tests fail with their fixes reverted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend review: managed results through the codecs, sessions on websocket fallbacks - An @ManagedOperation returning anything beyond a scalar or String is written through the JSON codecs; one returning a shape they cannot write is a build error. A DTO result was sent as its quoted toString(). - A websocket fallback router sees the server's sessions on the handshake (the upgrade bypasses the request wrapper), and what the handshake did to them is stored and released afterwards. - perf-baseline: calibrate linux-x64@amd-epyc-9v45-96-core-processor, a runner CPU model the gate had no row for (ratios within the spread of the existing AMD/Intel linux rows). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Backend review: gauge-only managed attributes, static-only and root-mounted servers - An @ManagedAttribute that is not a number or a boolean is a build error; it was listed under /manage/managed and silently missing from metrics. - A module that only serves static files (@StaticFilesConfig or cn1.static.root in a properties file) gets an entry point. - cn1.management.path=/ mounts the endpoints at the server root instead of being refused as not starting with /. Each has a test; the two outcome tests fail with their fix reverted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * perf-baseline: 30% RAM tolerance on linux-arm64 stringBuilding The arm64 gate flagged stringBuilding RAM at 0.32x against a 0.26x baseline. Not a regression: the VM, JavaAPI and benchmark are identical between this head and 3666deb288, which read 0.27x, and master reads 0.26x on every run -- the same code, two readings, one row's RAM moving between runs. The baseline stays 0.26x; the row gets the 0.3 memory tolerance other noisy RAM rows carry, so +30% is still a regression. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 656ea3d commit 65ae4d8

224 files changed

Lines changed: 38631 additions & 1501 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/scripts/build_javadocs.sh‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,25 @@ done < <("$BACKEND_DIR/shared-sources.sh")
172172
# server as for an app. The website's doclet marks the whole tree shared with
173173
# --shared-sources.
174174
BACKEND_SOURCES_ARGFILE="$CN1_DIR/build/backend-javadoc-sources.txt"
175+
# The vm/JavaAPI classes the backend's public API exposes beyond the CLDC set --
176+
# @Async's Future and what its get() throws, Config's Properties. The backend
177+
# compiles against vm/JavaAPI, so these work there; listing them here is what makes
178+
# them documented, supported API rather than an accident of the class library.
179+
# check-backend-jdk-surface.py fails the build when the backend exposes a JDK type
180+
# that is neither CLDC nor in this list. Keep the two lists in step.
181+
BACKEND_PROMOTED_JDK="java/util/Properties.java
182+
java/util/concurrent/CancellationException.java
183+
java/util/concurrent/ExecutionException.java
184+
java/util/concurrent/Future.java
185+
java/util/concurrent/TimeUnit.java
186+
java/util/concurrent/TimeoutException.java"
187+
python3 "$ROOT_DIR/scripts/check-backend-jdk-surface.py"
175188
{
176189
find "$BACKEND_STAGE" -name "*.java" | grep -v '/com/codename1/impl/'
177190
find "$ROOT_DIR/Ports/CLDC11/src" -name "*.java"
191+
echo "$BACKEND_PROMOTED_JDK" | while IFS= read -r promoted; do
192+
echo "$ROOT_DIR/vm/JavaAPI/src/$promoted"
193+
done
178194
} | LC_ALL=C sort > "$BACKEND_SOURCES_ARGFILE"
179195

180196
# Held to the same doclint as the client API above. --release 8 matches the backend module's
@@ -185,7 +201,7 @@ BACKEND_SOURCES_ARGFILE="$CN1_DIR/build/backend-javadoc-sources.txt"
185201
--add-script "$ROOT_DIR/maven/javadoc-resources/highlight.min.js" \
186202
--add-script "$ROOT_DIR/maven/javadoc-resources/javadoc-highlight-init.js" \
187203
--release 8 \
188-
-sourcepath "$BACKEND_STAGE:$ROOT_DIR/Ports/CLDC11/src" \
204+
-sourcepath "$BACKEND_STAGE:$ROOT_DIR/Ports/CLDC11/src:$ROOT_DIR/vm/JavaAPI/src" \
189205
-Xdoclint:all,-missing \
190206
-Xmaxerrs 10000 \
191207
-Xmaxwarns 10000 \

‎CodenameOne/src/com/codename1/annotations/JsonIgnore.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929

3030
/// Excludes a `@Mapped` field from the JSON projection. The same field still
3131
/// participates in XML mapping unless `@XmlTransient` is also present.
32+
///
33+
/// The server's generated codecs honour it too: a field that points back at its
34+
/// owner is the usual candidate, since writing both ends of that loop never
35+
/// finishes.
36+
@com.codename1.impl.SharedWithBackend
3237
@Retention(RetentionPolicy.CLASS)
3338
@Target(ElementType.FIELD)
3439
public @interface JsonIgnore {

‎CodenameOne/src/com/codename1/annotations/JsonProperty.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
/// Renames a `@Mapped` field in the JSON projection. The default JSON key is
3131
/// the field name; `@JsonProperty` lets a field map to `snake_case` or any
3232
/// alternative spelling without touching the Java identifier.
33+
///
34+
/// The server's generated codecs read it too, so a class shared between an app
35+
/// and its backend has one JSON form on both sides.
36+
@com.codename1.impl.SharedWithBackend
3337
@Retention(RetentionPolicy.CLASS)
3438
@Target(ElementType.FIELD)
3539
public @interface JsonProperty {

‎docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/DatabaseSnippets.java‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ public final class DatabaseSnippets {
3434
private DatabaseSnippets() {
3535
}
3636

37-
public static List open() throws IOException {
37+
public static List open(DataSource db) throws IOException {
3838
// tag::backend-database[]
39-
DataSource db = DataSource.open(System.getenv("DATABASE_URL")); // or ":memory:"
40-
39+
// db is the pool the server opened from cn1.datasource.url and injected
4140
List rows = db.query("SELECT id, body FROM note WHERE id > ?",
4241
new Object[] { Integer.valueOf(10) });
4342
// end::backend-database[]

‎docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/Notes.java‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,18 @@
3232
import com.codename1.backend.annotations.ResponseStatus;
3333
import com.codename1.backend.annotations.RestController;
3434
import java.util.ArrayList;
35+
import java.util.Collections;
3536
import java.util.LinkedHashMap;
3637
import java.util.List;
3738
import java.util.Map;
38-
import java.util.concurrent.ConcurrentHashMap;
3939
import java.util.concurrent.atomic.AtomicLong;
4040

4141
// tag::backend-first-server[]
4242
@RestController
4343
@RequestMapping("/notes")
4444
public class Notes {
45-
private final Map<Long, Map> store = new ConcurrentHashMap<Long, Map>();
45+
private final Map<Long, Map> store =
46+
Collections.synchronizedMap(new LinkedHashMap<Long, Map>());
4647
private final AtomicLong nextId = new AtomicLong(1);
4748

4849
@GetMapping("/healthz")
@@ -58,11 +59,13 @@ public Map read(@PathVariable("id") long id) {
5859
@GetMapping
5960
public List list(@RequestParam(value = "limit", defaultValue = "20") int limit) {
6061
List page = new ArrayList();
61-
for (Map note : store.values()) {
62-
if (page.size() >= limit) {
63-
break;
62+
synchronized (store) { // iterating needs the map's own lock
63+
for (Map note : store.values()) {
64+
if (page.size() >= limit) {
65+
break;
66+
}
67+
page.add(note);
6468
}
65-
page.add(note);
6669
}
6770
return page;
6871
}

‎docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/NotesEndpoint.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
*/
2323
package com.codenameone.developerguide.backend;
2424

25+
import java.util.Collections;
26+
import java.util.HashMap;
2527
import java.util.Map;
26-
import java.util.concurrent.ConcurrentHashMap;
2728

2829
// tag::backend-contract-server[]
2930
public class NotesEndpoint implements NotesApiServer {
30-
private final Map<String, Note> notes = new ConcurrentHashMap<String, Note>();
31+
private final Map<String, Note> notes =
32+
Collections.synchronizedMap(new HashMap<String, Note>());
3133

3234
public Note note(String id) { // no callback: this IS the server
3335
return notes.get(id);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
* This code is free software; you can redistribute it and/or modify it
5+
* under the terms of the GNU General Public License version 2 only, as
6+
* published by the Free Software Foundation. Codename One designates this
7+
* particular file as subject to the "Classpath" exception as provided
8+
* by Oracle in the LICENSE file that accompanied this code.
9+
*
10+
* This code is distributed in the hope that it will be useful, but WITHOUT
11+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13+
* version 2 for more details (a copy is included in the LICENSE file that
14+
* accompanied this code).
15+
*
16+
* You should have received a copy of the GNU General Public License version
17+
* 2 along with this work; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
19+
*
20+
* Please contact Codename One through http://www.codenameone.com/ if you
21+
* need additional information or have any questions.
22+
*/
23+
package com.codenameone.developerguide.backend;
24+
25+
import com.codename1.backend.HttpServer;
26+
import com.codename1.backend.annotations.DeleteMapping;
27+
import com.codename1.backend.annotations.GetMapping;
28+
import com.codename1.backend.annotations.PathVariable;
29+
import com.codename1.backend.annotations.PostMapping;
30+
import com.codename1.backend.annotations.RequestBody;
31+
import com.codename1.backend.annotations.RequestHeader;
32+
import com.codename1.backend.annotations.RequestMapping;
33+
import com.codename1.backend.annotations.RequestParam;
34+
import com.codename1.backend.annotations.ResponseStatus;
35+
import com.codename1.backend.annotations.RestController;
36+
37+
import java.util.ArrayList;
38+
import java.util.LinkedHashMap;
39+
import java.util.List;
40+
import java.util.Map;
41+
42+
// tag::backend-web-mappings[]
43+
@RestController
44+
@RequestMapping("/api/products")
45+
public class Products {
46+
private final Map<Long, Map<String, Object>> products =
47+
new LinkedHashMap<Long, Map<String, Object>>();
48+
49+
@GetMapping("/{id}") // GET /api/products/42
50+
public synchronized Map<String, Object> get(@PathVariable("id") long id) {
51+
return products.get(Long.valueOf(id)); // null answers 404
52+
}
53+
54+
@GetMapping("/search") // GET /api/products/search?q=mug
55+
public synchronized List<Map<String, Object>> search(
56+
@RequestParam(value = "q", required = false) String query,
57+
@RequestParam(value = "limit", defaultValue = "20") int limit) {
58+
List<Map<String, Object>> out = new ArrayList<Map<String, Object>>();
59+
for (Map<String, Object> p : products.values()) {
60+
if (out.size() < limit
61+
&& (query == null || String.valueOf(p.get("name")).contains(query))) {
62+
out.add(p);
63+
}
64+
}
65+
return out;
66+
}
67+
68+
@PostMapping // POST /api/products
69+
@ResponseStatus(201)
70+
public synchronized Map<String, Object> create(
71+
@RequestBody Map<String, Object> body,
72+
@RequestHeader(value = "Idempotency-Key", required = false) String key) {
73+
Long id = Long.valueOf(products.size() + 1);
74+
Map<String, Object> product = new LinkedHashMap<String, Object>(body);
75+
product.put("id", id);
76+
products.put(id, product);
77+
return product;
78+
}
79+
80+
@DeleteMapping("/{id}") // void answers 204
81+
public synchronized void delete(@PathVariable("id") long id) {
82+
products.remove(Long.valueOf(id));
83+
}
84+
85+
@GetMapping("/{id}/label")
86+
public HttpServer.Response label(HttpServer.Request request, @PathVariable("id") long id) {
87+
byte[] text = ("product " + id).getBytes();
88+
return request.respond(200, "text/plain; charset=utf-8", text);
89+
}
90+
}
91+
// end::backend-web-mappings[]

‎docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/PushFeedback.java‎

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@
2424

2525
import com.codename1.backend.Crypto;
2626
import com.codename1.backend.HttpServer;
27+
import com.codename1.backend.Json;
2728
import com.codename1.backend.annotations.PostMapping;
2829
import com.codename1.backend.annotations.RequestMapping;
2930
import com.codename1.backend.annotations.RestController;
30-
import com.codename1.io.JSONParser;
31+
import com.codename1.backend.annotations.Value;
3132
import java.nio.charset.StandardCharsets;
3233
import java.util.List;
3334
import java.util.Map;
@@ -57,25 +58,35 @@ void removeTargetAndMarkApplied(String eventKey, String provider, String target)
5758
throws Exception;
5859
}
5960

60-
/** Assigned once at start-up; there is no dependency injection here. */
61-
static DeviceStore store;
61+
private final DeviceStore store;
6262

63-
/** The signing secret shown in Push > Settings. Read it from configuration. */
64-
private static final String SECRET = System.getenv("CN1_PUSH_CALLBACK_SECRET");
63+
/** The signing secret shown in Push > Settings. */
64+
private final String secret;
65+
66+
/**
67+
* Both are injected: the store is your bean implementing DeviceStore, and
68+
* the secret is read from cn1.push.callbackSecret, which the environment
69+
* variable CN1_PUSH_CALLBACKSECRET overrides.
70+
*/
71+
public PushFeedback(DeviceStore store,
72+
@Value("${cn1.push.callbackSecret}") String secret) {
73+
this.store = store;
74+
this.secret = secret;
75+
}
6576

6677
/** Reject a digest whose timestamp is older than this, to bound replay. */
6778
private static final long MAX_AGE_MS = 5 * 60 * 1000L;
6879

6980
@PostMapping("/feedback")
7081
public HttpServer.Response feedback(HttpServer.Request request) throws Exception {
7182
String body = request.getBody();
72-
if (!verified(request.getHeader("X-CN1-Signature"), body)) {
83+
if (!verified(request.getHeader("X-CN1-Signature"), body, secret)) {
7384
// Anything but 2xx keeps the window at the sender and resends it,
7485
// which is what you want while a secret rotation is half-applied.
7586
return new HttpServer.Response(401, "text/plain",
7687
"bad signature".getBytes(StandardCharsets.UTF_8));
7788
}
78-
Map digest = JSONParser.parseJSON(body);
89+
Map digest = Json.parseObject(body);
7990
List events = (List) digest.get("events");
8091
if (events != null) {
8192
for (Object entry : events) {
@@ -118,8 +129,9 @@ public HttpServer.Response feedback(HttpServer.Request request) throws Exception
118129

119130
// throws, because String.getBytes(Charset) is a CHECKED throw in the
120131
// ParparVM class library even though it is not one on a JVM.
121-
private static boolean verified(String header, String body) throws Exception {
122-
if (header == null || SECRET == null) {
132+
private static boolean verified(String header, String body, String secret)
133+
throws Exception {
134+
if (header == null || secret == null || secret.length() == 0) {
123135
return false;
124136
}
125137
long timestamp = 0;
@@ -156,7 +168,7 @@ private static boolean verified(String header, String body) throws Exception {
156168
// no JCE. equalsConstantTime is here for the same reason a hand-written
157169
// loop would be -- an early exit on the first differing byte lets a MAC
158170
// be forged one byte at a time.
159-
byte[] expected = Crypto.hmacSha256(SECRET.getBytes(StandardCharsets.UTF_8),
171+
byte[] expected = Crypto.hmacSha256(secret.getBytes(StandardCharsets.UTF_8),
160172
(timestamp + "." + body).getBytes(StandardCharsets.UTF_8));
161173
return Crypto.equalsConstantTime(expected, decodeHex(provided));
162174
}

‎docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/TracingSnippets.java‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,10 @@
2222
*/
2323
package com.codenameone.developerguide.backend;
2424

25-
import com.codename1.backend.Config;
26-
import com.codename1.backend.Tracing;
2725
import com.codename1.backend.annotations.GetMapping;
2826
import com.codename1.backend.annotations.OpenTelemetry;
2927
import com.codename1.backend.annotations.PathVariable;
3028
import com.codename1.backend.annotations.RestController;
31-
import com.codename1.backend.otel.OtlpTracer;
3229

3330
/// The Backend chapter's tracing examples, compiled so they cannot drift. This
3431
/// module runs no annotation processing, which is what lets `@OpenTelemetry` appear
@@ -51,10 +48,4 @@ public String note(@PathVariable("id") String id) {
5148
// end::backend-otel-annotation[]
5249
}
5350

54-
public static void installByHand() throws Exception {
55-
// tag::backend-otel-install[]
56-
Tracing.install(OtlpTracer.open(Config.load(), "notes"));
57-
// end::backend-otel-install[]
58-
}
59-
6051
}

‎docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/WebSocketSnippets.java‎

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@
2222
*/
2323
package com.codenameone.developerguide.backend;
2424

25-
import com.codename1.backend.Backend;
26-
import com.codename1.backend.DataSource;
27-
import com.codename1.backend.orm.EntityManager;
28-
import com.codename1.backend.HttpServer;
2925
import com.codename1.backend.WebSocket;
3026
import com.codename1.backend.WebSocketSession;
3127
import com.codename1.backend.annotations.WebSocketMapping;
@@ -58,19 +54,6 @@ public void onBinary(WebSocketSession session, byte[] message, int offset, int l
5854
}
5955
// end::backend-websocket-echo[]
6056

61-
// tag::backend-websocket-register[]
62-
public static void main(String[] args) throws Exception {
63-
Backend.builder()
64-
.webSockets(new Backend.WebSocketEndpoints() {
65-
public void register(HttpServer.WebSocketRegistry registry,
66-
DataSource dataSource, EntityManager entities) {
67-
registry.route("/echo", new Echo());
68-
}
69-
})
70-
.run();
71-
}
72-
// end::backend-websocket-register[]
73-
7457
// tag::backend-websocket-annotated[]
7558
@WebSocketMapping("/chat")
7659
public static final class ChatEndpoint implements WebSocket {
@@ -143,18 +126,4 @@ public void onBinary(WebSocketSession session, byte[] message, int offset, int l
143126
}
144127
// end::backend-websocket-subprotocol[]
145128

146-
// tag::backend-websocket-raw[]
147-
public static void serveForever() throws Exception {
148-
HttpServer server = HttpServer.start(null, 8080, 512, 16, new HttpServer.Handler() {
149-
public HttpServer.Response handle(HttpServer.Request request) {
150-
return HttpServer.Response.text(200, "ok");
151-
}
152-
}, null, new HttpServer.WebSocketRoutes() {
153-
public void register(HttpServer.WebSocketRegistry registry) {
154-
registry.route("/echo", new Echo());
155-
}
156-
});
157-
server.awaitTermination();
158-
}
159-
// end::backend-websocket-raw[]
160129
}

0 commit comments

Comments
 (0)