Conversation
A spooler task does not have to come from the core. Since mail moved out into the Mail Extension, MailSpoolerTask is contributed by an extension, and an extension's classes are not visible to the core class loader: they live either in the extension's own OSGi bundle or - when the extension declares its implementation through Maven coordinates, as the mail extension does with <tag-class maven="org.lucee:mail:..."> - in one of the RPC class loaders. getTask() resolved classes against the core class loader alone, so reading the task back threw ClassNotFoundException, and because the task file is then deleted the queued mail disappeared. Nothing surfaced to the caller: cfmail returned normally and the spooler simply had nothing left to run. Fall back to ClassUtil (bundles) and then to the class loaders PhysicalClassLoaderFactory has cached (Maven coordinates), keeping the core class loader as the first choice. Everything the task references then has to be resolved through whichever loader was found, so that loader is remembered for the rest of the stream and preferred from then on. Resolving each class independently is not enough: the classes come out of different loaders and the same name becomes two distinct types, which fails later as "cannot assign instance of jakarta.mail.internet.InternetAddress to field SMTPClient.from of type jakarta.mail.internet.InternetAddress". Preferring the extension loader is safe for core classes, since it delegates to the core loader for anything it does not provide itself. This is the other half of 032ca3d: storing a task that cannot be persisted no longer loses it, but until now a task that stored fine was still lost on the way back in.
ba17ac6 to
f146aec
Compare
|
This is okay to close if you want. At least the test case should help. |
Replaces the class loader search: the engine already receives the live task in add(), so it remembers that class's loader and deserializes the file with it. No other loaders are consulted. A file whose task class is not loaded is skipped instead of deleted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
I've had claude rework/simplify this PR now, ready for re-review. Passed the full test suite again |
|
A fix for LDEV-6455 has landed on Approach: on deserialization, when the core engine class loader cannot resolve a task class, the class is resolved on demand against the currently installed extension — via the cfmail tag's registered Why resolve on demand rather than cache the creating loader: a spool task is written now and read later, and the read can happen in a different class loader context than the write (after a restart, or after an extension update). So the read must resolve the class against whatever loader currently provides it, not a loader remembered from write time. Resolving from the current tag registration works straight after a restart (the tag library is registered at startup) and tracks extension updates. Also added |
|
@shane-tw thanks for digging into this and for the clear writeup — the diagnosis is exactly right, and your I went a different route for the fix, and I want to explain why we can't take the cached-loader approach. Remembering the loader that created the task (the
So the read has to resolve the class fresh, against whatever loader currently provides it, rather than a loader remembered from write time. That is what the landed fix does (resolve via the current cfmail tag registration), so it works right after a restart and tracks extension updates. The one thing no loader strategy can fix is serialization format compatibility across an incompatible class change ( Your "keep the file on |
Ticket
LDEV-6455
Problem
SpoolerEngineImpl.getTask()deserializes task files with the core class loader. Since mail moved into an extension,MailSpoolerTaskand thejakarta.mailtypes it holds live in the extension's RPC class loader (declared viamaven="org.lucee:mail:1.1.0.8-RC"), so reading the file fails withClassNotFoundException: org.lucee.extension.mail.spooler.MailSpoolerTaskand the file is deleted. Spooledcfmailis stored fine and then silently lost.Fix
The engine is handed the live task in
add(), so it already knows the loader that defines the task class. It now remembers that loader per task class and deserializes each file with the loader of the class that wrote it, the same rule Java serialization itself follows. The core loader remains the default, so core tasks are read exactly as before. No other class loaders are consulted.A file whose task class cannot be resolved is no longer deleted. It is skipped, logged, and read once the class becomes known again (for example after a restart, as soon as the extension queues its next task). Files that fail for any other reason are still deleted as before.
Verification
test/tickets/LDEV6455.cfcspools a mail with asendTimea day out, then lists spooler tasks, which reads the file back. Fails on7.1as it stands, passes with this change.The second commit replaces the class loader search from the first one; the first commit is kept for history.
🤖 Generated with Claude Code