You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Went through the events/CDC work (core 2.9.x and master, plus artemis/camel/debezium). I confirmed each item below by running it on a 2.9.x install against MySQL, or against the real classes, jars, and bytecode; the file:line references are from the code. Each item notes how I confirmed it.
Confirmed
Outbox events are never delivered when the @OutboxEventListener lives in a module, which is the normal case. On a running server I registered a module @OutboxEventListener, fired a matching event, and the outbox_event row was written but stayed PENDING with the listener never called; bc2739ac-5bf3-4ecd-8e11-d383d53695bb was absent from jobrunr_recurring_jobs. In the same run OutboxEventRegistry.hasOutboxListeners() returned true (so the registry does see the module listener), and when I force-scheduled the poller myself the very same row was delivered and marked COMPLETED. So the poller logic is fine; OutboxTaskSchedulerInitializer just makes the schedule-or-not decision once at core startup, before modules register listeners, and never revisits it. Same code on master.
The outbox poller can't load module-defined event types. Same run: a module-defined OutboxableEvent was persisted to the outbox, then the poller's Class.forName(item.getEventType()) (OutboxPollingTaskHandler.java:101; line 107 on master) threw java.lang.ClassNotFoundException, the row's error_count kept climbing, and the listener was never called. Use OpenmrsClassLoader/Context.loadClass. (This is what PR TRUNK-6429: core Events/CDC sign-off fixes (#6289) #6296 changes.)
CDCEvent on 2.9.x cannot be serialized by the outbox/broker mapper. I serialized the real org.openmrs.event.CDCEvent with the real JacksonConfig.objectMapper() and it threw InvalidDefinitionException: Direct self-reference leading to cycle (... CDCEvent["resolvableType"] -> ResolvableType["componentType"] ...), because line 12 imports org.codehaus.jackson.annotate.JsonIgnore and FasterXML ignores it. The sibling SaveServiceEvent (same getResolvableType() getter, FasterXML @JsonIgnore) serialized fine in the same test. master already uses the FasterXML import.
outbox_event.payload is TEXT (64 KB) though the field is @Lob, and ordinary saves overflow it. The column is text(65535), sql_mode is STRICT_TRANS_TABLES. This is not just a large-payload edge case: a routine savePerson (one Person, a couple of names) rolled back on me with MysqlDataTruncation: Data too long for column 'payload', because the serialized SaveServiceEvent<Person> measured about 101 KB. The size comes from JacksonConfig enabling Hibernate5Module.FORCE_LAZY_LOADING, so serializing the event eagerly pulls the whole reachable graph (person -> creator -> roles -> privileges -> ...). Since that insert runs synchronously inside the business transaction (the same publish path that failed a save in item 7), the clinical write rolls back. So with an outbox listener registered, common saves fail outright. Make payload (and completed_listeners) MEDIUMTEXT/LONGTEXT, and reconsider FORCE_LAZY_LOADING (it also means extra lazy-load queries per event). Same on master.
An Artemis @BrokerEventListener typed BrokerIncomingEvent<?> dead-letters every message. Publishing a String threw JsonParseException, a JSON object threw InvalidTypeIdException: missing '@class', both were redelivered to the DLQ, and the publisher saw success. A concrete BrokerIncomingEvent<Foo> worked (verified with a ProbeDto). Handle the Object/<?> case or reject it at registration.
Outgoing broker sends aren't transactional. I published a BrokerOutgoingEvent inside a transaction that then rolled back, and the consumer still received it, because handleEvent (ArtemisEventListener.java:172) is a plain @EventListener that sends synchronously. So a send can fire for a change that later rolls back, or be lost if the process dies before the send, and there's no retry. Decide whether this should be @OutboxEventListener or @TransactionalEventListener(AFTER_COMMIT).
A synchronous @EventListener that throws fails the service call that produced the event. A listener that threw (I called getId() on a GlobalProperty, which is unsupported) propagated out of the publish and failed the saveGlobalProperty that triggered it. The advice publishes before proceed() (OpenmrsServiceEventAdvice.java:109) with no isolation around listeners. Isolate listener exceptions or document the constraint.
Camel never gets the Elasticsearch client. CamelConfig.java:82 does .backend().unwrap(RestClient.class); against the Hibernate Search 6.2.4 bytecode, unwrap returns only for ElasticsearchBackend and otherwise throws, and the catch returns null. So on an Elasticsearch backend the route indexes nothing while looking healthy. Should be .unwrap(ElasticsearchBackend.class).client(RestClient.class) with a warning when the client is unavailable.
Camel hawtio hardening. The built omod bundles geronimo-jms_2.0_spec-1.0-alpha-2.jar (its own copy of javax.jms); CamelLoginModule.java:56 compares the console password with String.equals (not constant-time); and the console WAR is never re-extracted on upgrade (CamelConfig.java:152, if (!consoleWar.exists())).
Artemis wires broker listeners only once, so a consumer module started at runtime never receives. setupListeners is guarded by if (initialized) return;, so it builds JMS containers only from the listeners present at the first context refresh. I confirmed this with two consumers using the same concrete-typed @BrokerEventListener: one in the startup batch received its message, and one in a module I started at runtime with ModuleFactory.startModule received nothing (the message reached the broker with no consumer container, and its listener was never invoked). OpenMRS starts modules at runtime, so this is a real path.
Camel's jms component is left unwired by the camel-jms classloader split, so jms: routes can't work even when a broker is present. In the running server with artemis loaded, the jms component bean resolved to Spring's NullBean (that is, CamelConfig.jms() returned null) while two broker ConnectionFactory beans were visible through core's javax.jms interface. Camel's @Autowired ConnectionFactory binds the javax.jms.ConnectionFactory from the geronimo-jms jar bundled in the omod (item 9), which is a different class from the core javax.jms-api the broker factories implement, so nothing matched and the component came back null. Excluding the geronimo spec so the module shares core's javax.jms-api fixes it. (This also means the earlier "two ConnectionFactory beans cause NoUniqueBeanException" theory is wrong: Camel sees zero, not two, which is why boot doesn't crash.)
Debezium floods consumers with framework-table churn. The engine captures the whole database and table.exclude.list excludes only debezium's own three tables, so it emits CDCEvents for non-domain tables like jobrunr_jobs, jobrunr_backgroundjobservers, jobrunr_recurring_jobs, liquibasechangeloglock, and the outbox_event table itself. In one short run the CDCEvents broke down as 210 jobrunr_jobs, 37 outbox_event, 23 other jobrunr_*, 2 liquibasechangeloglock, and only 12 for the real domain change (global_property). So the large majority of the stream is framework churn with entityType=null that every @EventListener(CDCEvent) consumer has to handle, plus the outbox is re-captured as CDC. Exclude the infrastructure tables or document that consumers must filter.
A single poison event blocks the whole outbox until it exhausts the retry limit. The poller processes strictly in id order and, on any listener failure, resets the row to PENDING (until errorCount >= outboxevent.retry.limit, default 16) and throws, aborting the cycle. So one event that keeps failing (for example item 2's ClassNotFoundException, or a transient broker outage) blocks every later event for up to 16 retries before it goes FAILED and the queue drains. I watched this happen: the failing row climbed to error_count=16 and flipped to FAILED, and the six SaveServiceEvent rows queued behind it stayed PENDING at error_count=0 (never attempted) the whole time, then all drained to COMPLETED the moment the poison row went FAILED. Transient failures count toward the same limit, so a temporary downstream outage can also push an otherwise-good event to FAILED (then needing a manual retryFailedOutboxEvent).
@rkorytkowski flagging for you. The three per-module review issues (artemis#4, camel#4, debezium#1) are closed and folded in here; debezium is otherwise clean.
Went through the events/CDC work (core 2.9.x and master, plus artemis/camel/debezium). I confirmed each item below by running it on a 2.9.x install against MySQL, or against the real classes, jars, and bytecode; the file:line references are from the code. Each item notes how I confirmed it.
Confirmed
Outbox events are never delivered when the
@OutboxEventListenerlives in a module, which is the normal case. On a running server I registered a module@OutboxEventListener, fired a matching event, and theoutbox_eventrow was written but stayed PENDING with the listener never called;bc2739ac-5bf3-4ecd-8e11-d383d53695bbwas absent fromjobrunr_recurring_jobs. In the same runOutboxEventRegistry.hasOutboxListeners()returnedtrue(so the registry does see the module listener), and when I force-scheduled the poller myself the very same row was delivered and marked COMPLETED. So the poller logic is fine;OutboxTaskSchedulerInitializerjust makes the schedule-or-not decision once at core startup, before modules register listeners, and never revisits it. Same code on master.The outbox poller can't load module-defined event types. Same run: a module-defined
OutboxableEventwas persisted to the outbox, then the poller'sClass.forName(item.getEventType())(OutboxPollingTaskHandler.java:101; line 107 on master) threwjava.lang.ClassNotFoundException, the row'serror_countkept climbing, and the listener was never called. UseOpenmrsClassLoader/Context.loadClass. (This is what PR TRUNK-6429: core Events/CDC sign-off fixes (#6289) #6296 changes.)CDCEventon 2.9.x cannot be serialized by the outbox/broker mapper. I serialized the realorg.openmrs.event.CDCEventwith the realJacksonConfig.objectMapper()and it threwInvalidDefinitionException: Direct self-reference leading to cycle (... CDCEvent["resolvableType"] -> ResolvableType["componentType"] ...), because line 12 importsorg.codehaus.jackson.annotate.JsonIgnoreand FasterXML ignores it. The siblingSaveServiceEvent(samegetResolvableType()getter, FasterXML@JsonIgnore) serialized fine in the same test. master already uses the FasterXML import.outbox_event.payloadisTEXT(64 KB) though the field is@Lob, and ordinary saves overflow it. The column istext(65535),sql_modeisSTRICT_TRANS_TABLES. This is not just a large-payload edge case: a routinesavePerson(one Person, a couple of names) rolled back on me withMysqlDataTruncation: Data too long for column 'payload', because the serializedSaveServiceEvent<Person>measured about 101 KB. The size comes fromJacksonConfigenablingHibernate5Module.FORCE_LAZY_LOADING, so serializing the event eagerly pulls the whole reachable graph (person -> creator -> roles -> privileges -> ...). Since that insert runs synchronously inside the business transaction (the same publish path that failed a save in item 7), the clinical write rolls back. So with an outbox listener registered, common saves fail outright. Makepayload(andcompleted_listeners)MEDIUMTEXT/LONGTEXT, and reconsiderFORCE_LAZY_LOADING(it also means extra lazy-load queries per event). Same on master.An Artemis
@BrokerEventListenertypedBrokerIncomingEvent<?>dead-letters every message. Publishing a String threwJsonParseException, a JSON object threwInvalidTypeIdException: missing '@class', both were redelivered to the DLQ, and the publisher saw success. A concreteBrokerIncomingEvent<Foo>worked (verified with aProbeDto). Handle theObject/<?>case or reject it at registration.Outgoing broker sends aren't transactional. I published a
BrokerOutgoingEventinside a transaction that then rolled back, and the consumer still received it, becausehandleEvent(ArtemisEventListener.java:172) is a plain@EventListenerthat sends synchronously. So a send can fire for a change that later rolls back, or be lost if the process dies before the send, and there's no retry. Decide whether this should be@OutboxEventListeneror@TransactionalEventListener(AFTER_COMMIT).A synchronous
@EventListenerthat throws fails the service call that produced the event. A listener that threw (I calledgetId()on aGlobalProperty, which is unsupported) propagated out of the publish and failed thesaveGlobalPropertythat triggered it. The advice publishes beforeproceed()(OpenmrsServiceEventAdvice.java:109) with no isolation around listeners. Isolate listener exceptions or document the constraint.Camel never gets the Elasticsearch client.
CamelConfig.java:82does.backend().unwrap(RestClient.class); against the Hibernate Search 6.2.4 bytecode,unwrapreturns only forElasticsearchBackendand otherwise throws, and thecatchreturns null. So on an Elasticsearch backend the route indexes nothing while looking healthy. Should be.unwrap(ElasticsearchBackend.class).client(RestClient.class)with a warning when the client is unavailable.Camel hawtio hardening. The built omod bundles
geronimo-jms_2.0_spec-1.0-alpha-2.jar(its own copy ofjavax.jms);CamelLoginModule.java:56compares the console password withString.equals(not constant-time); and the console WAR is never re-extracted on upgrade (CamelConfig.java:152,if (!consoleWar.exists())).Artemis wires broker listeners only once, so a consumer module started at runtime never receives.
setupListenersis guarded byif (initialized) return;, so it builds JMS containers only from the listeners present at the first context refresh. I confirmed this with two consumers using the same concrete-typed@BrokerEventListener: one in the startup batch received its message, and one in a module I started at runtime withModuleFactory.startModulereceived nothing (the message reached the broker with no consumer container, and its listener was never invoked). OpenMRS starts modules at runtime, so this is a real path.Camel's
jmscomponent is left unwired by thecamel-jmsclassloader split, sojms:routes can't work even when a broker is present. In the running server with artemis loaded, thejmscomponent bean resolved to Spring'sNullBean(that is,CamelConfig.jms()returned null) while two brokerConnectionFactorybeans were visible through core'sjavax.jmsinterface. Camel's@Autowired ConnectionFactorybinds thejavax.jms.ConnectionFactoryfrom thegeronimo-jmsjar bundled in the omod (item 9), which is a different class from the corejavax.jms-apithe broker factories implement, so nothing matched and the component came back null. Excluding the geronimo spec so the module shares core'sjavax.jms-apifixes it. (This also means the earlier "two ConnectionFactory beans cause NoUniqueBeanException" theory is wrong: Camel sees zero, not two, which is why boot doesn't crash.)Debezium floods consumers with framework-table churn. The engine captures the whole database and
table.exclude.listexcludes only debezium's own three tables, so it emits CDCEvents for non-domain tables likejobrunr_jobs,jobrunr_backgroundjobservers,jobrunr_recurring_jobs,liquibasechangeloglock, and theoutbox_eventtable itself. In one short run the CDCEvents broke down as 210jobrunr_jobs, 37outbox_event, 23 otherjobrunr_*, 2liquibasechangeloglock, and only 12 for the real domain change (global_property). So the large majority of the stream is framework churn withentityType=nullthat every@EventListener(CDCEvent)consumer has to handle, plus the outbox is re-captured as CDC. Exclude the infrastructure tables or document that consumers must filter.A single poison event blocks the whole outbox until it exhausts the retry limit. The poller processes strictly in id order and, on any listener failure, resets the row to PENDING (until
errorCount >= outboxevent.retry.limit, default 16) and throws, aborting the cycle. So one event that keeps failing (for example item 2'sClassNotFoundException, or a transient broker outage) blocks every later event for up to 16 retries before it goes FAILED and the queue drains. I watched this happen: the failing row climbed toerror_count=16and flipped to FAILED, and the sixSaveServiceEventrows queued behind it stayed PENDING aterror_count=0(never attempted) the whole time, then all drained to COMPLETED the moment the poison row went FAILED. Transient failures count toward the same limit, so a temporary downstream outage can also push an otherwise-good event to FAILED (then needing a manualretryFailedOutboxEvent).@rkorytkowski flagging for you. The three per-module review issues (artemis#4, camel#4, debezium#1) are closed and folded in here; debezium is otherwise clean.