Skip to content

Commit cd0a7ee

Browse files
committed
Merged in bugfix/EL-365-datastore-not-accessible-at-runti (pull request #185)
EL-365 Made morphia accessible via the mongo dao ElementDependency
2 parents 5db7ebd + c8e072b commit cd0a7ee

50 files changed

Lines changed: 2417 additions & 384 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.

common-servlet/src/main/java/dev/getelements/elements/servlet/security/HttpServletAuthenticationFilter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public void doFilter(final ServletRequest _request,
7878
}
7979

8080
private void fail(final HttpServletResponse response, final Exception ex) throws IOException {
81+
if (response.isCommitted()) {
82+
logger.debug("Response already committed; suppressing secondary error write.", ex);
83+
return;
84+
}
8185
final var error = new ErrorResponse();
8286
error.setCode(UNKNOWN.toString());
8387
error.setMessage(ex.getMessage());
@@ -86,6 +90,10 @@ private void fail(final HttpServletResponse response, final Exception ex) throws
8690
}
8791

8892
private void fail(final HttpServletResponse response, final BaseException ex) throws IOException {
93+
if (response.isCommitted()) {
94+
logger.debug("Response already committed; suppressing secondary error write.", ex);
95+
return;
96+
}
8997
final var error = new ErrorResponse();
9098
error.setCode(ex.getCode().toString());
9199
error.setMessage(ex.getMessage());

deployment-jetty/src/main/java/dev/getelements/elements/deployment/jetty/JettyElementContainerService.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,23 @@ public List<ContainerRecord> getActiveContainers() {
123123
try (var mon = Monitor.enter(lock)) {
124124
return activeContainers.values()
125125
.stream()
126-
.map(active -> new ContainerRecord(
127-
active.runtime(),
128-
active.status(),
129-
active.uris(),
130-
active.logs(),
131-
active.errors(),
132-
active.elements()
133-
))
126+
.map(active -> {
127+
// Override status to LOADING if any loader still has background work
128+
// in progress for any element in this container (e.g. async Jersey start).
129+
final var status = active.status() != FAILED
130+
&& active.elements().stream().anyMatch(
131+
e -> getLoaders().stream().anyMatch(l -> l.hasPendingWork(e)))
132+
? LOADING
133+
: active.status();
134+
return new ContainerRecord(
135+
active.runtime(),
136+
status,
137+
active.uris(),
138+
active.logs(),
139+
active.errors(),
140+
active.elements()
141+
);
142+
})
134143
.toList();
135144
}
136145
}
@@ -174,7 +183,11 @@ public void onRuntimeLoaded(final ElementRuntimeService.RuntimeRecord runtimeRec
174183
// Check if already mounted
175184
final var existing = activeContainers.get(deploymentId);
176185
if (existing != null) {
177-
logger.warn("Container already mounted for deployment: {}, remounting", deploymentId);
186+
if (existing.runtime().deployment().version() == runtimeRecord.deployment().version()) {
187+
logger.debug("Container already mounted at current version for deployment: {}, skipping event-driven mount", deploymentId);
188+
return;
189+
}
190+
logger.info("Container mounted at stale version for deployment: {}, remounting", deploymentId);
178191
doUnmount(deploymentId);
179192
}
180193

@@ -218,9 +231,22 @@ public void onRuntimeLoaded(final ElementRuntimeService.RuntimeRecord runtimeRec
218231
public void onRuntimeUnloaded(final String deploymentId) {
219232
boolean unmounted = false;
220233

234+
// Check BEFORE acquiring the container lock whether the deployment is still active
235+
// in the runtime service. If it is, the deployment was reloaded (not truly removed),
236+
// and the RuntimeUnloaded event is the "old version" notification from reconcile().
237+
// In that case we must not unmount the freshly-loaded container.
238+
final var stillActive = getElementRuntimeService().getActiveRuntimes()
239+
.stream()
240+
.anyMatch(r -> r.deployment().id().equals(deploymentId));
241+
221242
try (var mon = Monitor.enter(lock)) {
222243
logger.info("Received RuntimeUnloaded event for deployment: {}", deploymentId);
223244

245+
if (stillActive) {
246+
logger.debug("Deployment {} was reloaded; skipping event-driven unmount", deploymentId);
247+
return;
248+
}
249+
224250
// Unmount the container if it exists
225251
final var existing = activeContainers.get(deploymentId);
226252
if (existing != null) {
@@ -335,13 +361,16 @@ private void doMount(final RuntimeRecord runtime) {
335361
logs.add("Starting container mount for deployment " + deploymentId);
336362

337363
try {
364+
// Identity set to deduplicate elements across multiple loaders that may report the same element.
365+
final var seenElements = Collections.newSetFromMap(new IdentityHashMap<>());
366+
338367
// Create pending deployment context
339368
final var pending = new Loader.PendingDeployment(
340369
uris::add,
341370
logs::add,
342371
warnings::add,
343372
errors::add,
344-
elements::add
373+
element -> { if (seenElements.add(element)) elements.add(element); }
345374
);
346375

347376
// Run all loaders

deployment-jetty/src/main/java/dev/getelements/elements/deployment/jetty/StandardElementRuntimeService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,8 @@ private List<Element> loadElements(final DeploymentContext context) {
498498

499499
// PHASE A: STAGING
500500
// Collect all element source paths into a list
501-
context.log("=== Phase A: Staging all element sources ===");
501+
context.log("=== Staging all element sources ===");
502+
final var phaseAStartNs = System.nanoTime();
502503

503504
// Strategy 1: Stage uploaded ELM from LargeObject if present
504505
if (hasElmFromLargeObject(context.deployment())) {
@@ -573,10 +574,12 @@ private List<Element> loadElements(final DeploymentContext context) {
573574
}
574575

575576
context.log("Staged " + context.elementPaths().size() + " element path(s)");
577+
context.log("Staging completed in " + (System.nanoTime() - phaseAStartNs) / 1_000_000 + " ms");
576578

577579
// PHASE B: LOADING
578580
// Load all elements in a single operation
579-
context.log("=== Phase B: Loading all elements ===");
581+
context.log("=== Loading all elements ===");
582+
final var phaseBStartNs = System.nanoTime();
580583

581584
final List<Element> allElements;
582585

@@ -619,6 +622,7 @@ private List<Element> loadElements(final DeploymentContext context) {
619622
));
620623
}
621624

625+
context.log("Guice/classloader completed in " + (System.nanoTime() - phaseBStartNs) / 1_000_000 + " ms");
622626
context.log("Successfully loaded " + allElements.size() + " element(s)");
623627

624628
} catch (Exception ex) {
@@ -1257,6 +1261,9 @@ public void close() {
12571261
for (final FileSystem fileSystem : filesystems) {
12581262
try {
12591263
fileSystem.close();
1264+
} catch (java.nio.file.NoSuchFileException ex) {
1265+
logger.debug("File system backing file already gone for deployment {} (file was likely replaced): {}",
1266+
deployment.id(), ex.getFile());
12601267
} catch (IOException ex) {
12611268
logger.warn("Failed to close file system {} for deployment {}", fileSystem, deployment.id(), ex);
12621269
}

deployment-jetty/src/main/java/dev/getelements/elements/deployment/jetty/guice/JettySdkElementModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import dev.getelements.elements.deployment.jetty.StandardElementRuntimeService;
66
import dev.getelements.elements.deployment.jetty.loader.AuthFilterFeature;
77
import dev.getelements.elements.deployment.jetty.loader.HttpPathRegistry;
8+
import dev.getelements.elements.deployment.jetty.loader.ElementEntityLoader;
89
import dev.getelements.elements.deployment.jetty.loader.JakartaRsLoader;
910
import dev.getelements.elements.deployment.jetty.loader.JakartaWebsocketLoader;
1011
import dev.getelements.elements.deployment.jetty.loader.Loader;
@@ -46,6 +47,7 @@ protected void configureElement() {
4647
.asEagerSingleton();
4748

4849
final var loaders = newSetBinder(binder(), Loader.class);
50+
loaders.addBinding().to(ElementEntityLoader.class);
4951
loaders.addBinding().to(JakartaRsLoader.class);
5052
loaders.addBinding().to(JakartaWebsocketLoader.class);
5153
loaders.addBinding().to(StaticContentLoader.UI.class);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package dev.getelements.elements.deployment.jetty.loader;
2+
3+
import org.eclipse.jetty.server.Handler;
4+
import org.eclipse.jetty.server.Request;
5+
import org.eclipse.jetty.server.Response;
6+
import org.eclipse.jetty.util.Callback;
7+
8+
/**
9+
* A Jetty {@link Handler.Wrapper} that switches the thread context classloader (TCCL) to the
10+
* element's {@link ClassLoader} for the duration of each request, restoring the original
11+
* classloader in a {@code finally} block.
12+
*
13+
* <p>This is required for element-hosted services that share the platform Morphia
14+
* {@code Datastore}. Morphia's {@code DiscriminatorLookup} falls back to
15+
* {@code Class.forName(discriminatorValue)} when an entity class is not pre-registered; that
16+
* call resolves classes using the calling thread's context classloader. Without this wrapper it
17+
* would use the Jetty thread pool's classloader (the platform classloader), making
18+
* element-specific entity classes invisible during document decoding.
19+
*
20+
* <p>Placing this wrapper outside the {@link org.eclipse.jetty.ee10.servlet.ServletContextHandler}
21+
* ensures the TCCL is set before Jetty's own context scope handling, and also covers any
22+
* non-servlet code paths (e.g. async dispatch, error handling) that Jetty may route through
23+
* the handler chain without re-entering the servlet context.
24+
*/
25+
class ClassLoaderSwitchHandler extends Handler.Wrapper {
26+
27+
private final ClassLoader classLoader;
28+
29+
ClassLoaderSwitchHandler(final ClassLoader classLoader, final Handler handler) {
30+
super(handler);
31+
this.classLoader = classLoader;
32+
}
33+
34+
@Override
35+
public boolean handle(final Request request, final Response response, final Callback callback)
36+
throws Exception {
37+
final var thread = Thread.currentThread();
38+
final var previous = thread.getContextClassLoader();
39+
// Wrap the callback so the element's TCCL is also active when Jetty invokes
40+
// succeeded()/failed() — which may happen on a different thread for async requests
41+
// (e.g. async dispatch, error handling, or non-blocking I/O completion).
42+
final var wrapped = Callback.from(
43+
() -> withTccl(callback::succeeded),
44+
x -> withTccl(() -> callback.failed(x))
45+
);
46+
try {
47+
thread.setContextClassLoader(classLoader);
48+
return super.handle(request, response, wrapped);
49+
} finally {
50+
thread.setContextClassLoader(previous);
51+
}
52+
}
53+
54+
private void withTccl(final Runnable action) {
55+
final var thread = Thread.currentThread();
56+
final var previous = thread.getContextClassLoader();
57+
try {
58+
thread.setContextClassLoader(classLoader);
59+
action.run();
60+
} finally {
61+
thread.setContextClassLoader(previous);
62+
}
63+
}
64+
65+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package dev.getelements.elements.deployment.jetty.loader;
2+
3+
import dev.getelements.elements.sdk.Element;
4+
import dev.getelements.elements.sdk.dao.ElementEntityRegistrar;
5+
import dev.getelements.elements.sdk.deployment.ElementRuntimeService.RuntimeRecord;
6+
import jakarta.inject.Inject;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
/**
11+
* A {@link Loader} that registers an element's entity classes with the platform database mapper
12+
* on load and deregisters them on unload.
13+
*
14+
* <p>This loader is independent of the RS and WS loaders so that entity registration applies
15+
* to all element types uniformly.
16+
*/
17+
public class ElementEntityLoader implements Loader {
18+
19+
private static final Logger logger = LoggerFactory.getLogger(ElementEntityLoader.class);
20+
21+
private ElementEntityRegistrar elementEntityRegistrar;
22+
23+
@Override
24+
public void load(final PendingDeployment pending, final RuntimeRecord record, final Element element) {
25+
if (elementEntityRegistrar != null) {
26+
elementEntityRegistrar.registerEntityClasses(element);
27+
}
28+
}
29+
30+
@Override
31+
public void unload(final Element element) {
32+
if (elementEntityRegistrar != null) {
33+
elementEntityRegistrar.unregisterEntityClasses(element);
34+
} else {
35+
logger.debug("No ElementEntityRegistrar bound; skipping entity deregistration for {}",
36+
element.getElementRecord().definition().name());
37+
}
38+
}
39+
40+
public ElementEntityRegistrar getElementEntityRegistrar() {
41+
return elementEntityRegistrar;
42+
}
43+
44+
@Inject
45+
public void setElementEntityRegistrar(final ElementEntityRegistrar elementEntityRegistrar) {
46+
this.elementEntityRegistrar = elementEntityRegistrar;
47+
}
48+
49+
}

0 commit comments

Comments
 (0)