This is an automated email from the ASF dual-hosted git repository.
mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-plugins.git
The following commit(s) were added to refs/heads/trunk by this push:
new e379514a7 Reload the entity readers through their reload methods
e379514a7 is described below
commit e379514a72cfbaae8df180bf2f14024b3d73fdb7
Author: Mridul Pathak <[email protected]>
AuthorDate: Sun Nov 1 14:19:08 2026 +0530
Reload the entity readers through their reload methods
EntityModelReloader reset the private entityCache and groupCache fields of
ModelReader and ModelGroupReader by reflection and rebuilt them, re-nulling
them on failure. It now calls the readers' new reload methods for the reader of
every configured delegator, which keep the previous model in use if a save is
broken, and no longer reflects into the readers. The entry points are renamed
reloadEntityModels and reloadGroupModels, and the README describes the new
behaviour.
---
devreload/README.md | 26 +--
.../apache/ofbiz/devreload/DevReloadContainer.java | 26 +--
.../ofbiz/devreload/EntityModelReloader.java | 253 ++++++---------------
3 files changed, 90 insertions(+), 215 deletions(-)
diff --git a/devreload/README.md b/devreload/README.md
index 26d42bd4c..1c3e861a1 100644
--- a/devreload/README.md
+++ b/devreload/README.md
@@ -140,7 +140,7 @@ sixth class, since it's just one more `UtilCache` clear on
`DevReloadContainer`
| `HotSwapAgent` | A tiny self-attaching Java agent that gives
`DevReloadContainer` access to `Instrumentation.redefineClasses` — the same
mechanism an IDE debugger uses for HotSwap |
| `Debouncer<T>` | Coalesces rapid-fire change notifications (a burst of
file-system events, or one compile producing several `.class` files) into a
single batched action; shared by all five watch paths below |
| `RecordingFileManager` | Wraps the in-process compiler's file manager to
record exactly which `.class` files it wrote, so the reload step hot-swaps
precisely what was compiled instead of guessing from the source file's name |
-| `EntityModelReloader` | Reflectively resets the already-running
`ModelReader`/`ModelGroupReader` singletons so an `entitydef/*.xml` edit
re-parses on the next access, clears delegator data caches afterwards, and
(opt-in) creates any missing table/column the change needs |
+| `EntityModelReloader` | Reloads the already-running
`ModelReader`/`ModelGroupReader` instances through their `reload()` methods so
an `entitydef/*.xml` edit takes effect at once, clears delegator data caches
afterwards, and (opt-in) creates any missing table/column the change needs |
At startup, `DevReloadContainer` self-attaches `HotSwapAgent` to the running
JVM (via the Attach API — no `-javaagent` flag needed). This grants access to
@@ -156,7 +156,7 @@ this plugin exists.
|---|---|---|---|
| 1. Java source | Every component's `src/main/java` | Compiles the file
in-process, then hot-swaps the resulting class(es) | New method bodies live in
< 1s |
| 2. `services.xml` | Every component's `servicedef/` directory | Clears the
`service.ModelServiceMapByModel` cache | OFBiz re-reads service definitions on
the next call |
-| 3. `entitydef/*.xml` | Every component's `entitydef/` directory
(`entity-resource type="model"`/`"group"` only — not seed/demo data) |
Reflectively resets and eagerly rebuilds the affected
`ModelReader`/`ModelGroupReader` singleton(s), then clears delegator data
caches (see `EntityModelReloader`) | Entity/view-entity/entity-group definition
changes live in < 1s |
+| 3. `entitydef/*.xml` | Every component's `entitydef/` directory
(`entity-resource type="model"`/`"group"` only — not seed/demo data) | Reloads
the affected `ModelReader`/`ModelGroupReader` instance(s) through their
`reload()` methods, then clears delegator data caches (see
`EntityModelReloader`) | Entity/view-entity/entity-group definition changes
live in < 1s |
| 4. `config/*Labels.xml` / `*.properties` | Every component's `config/`
directory (scoped to files ending `Labels.xml` or `.properties` — not other
`config/*.xml` files like `entityengine.xml`) | Clears
`UtilCache.clearCachesThatStartWith("properties.")` | Label/message bundle and
property lookups re-read on the next call |
| 5. Build output (opt-in) | `build/classes/java/main` | Same hot-swap as Path
1 | Picks up `./gradlew -t classes` run in a second terminal |
@@ -188,14 +188,15 @@ Unlike `services.xml` (clear a cache the service engine
looks up by name on ever
call), entity reload can't work that way: every `GenericDelegator` grabs a
direct
object reference to a `ModelReader` at construction time and keeps it, so
clearing
`ModelReader`'s own cache would only affect a brand-new delegator, not the one
-already serving traffic. `EntityModelReloader` instead reflectively nulls the
-already-running `ModelReader`/`ModelGroupReader` singleton's private
parsed-model
-field and eagerly rebuilds it in place — every delegator sharing a reader name
-(almost always `"main"`) picks up the change for free.
+already serving traffic. `EntityModelReloader` instead calls `reload()` on the
+already-running `ModelReader`/`ModelGroupReader` instances — every delegator
sharing a
+reader name (almost always `"main"`) picks up the change for free. The readers
build
+the new model on the side and swap it in only once it is complete, so requests
keep
+being served from the previous model while the reload runs.
A broken save (invalid XML, a view-entity referencing a non-existent member
entity)
-is caught and logged clearly, and self-heals: the very next save (or any
access, at
-reparse cost) retries the parse instead of staying stuck until a restart.
+is caught and logged clearly, and the previous model stays in use: the next
save
+retries the reload, with no restart needed.
Optionally, `-Dofbiz.hotreload.autoUpdateSchema=true` (or
`-Photreload.autoUpdateSchema=true`) also creates any missing table/column a
changed
@@ -309,7 +310,7 @@ Everything `devreload` reads or writes, in one place.
| Repo | Contains | Why |
|---|---|---|
| `plugins/devreload` (a separate repo, dropped into a local checkout) |
`DevReloadContainer.java`, `HotSwapAgent.java`, `Debouncer.java`,
`RecordingFileManager.java`, `EntityModelReloader.java`, `ofbiz-component.xml`,
`build.gradle` (the `ofbizDev` task), `README.md` — everything | OFBiz plugins
are conventionally distributed as separate repos; `ofbiz-framework`'s
`.gitignore` excludes `/plugins/` for exactly this reason |
-| `ofbiz-framework` | Nothing — no files, no diffs | The current design
redefines already-loaded `Class` objects in place, so no framework code ever
needs to ask "is a fresher class available." (An earlier prototype *did* need a
small reflective bridge into `framework/base`, since replaced.)
Entity/view-entity reload takes a related but separate approach: it reflects
into `framework/entity`'s already-running `ModelReader`/`ModelGroupReader`
instances rather than changing them. |
+| `ofbiz-framework` | Nothing — no files, no diffs | The current design
redefines already-loaded `Class` objects in place, so no framework code ever
needs to ask "is a fresher class available." (An earlier prototype *did* need a
small reflective bridge into `framework/base`, since replaced.)
Entity/view-entity reload uses the public `reload()` method of
`framework/entity`'s already-running `ModelReader`/`ModelGroupReader`
instances. |
To use it in a checkout: `git clone <devreload-repo-url> plugins/devreload`,
then run `./gradlew ofbizDev --no-watch-fs`. Deleting `plugins/devreload/` at
@@ -328,8 +329,8 @@ OFBiz.
| One shared `Debouncer` class for all five watch paths | Avoids five
hand-rolled copies of the same concurrency logic that could drift out of sync |
| Explicit `-PdcevmHome`/`DCEVM_HOME` only, no auto-detection | Guessing IDE
install paths breaks silently whenever a vendor changes packaging; one explicit
input is easier to keep working |
| Failed directory watch = log a warning and skip it | No hidden fallback
(like polling); a clear, actionable warning instead of silent degraded behavior
|
-| Reflectively reset `ModelReader`/`ModelGroupReader`'s own parsed-model field
in place, instead of clearing the `UtilCache` that hands them out | Every
`GenericDelegator` grabs a direct object reference to a `ModelReader` at
construction time and keeps it — clearing the lookup cache (the `services.xml`
trick) would only affect a brand-new delegator, never the one already serving
traffic |
-| A failed entity/group rebuild re-nulls the field again instead of leaving it
half-populated | `ModelReader.getEntityCache()` populates its map field
directly as it parses rather than swapping in a finished copy at the end, so a
mid-parse failure would otherwise leave a broken, partial model served forever
with no automatic retry; re-nulling makes the next save (or any access) retry
instead |
+| Reload `ModelReader`/`ModelGroupReader` in place through `reload()`, instead
of clearing the `UtilCache` that hands them out | Every `GenericDelegator`
grabs a direct object reference to a `ModelReader` at construction time and
keeps it — clearing the lookup cache (the `services.xml` trick) would only
affect a brand-new delegator, never the one already serving traffic |
+| A failed entity/group reload keeps the previous model | `reload()` builds
the new model on the side and swaps it in only once complete, so a broken save
never replaces a working model with a partial one, and the next save retries |
| Every entitydef save reloads both the entity model and the group model,
regardless of which file actually changed | Both operations are cheap and a
no-op in effect when nothing of that kind changed; same trade this plugin
already makes elsewhere (see bug fix #10) in exchange for simpler bookkeeping |
| Schema auto-update (`-Dofbiz.hotreload.autoUpdateSchema`) is opt-in, off by
default, and additive-only (`checkDb(..., addMissing=true)`) | It's the one
thing in this plugin that writes to the database automatically; non-destructive
by construction (same call webtools' "Update Database" uses), but still a
bigger blast radius than reloading in-memory definitions, so it stays explicit
rather than bundled into entitydef reload unconditionally |
| Label/properties reload copies the changed file into `hotReloadOutputDir`,
then calls `UtilCache.clearCachesThatStartWith("properties.")` on
`DevReloadContainer` itself — no new `XxxReloader` class | Nothing holds a
direct reference to a parsed bundle the way `GenericDelegator` holds a
`ModelReader`, so no reflection is needed — but a cache clear alone re-reads
Gradle's stale `build/resources/main` copy, not the live source file (confirmed
live); the overlay-copy step reuses the exact [...]
@@ -355,9 +356,8 @@ OFBiz.
| Same class keeps reporting a structural-change warning even after a pure
method-body edit | Once a stock JVM rejects one structural change on a class,
that class stays "stuck" until restart — every subsequent diff against its
still-loaded old bytecode still includes the pending change | Expected JVM
`redefineClasses` behavior, not a bug; restart to clear it |
| Log: "batch redefinition rejected (...) -- retrying each class
individually", but only some classes in the save show "Hot-reload complete" |
One class in a debounced batch has a change the JVM can't apply; the others
were only rejected as part of the same all-or-nothing batch call | Expected
fallback behavior, not a bug — every class the JVM *can* apply still succeeds
individually; only the named, rejected class needs a restart |
| `<attribute>`/`<override>` schema warning (`cvc-complex-type.2.4.a`) repeats
on every reload cycle | `<attribute>` elements must come before `<override>`
per `services.xsd` | Reorder the elements in the edited `services.xml` |
-| Log: "Hot-reload: failed to reload entity model '...' -- fix the entitydef
XML and save again" | Invalid XML, or a view-entity referencing a non-existent
member entity | Fix the reported error and save again — self-heals
automatically, no restart needed even for a broken intermediate save. In the
meantime, only the specifically-broken entity/view is unavailable (confirmed
live) — every other already-working entity keeps responding normally |
+| Log: "Hot-reload: failed to reload entity model '...' -- the previous model
stays in use; fix the entitydef XML and save again" | Invalid XML, or a
view-entity referencing a non-existent member entity | Fix the reported error
and save again — no restart needed even for a broken intermediate save. In the
meantime the previous model, from before the broken save, keeps serving every
entity |
| An `entitydef` change isn't picked up at all | Either a brand-new file not
yet declared via `entity-resource` in `ofbiz-component.xml` (needs a restart —
same as a new component), or it's seed/demo data (`entity-resource
type="data"`/`"data-security"`), which this plugin doesn't watch | Restart for
a new resource declaration; seed data reload is a different, unaddressed
concern |
-| Log: "could not resolve ModelReader's private fields (READERS/entityCache)
via reflection" | This OFBiz version's `ModelReader` implementation changed in
a way `EntityModelReloader`'s reflection didn't account for |
Entity/view-entity hot-reload is disabled for the session (Java and
services.xml hot-reload are unaffected); file an issue against `devreload`
naming the OFBiz version |
| New field/entity shows up in the reloaded model but queries against it fail
(missing column/table) | The DB schema itself wasn't updated — entitydef reload
only ever changes the in-memory model | Run webtools' "Update Database", or set
`-Dofbiz.hotreload.autoUpdateSchema=true` (`-Photreload.autoUpdateSchema=true`)
to have it happen automatically on every entitydef save |
| Log: "schema auto-update failed for delegator '...'" | The datasource for
that group/helper isn't reachable, or `checkDb` itself hit a DB-specific error
| `checkDb` only ever adds — it's safe to fix the underlying DB issue and save
again |
| A `*Labels.xml` or `.properties` edit under `config/` isn't picked up |
Either the file doesn't end in `Labels.xml` (e.g. a component using the older
singular convention, if any), or it's one of the two static-snapshot exceptions
(`debug.properties` log levels, `security.properties`'s
`password.encrypt.pbkdf2.iterations`) | Rename to match the `Labels.xml`
convention, or restart for the two named exceptions — every other property and
label reloads live |
diff --git
a/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
index d489992f9..5a1c0854e 100644
--- a/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
+++ b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
@@ -138,11 +138,11 @@ import org.apache.ofbiz.base.util.cache.UtilCache;
* {@code GenericDelegator} grabs a direct object reference to a {@code
ModelReader} at
* construction time and keeps it, so clearing {@code ModelReader}'s own
lookup cache
* would only affect a brand-new delegator, never the one already serving
traffic. See
- * {@link EntityModelReloader}'s class javadoc for the full mechanism: it
reflectively
- * resets the already-running {@code ModelReader}/{@code ModelGroupReader}
singleton's
- * parsed-model field and eagerly rebuilds it in place, then clears delegator
data
- * caches. Optionally, with {@code -Dofbiz.hotreload.autoUpdateSchema=true},
it also
- * creates any missing table/column a changed entity needs, non-destructively.
+ * {@link EntityModelReloader}'s class javadoc for the full mechanism: it
eagerly reloads
+ * the already-running {@code ModelReader}/{@code ModelGroupReader} instances
in place
+ * through their {@code reload()} methods, then clears delegator data caches.
+ * Optionally, with {@code -Dofbiz.hotreload.autoUpdateSchema=true}, it also
creates
+ * any missing table/column a changed entity needs, non-destructively.
*
* <h2>How config (label/properties) changes are handled</h2>
* Every component's {@code config/} directory is also watched, scoped to
files ending
@@ -243,8 +243,8 @@ public class DevReloadContainer implements Container {
* {@code type="group"} file (an {@code entitydef/entitymodel*.xml} or
* {@code entitygroup*.xml}). Deliberately not split into two separate
sets keyed by
* resource type: a save anywhere in one of these directories triggers both
- * {@link EntityModelReloader#resetAndRebuildEntityModels()} and
- * {@link EntityModelReloader#resetAndRebuildGroupModels()} (see
+ * {@link EntityModelReloader#reloadEntityModels()} and
+ * {@link EntityModelReloader#reloadGroupModels()} (see
* {@code applyEntitydefReload}), same as this container already accepts
some
* harmless redundant work elsewhere (e.g. a WatchService event storm
re-triggering
* unrelated classes) in exchange for simpler bookkeeping -- both reload
calls are
@@ -893,11 +893,11 @@ public class DevReloadContainer implements Container {
* Reloads entity/view-entity and entity-group definitions after an
* {@code entitydef/*.xml} save. Unlike {@link #applyServiceXmlReload},
this can't
* just clear a {@code UtilCache} keyed by name -- see {@link
EntityModelReloader}'s
- * class javadoc for why entity definitions need to be reset on the
already-running
- * {@code ModelReader}/{@code ModelGroupReader} singletons directly
instead.
+ * class javadoc for why entity definitions need to be reloaded on the
already-running
+ * {@code ModelReader}/{@code ModelGroupReader} instances directly instead.
*
- * <p>Both {@link EntityModelReloader#resetAndRebuildEntityModels()} and
- * {@link EntityModelReloader#resetAndRebuildGroupModels()} run on every
entitydef
+ * <p>Both {@link EntityModelReloader#reloadEntityModels()} and
+ * {@link EntityModelReloader#reloadGroupModels()} run on every entitydef
* save regardless of which specific file changed (see {@link
#entitydefDirs}'s
* javadoc for why that's an acceptable simplification). Delegator data
caches are
* only cleared if at least one of the two actually succeeded -- clearing
them after
@@ -911,8 +911,8 @@ public class DevReloadContainer implements Container {
*/
private void applyEntitydefReload(Set<Path> batch) {
Debug.logInfo("Hot-reload: entitydef changed " + batch, MODULE);
- boolean entityOk = EntityModelReloader.resetAndRebuildEntityModels();
- boolean groupOk = EntityModelReloader.resetAndRebuildGroupModels();
+ boolean entityOk = EntityModelReloader.reloadEntityModels();
+ boolean groupOk = EntityModelReloader.reloadGroupModels();
if (entityOk || groupOk) {
EntityModelReloader.clearAllDelegatorCaches();
}
diff --git
a/devreload/src/main/java/org/apache/ofbiz/devreload/EntityModelReloader.java
b/devreload/src/main/java/org/apache/ofbiz/devreload/EntityModelReloader.java
index e0ea8f3bf..031e5cb4b 100644
---
a/devreload/src/main/java/org/apache/ofbiz/devreload/EntityModelReloader.java
+++
b/devreload/src/main/java/org/apache/ofbiz/devreload/EntityModelReloader.java
@@ -25,14 +25,15 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.function.BiConsumer;
+import java.util.function.Function;
import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.cache.UtilCache;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.DelegatorFactory;
import org.apache.ofbiz.entity.config.model.DelegatorElement;
@@ -44,39 +45,35 @@ import org.apache.ofbiz.entity.model.ModelGroupReader;
import org.apache.ofbiz.entity.model.ModelReader;
/**
- * Forces already-running {@link ModelReader}/{@link ModelGroupReader}
singletons to
- * forget their parsed entity model/group mapping and re-read it from disk, so
edits to
- * an {@code entitydef/*.xml} file take effect in the already-running JVM
without a
- * restart.
+ * Makes the already-running {@link ModelReader}/{@link ModelGroupReader}
instances re-read
+ * their entity model/group mapping from disk, so edits to an {@code
entitydef/*.xml} file
+ * take effect in the already-running JVM without a restart.
*
- * <h2>Why this needs reflection</h2>
+ * <h2>Why the readers are reloaded in place</h2>
* Unlike {@code services.xml} reload (which just clears a {@code UtilCache}
that the
* service engine looks up <em>by name on every call</em>), entity reload
can't work
* that way: every {@code GenericDelegator} grabs a direct object reference to
a
* {@link ModelReader} at construction time and keeps it
- * (see {@code GenericDelegator.modelReader}), and {@code ModelReader} itself
has no
- * public API to make it forget its parsed {@code entityCache} once built —
it's a
- * {@code private volatile} field populated once via double-checked locking
and never
- * reset. Clearing the {@code ModelReader.READERS} cache (the {@code
services.xml}
- * trick) would therefore only affect a brand-new delegator; every delegator
already
- * serving traffic would keep using its already-fetched, stale {@code
ModelReader}
- * forever. {@link ModelGroupReader} (entitygroup.xml → datasource-group
mapping) has
- * the exact same shape: a static {@code READERS} cache plus a private,
build-once
- * {@code groupCache} field.
+ * (see {@code GenericDelegator.modelReader}), so dropping the reader from its
lookup
+ * cache would only affect a brand-new delegator; every delegator already
serving
+ * traffic would keep using its already-fetched, stale {@code ModelReader}
forever.
+ * {@link ModelGroupReader} (entitygroup.xml → datasource-group mapping) has
the exact
+ * same shape.
*
- * <p>The only way to make the model already in use forget itself, without
changing
- * {@code framework/entity}, is to reflectively null out that private field on
the
- * singleton(s) already sitting in each class's {@code READERS} cache — every
delegator
- * sharing a reader name (almost always {@code "main"}) picks up the change
for free,
- * since they share the same object. The underlying XML itself needs no
separate cache
- * invalidation: {@code MainResourceHandler.getDocument()} re-reads its
- * {@code InputStream} fresh on every call.
+ * <p>Instead, {@link ModelReader#reload()} and {@link
ModelGroupReader#reload()} are
+ * called on the reader of every configured delegator -- every delegator
sharing a
+ * reader name (almost always {@code "main"}) picks up the change for free,
since they
+ * share the same object. Both readers build the new model on the side and
swap it in
+ * only once it is complete, so requests keep being served from the previous
model
+ * while the reload runs, and a broken save leaves that previous model in
place. The
+ * underlying XML itself needs no separate cache invalidation:
+ * {@code MainResourceHandler.getDocument()} re-reads its {@code InputStream}
fresh on
+ * every call.
*
- * <p>All reflective {@link Field} handles are resolved once, the first time
they're
- * needed, and cached: a resolution failure (e.g. a future OFBiz version
renaming one
- * of these private fields) is logged once with the exact class/field name and
- * disables that half (entity or group) of the reload for the rest of the
session,
- * rather than failing on every save or crashing the container.
+ * <p>Enumerating already-created delegators (to clear their data caches)
still needs
+ * reflection into {@link DelegatorFactory}; that {@link Field} handle is
resolved once,
+ * the first time it's needed, and a resolution failure is logged once and
disables
+ * only that part for the rest of the session.
*/
final class EntityModelReloader {
@@ -84,95 +81,38 @@ final class EntityModelReloader {
private static volatile boolean resolutionAttempted;
- private static volatile boolean entityReloadAvailable;
- private static Field modelReaderReadersField;
- private static Field modelReaderEntityCacheField;
-
- private static volatile boolean groupReloadAvailable;
- private static Field modelGroupReaderReadersField;
- private static Field modelGroupReaderGroupCacheField;
-
private static volatile boolean delegatorCacheClearAvailable;
private static Field delegatorFactoryDelegatorsField;
private EntityModelReloader() { }
/**
- * Reflectively resets every currently-cached {@link ModelReader}'s parsed
entity
- * model and eagerly rebuilds it in place (rather than leaving the rebuild
for the
- * next unrelated request to trigger lazily), so a broken {@code
entitydef} save is
- * caught and logged right here instead of surfacing as a random failure
later.
- *
- * <p>{@code ModelReader.getEntityCache()} populates its map field
directly as it
- * parses, rather than building into a local variable and swapping it in
at the
- * end. That means a parse failure partway through (bad XML, a view-entity
- * referencing a non-existent member entity, etc.) would normally leave
the field
- * non-null but incomplete — and since the reader's own
double-checked-locking guard
- * only rebuilds when the field is {@code null}, that broken, partial
model would
- * otherwise be served forever, even after the mistake is fixed. Because
this class
- * controls the field directly, a failed rebuild re-nulls it before
returning, so
- * the very next save (or even the next unrelated access, at reparse cost)
retries
- * the parse instead of staying poisoned until a restart.
+ * Reloads the {@link ModelReader} of every configured delegator, eagerly
(rather
+ * than leaving it for the next unrelated request), so a broken {@code
entitydef}
+ * save is caught and logged right here instead of surfacing as a random
failure
+ * later. A failed reload keeps the previous model, and the next save
retries.
*
- * @return {@code true} if every cached {@link ModelReader} rebuilt
cleanly.
+ * @return {@code true} if every {@link ModelReader} reloaded cleanly.
*/
- static boolean resetAndRebuildEntityModels() {
- ensureFieldsResolved();
- if (!entityReloadAvailable) {
- return false;
- }
-
- UtilCache<String, ModelReader> readers;
- try {
- readers = castCache(modelReaderReadersField.get(null));
- } catch (ReflectiveOperationException | ClassCastException e) {
- Debug.logError(e, "Hot-reload: could not read ModelReader.READERS;
entity model reload is disabled "
- + "for this session.", MODULE);
- entityReloadAvailable = false;
- return false;
- }
-
+ static boolean reloadEntityModels() {
boolean allSucceeded = true;
- for (String readerName : readers.getCacheLineKeys()) {
- ModelReader reader = readers.get(readerName);
- if (reader != null) {
- allSucceeded &= rebuildOneEntityReader(readerName, reader);
- }
+ for (Map.Entry<String, String> entry :
readerDelegators(DelegatorElement::getEntityModelReader).entrySet()) {
+ allSucceeded &= reloadOneEntityReader(entry.getKey(),
entry.getValue());
}
return allSucceeded;
}
/**
- * Reflectively resets every currently-cached {@link ModelGroupReader}'s
parsed
- * entity-group mapping and eagerly rebuilds it in place. Same rationale
and
- * self-healing behavior as {@link #resetAndRebuildEntityModels()}; kept
as a
- * separate entry point (and a separate availability flag) so a reflection
failure
- * against one class never disables the other.
+ * Reloads the {@link ModelGroupReader} of every configured delegator.
Same rationale
+ * as {@link #reloadEntityModels()}; kept as a separate entry point so a
failure in
+ * one never skips the other.
*
- * @return {@code true} if every cached {@link ModelGroupReader} rebuilt
cleanly.
+ * @return {@code true} if every {@link ModelGroupReader} reloaded cleanly.
*/
- static boolean resetAndRebuildGroupModels() {
- ensureFieldsResolved();
- if (!groupReloadAvailable) {
- return false;
- }
-
- UtilCache<String, ModelGroupReader> readers;
- try {
- readers = castCache(modelGroupReaderReadersField.get(null));
- } catch (ReflectiveOperationException | ClassCastException e) {
- Debug.logError(e, "Hot-reload: could not read
ModelGroupReader.READERS; entitygroup.xml reload is "
- + "disabled for this session.", MODULE);
- groupReloadAvailable = false;
- return false;
- }
-
+ static boolean reloadGroupModels() {
boolean allSucceeded = true;
- for (String readerName : readers.getCacheLineKeys()) {
- ModelGroupReader reader = readers.get(readerName);
- if (reader != null) {
- allSucceeded &= rebuildOneGroupReader(readerName, reader);
- }
+ for (Map.Entry<String, String> entry :
readerDelegators(DelegatorElement::getEntityGroupReader).entrySet()) {
+ allSucceeded &= reloadOneGroupReader(entry.getKey(),
entry.getValue());
}
return allSucceeded;
}
@@ -235,22 +175,17 @@ final class EntityModelReloader {
* them, so schema sync only ever touches what changed in this save -- not
the whole
* data model, which is what keeps it fast enough to run on every save
instead of
* only at startup. {@code ModelEntity.getLocation()} is set fresh on
every entity
- * object every time {@code ModelReader.getEntityCache()} runs (see {@code
+ * object every time {@code ModelReader} loads its entity cache (see {@code
* buildEntity()} in {@code framework/entity}), so comparing it against
the changed
* paths is reliable even across repeated reloads within the same session
-- unlike
- * {@code ModelReader.getResourceHandlerEntities()}, whose backing
collections are
- * never cleared between reloads and would otherwise need extra
bookkeeping here to
- * stay accurate.
+ * {@code ModelReader.getResourceHandlerEntities()}, which is not part of
the model a
+ * reload swaps in atomically.
*/
private static Set<String> resolveChangedEntityNames(Set<Path>
changedFiles) {
- ensureFieldsResolved();
- if (!entityReloadAvailable) {
- return Set.of();
- }
Set<String> entityNames = new HashSet<>();
try {
- UtilCache<String, ModelReader> readers =
castCache(modelReaderReadersField.get(null));
- for (ModelReader reader : readers.values()) {
+ for (String delegatorName :
readerDelegators(DelegatorElement::getEntityModelReader).values()) {
+ ModelReader reader = ModelReader.getModelReader(delegatorName);
for (Map.Entry<String, ModelEntity> entry :
reader.getEntityCache().entrySet()) {
if (locationMatches(entry.getValue().getLocation(),
changedFiles)) {
entityNames.add(entry.getKey());
@@ -409,87 +344,55 @@ final class EntityModelReloader {
}
}
- /** Nulls {@code reader}'s {@code entityCache} field, then eagerly
rebuilds it, self-healing on failure. */
- private static boolean rebuildOneEntityReader(String readerName,
ModelReader reader) {
+ /** Reloads the entity model reader named {@code readerName}, found
through {@code delegatorName}. */
+ private static boolean reloadOneEntityReader(String readerName, String
delegatorName) {
long start = System.currentTimeMillis();
try {
- modelReaderEntityCacheField.set(reader, null);
+ ModelReader reader = ModelReader.getModelReader(delegatorName);
+ reader.reload();
int entityCount = reader.getEntityCache().size();
Debug.logInfo("Hot-reload: entity model '" + readerName + "'
reloaded (" + entityCount
+ " entities/view-entities) in " +
(System.currentTimeMillis() - start) + "ms", MODULE);
return true;
} catch (Throwable t) {
- // Re-null rather than leaving a partially-populated map in place
-- see this
- // class's javadoc for why a half-built entityCache would
otherwise be served
- // forever instead of retried on the next save.
- reNullQuietly(modelReaderEntityCacheField, reader, "entityCache",
readerName);
Debug.logError(t, "Hot-reload: failed to reload entity model '" +
readerName
- + "' -- fix the entitydef XML and save again.", MODULE);
+ + "' -- the previous model stays in use; fix the entitydef
XML and save again.", MODULE);
return false;
}
}
- /** Nulls {@code reader}'s {@code groupCache} field, then eagerly rebuilds
it, self-healing on failure. */
- private static boolean rebuildOneGroupReader(String readerName,
ModelGroupReader reader) {
- String delegatorName = resolveDelegatorNameForGroupReader(readerName);
- if (delegatorName == null) {
- Debug.logWarning("Hot-reload: could not find a delegator whose
entity-group-reader is '" + readerName
- + "' in entityengine.xml -- skipping entitygroup reload
for this reader.", MODULE);
- return false;
- }
+ /** Reloads the entity group reader named {@code readerName}, found
through {@code delegatorName}. */
+ private static boolean reloadOneGroupReader(String readerName, String
delegatorName) {
long start = System.currentTimeMillis();
try {
- modelGroupReaderGroupCacheField.set(reader, null);
+ ModelGroupReader reader =
ModelGroupReader.getModelGroupReader(delegatorName);
+ reader.reload();
int groupCount = reader.getGroupCache(delegatorName).size();
Debug.logInfo("Hot-reload: entity group model '" + readerName + "'
reloaded (" + groupCount
+ " entity-group mappings) in " +
(System.currentTimeMillis() - start) + "ms", MODULE);
return true;
} catch (Throwable t) {
- reNullQuietly(modelGroupReaderGroupCacheField, reader,
"groupCache", readerName);
Debug.logError(t, "Hot-reload: failed to reload entity group model
'" + readerName
- + "' -- fix the entitygroup XML and save again.", MODULE);
+ + "' -- the previous mapping stays in use; fix the
entitygroup XML and save again.", MODULE);
return false;
}
}
/**
- * {@code ModelGroupReader.getGroupCache(String delegatorName)} takes a
delegator
- * name purely to validate each group against {@code entityengine.xml}
while
- * rebuilding (see its javadoc in {@code framework/entity}) -- it's not
stored on
- * the reader itself, so this class has no instance-local way to recover
one.
- * Resolved instead straight from parsed config: the first configured
delegator
- * whose {@code entity-group-reader} matches this reader's model name. Pure
- * {@link EntityConfig} lookups, no reflection needed for this part.
+ * Maps each reader name {@code readerOf} returns for the configured
delegators to the
+ * first delegator that uses it, so a reader shared by several delegators
is reloaded
+ * once.
*/
- private static String resolveDelegatorNameForGroupReader(String
groupReaderModelName) {
+ private static Map<String, String>
readerDelegators(Function<DelegatorElement, String> readerOf) {
+ Map<String, String> readerDelegators = new LinkedHashMap<>();
try {
- List<DelegatorElement> delegators =
EntityConfig.getInstance().getDelegatorList();
- for (DelegatorElement delegator : delegators) {
- if
(groupReaderModelName.equals(delegator.getEntityGroupReader())) {
- return delegator.getName();
- }
+ for (DelegatorElement delegator :
EntityConfig.getInstance().getDelegatorList()) {
+ readerDelegators.putIfAbsent(readerOf.apply(delegator),
delegator.getName());
}
} catch (Exception e) {
- Debug.logError(e, "Hot-reload: could not read delegator config
while resolving a delegator name for "
- + "entity-group-reader '" + groupReaderModelName + "'",
MODULE);
- }
- return null;
- }
-
- /** Best-effort re-null of {@code field} on {@code target}, logging if
even that fails. */
- private static void reNullQuietly(Field field, Object target, String
fieldLabel, String readerName) {
- try {
- field.set(target, null);
- } catch (ReflectiveOperationException inner) {
- Debug.logError(inner, "Hot-reload: could not re-null " +
fieldLabel + " for '" + readerName
- + "' after a failed rebuild -- it may now be serving a
broken, partial model until OFBiz is "
- + "restarted.", MODULE);
+ Debug.logError(e, "Hot-reload: could not read the delegator
config", MODULE);
}
- }
-
- @SuppressWarnings("unchecked")
- private static <T> UtilCache<String, T> castCache(Object value) {
- return (UtilCache<String, T>) value;
+ return readerDelegators;
}
@SuppressWarnings("unchecked")
@@ -497,7 +400,7 @@ final class EntityModelReloader {
return (Map<String, Future<Delegator>>) value;
}
- /** Resolves and caches every {@link Field} handle this class needs,
exactly once. */
+ /** Resolves and caches the {@link Field} handle this class needs, exactly
once. */
private static void ensureFieldsResolved() {
if (resolutionAttempted) {
return;
@@ -508,34 +411,6 @@ final class EntityModelReloader {
}
resolutionAttempted = true;
- try {
- modelReaderReadersField =
ModelReader.class.getDeclaredField("READERS");
- modelReaderReadersField.setAccessible(true);
- modelReaderEntityCacheField =
ModelReader.class.getDeclaredField("entityCache");
- modelReaderEntityCacheField.setAccessible(true);
- entityReloadAvailable = true;
- } catch (ReflectiveOperationException | SecurityException e) {
- Debug.logError(e, "Hot-reload: could not resolve ModelReader's
private fields (READERS/entityCache)"
- + " via reflection -- this OFBiz version's ModelReader
implementation may have "
- + "changed. Entity/view-entity hot-reload is disabled
for this session; Java and "
- + "services.xml hot-reload are unaffected.", MODULE);
- entityReloadAvailable = false;
- }
-
- try {
- modelGroupReaderReadersField =
ModelGroupReader.class.getDeclaredField("READERS");
- modelGroupReaderReadersField.setAccessible(true);
- modelGroupReaderGroupCacheField =
ModelGroupReader.class.getDeclaredField("groupCache");
- modelGroupReaderGroupCacheField.setAccessible(true);
- groupReloadAvailable = true;
- } catch (ReflectiveOperationException | SecurityException e) {
- Debug.logError(e, "Hot-reload: could not resolve
ModelGroupReader's private fields (READERS/"
- + "groupCache) via reflection -- this OFBiz version's
ModelGroupReader "
- + "implementation may have changed. entitygroup.xml
hot-reload is disabled for this "
- + "session; entity/view-entity hot-reload is
unaffected.", MODULE);
- groupReloadAvailable = false;
- }
-
try {
delegatorFactoryDelegatorsField =
DelegatorFactory.class.getDeclaredField("DELEGATORS");
delegatorFactoryDelegatorsField.setAccessible(true);