feat(engine): swap a module's code instead of reinstalling it - #747
feat(engine): swap a module's code instead of reinstalling it#747romain-pm wants to merge 3 commits into
Conversation
Rebuilding a module takes tens of milliseconds and reinstalling it takes seconds: a tarball is packed, turned into an OSGi bundle, written to the JCR, and the module is restarted. The rebuild is not what makes the loop slow, so `yarn dev:fast` keeps it and replaces everything after it. The engine gains a development endpoint at /modules/jsm-dev. It takes the server bundle a build just produced and swaps it into the running GraalVM engine: the Source for that bundle is replaced in place, the context pool version is bumped so pooled contexts recycle, and the registrars re-run — unregister before register, since they accumulate their OSGi services. It then fires the redeploy event, because nothing else does on a swap and the HTML fragment cache would keep serving the views that were just replaced; development mode does not disable that cache. A registrar whose registration also performs a one-off effect can now opt out of the hot path with runsOnHotReload(). The endpoint also proxies the module's files to the development server, so client bundles, the stylesheet and emitted assets come from the build that just ran rather than from the installed bundle — buildModuleFileUrl points there while a session is open. Pages poll a reload counter and reload themselves once a swap lands. It executes JavaScript that the caller supplies, so it answers only when its own OSGi property enables it AND Jahia runs in development mode, and pushing code requires the root user. Development mode alone would not do: Jahia's operating mode defaults to development, including in the published images. ViewsRegistrar.viewsPerBundle becomes a ConcurrentHashMap: it is read on every render, and the loop turns its one-write-per-deploy into a write per save. Node type definitions, imported content, locales, resource bundles and OSGi configurations still come from the installed bundle and still need a redeploy. The loop says so when one of them changes.
Everything here was found by running the loop against Jahia 8.2.3 in Docker; each was invisible to unit tests. The proxy forwarded the wrong path. Jahia dispatches into OSGi through a proxy servlet mapped at /modules, so the request arrives without that prefix and the development server answered 404 for every asset. The path is now rebuilt from the prefix the servlet owns. The listener published no service under its own type, only BundleListener, so the servlet's reference to it was never satisfied and the whole endpoint stayed silent — a 404 that looked exactly like a disabled one. A session could be opened onto an origin Jahia cannot reach. Jahia in a container does not share the developer's localhost, so the endpoint now probes the origin before accepting it, which is what lets the CLI fall back to the name the container knows the host by. The CLI pushed twice per rebuild, because Vite prints "built in" once per environment. Two swaps racing left the context pool unable to validate any context, and the push failed with a pool error. It now watches the server bundle itself, coalesces, and never overlaps two pushes; reloadServerBundle is synchronized so the engine holds that line whatever the caller does. The build's own watch callback still packed and deployed the module on every save, putting back the round trip this loop removes: it now stands down when the loop drives the build. Measured after the fixes, on the hydrogen sample: edit to visible 590 / 462 / 435 ms, of which 124-218 ms is the engine swap. The same loop was 4.1-4.9 s through pack and provisioning.
📝 Documentation GuidelinesThank you for contributing to our documentation! To ensure your contributions meet our standards, please review these resources:
This comment is posted automatically when changes are detected in the |
🦜 Chachalog
|
commit: |
|
Verified in Page Builder (jContent, Jahia 8.2.3), not just live rendering:
So the reload script does not fight the editor, which was the open risk. One cost worth noting: each open page polls |
Attaching changes what every page of that module renders — its files are addressed on the development server, and the reload script goes in — but nothing told Jahia to forget what it had already rendered. A page cached before the session opened kept the old URLs and, having no reload script, never learned to reload itself: the developer edited, the engine swapped, and the browser sat there showing the previous build. Attaching and detaching now drop what Jahia derived from the module, the same way a swap already did. Found in Page Builder, where the frame served a render made while detached.
|
Live test in Page Builder found one more real bug, now fixed in Attaching a session changes what every page renders — the module's files move to the development server and the reload script goes in — but nothing invalidated Jahia's output cache at that moment; only a swap did. A page cached while detached therefore kept the installed-bundle URLs and, carrying no reload script, never learned to reload itself. Symptom: you edit, the engine reports the swap, and the browser sits on the previous build until a hard refresh. Attach and detach now drop what Jahia derived from the module, exactly as a swap does. Verified afterwards, with Page Builder open the whole time: editing For the record, the Page Builder frame is |
|
CI history note: the first run of It was a fixture flake, not a regression: the suite passed on All checks green now: 13 pass, 0 fail. |
Closes #699 (first stage).
yarn watchrebuilds a module in tens of milliseconds and then spends seconds reinstalling it: pack a tarball, turn it into an OSGi bundle, write it to the JCR, restart the module.yarn dev:fastkeeps the rebuild and replaces everything after it.dist/server/index.jsis pushed to a new engine endpoint, which replaces the GraalSourcein place, bumps the context-pool version and re-runs the registrars.distthrough the engine, so the browser reads the build that just ran.buildModuleFileUrlpoints there while a session is open, and back at the installed bundle when it closes.Measured
On
samples/hydrogen, against Jahia 8.2.3 in Docker: edit → visible 590 / 462 / 435 ms, of which 124–218 ms is the engine swap. The same loop through pack + provisioning measures 4.1–4.9 s.Correctness details worth reviewing
AbstractServiceRegistraraccumulates its OSGi registrations, so a second register would publish every render filter twice.TemplatePackageRedeployedEvent. Nothing else does — a swap raises no bundle event — and development mode does not disable the HTML fragment cache, so without it the old view keeps being served.Registrar.runsOnHotReload()lets a registrar opt out of the hot path. Content patches will need it: their records are terminal, so a per-save loop would burn them.ViewsRegistrar.viewsPerBundlebecomes aConcurrentHashMap— read on every render, and this loop turns one write per deploy into one per save.reloadServerBundleis synchronized: two swaps racing leave the context pool unable to validate any context.Security
The endpoint evaluates JavaScript the caller supplies, inside the Jahia JVM. It answers only when its own OSGi property enables it and Jahia runs in development mode, and pushing code requires the root user. Development mode alone would not do:
operatingModedefaults to development inSettingsBean, in the shippedjahia.propertiesand in the published images. Activation logs a WARN naming the open endpoint. While a session is open, the module'sdistis readable by anyone who can reach that Jahia — stated in the guide.Scope
Node type definitions, imported content, locales, resource bundles, icons,
.cfgandpackage.jsonall come from the installed bundle and still need a redeploy; the loop names the file when one of them changes. #742 would make that redeploy cheap too.A Vite dev server with HMR is deliberately not here — it needs #740 first (the engine ships no
react/jsx-dev-runtimeand a production React build, so islands would not hydrate and Fast Refresh cannot work). Tracked as #741.Note for review
server.devis declared by hand inglobals.d.tsrather than imported from the generated Java typings: java-ts-bind emitsDevHelperwithout its methods and I have not found why. Worth a look from someone who knows that generator.