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
The OpenmrsServiceEventAdvice from #6084 emits *ServiceEvents only for public service methods whose name matches save*/create*/void*/unvoid*/retire*/unretire*/purge* and that are invoked through the Spring proxy. A class of service operations mutates indexed clinical data while defeating both conditions, so they emit no event and event-driven consumers (read stores, caches, analytics, FHIR projections) silently go stale. This issue collects the confirmed instances and proposes a systemic fix; the individual high-impact cases are filed separately (#6194, #6195, #6196, #6197) and are instances of the same root pattern.
Environment
OpenMRS Platform 2.9.0-SNAPSHOT (bytecode verified; behaviour cross-checked on referenceapplication-standalone 3.7.0-SNAPSHOT).
Found while building a CQRS read store on the TRUNK-6429: Create application events for service method calls and en… #6084 events. The audit below covers every core service that mutates a type the read store indexes (Obs, Condition, Diagnosis, Encounter, Visit, Patient, Allergy, Orders, PatientProgram, MedicationDispense).
Non-pattern method name — merge*, discontinue*, transfer*, remove*, bulk stop*, set* are never matched by the advice pointcut.
Self-invocation / direct-DAO / private *Internal — even a pattern-named write bypasses the proxy when a service mutates a record via this.x(...), dao.x(...), or a private xInternal(...) rather than the proxied public service method.
Both defeat the advice because it depends on AspectJ interception of an externally-called, proxied public method.
Contrast (works correctly):VisitService.endVisit sets stopDatetime then calls Context.getVisitService().saveVisit(...)through the proxy, so it does fire a SaveServiceEvent. This is the pattern the others should follow.
OrderService.updateOrderFulfillerStatus also bypasses the advice but mutates a field most consumers don't track, so it is lower priority.
Suggested systemic fix
Rather than patching each method, address the structural cause so future mutators don't silently regress:
Option A — route internal mutations through the proxied service. Have services call Context.getXService().saveX(...) / voidX(...) (as endVisit already does) instead of this.xInternal(...) / dao.x(...), and add the missing pattern-named methods (or have the advice recognise merge*/discontinue*/transfer*). Lowest-risk, but easy to regress again.
Option B — emit below the proxy. Publish entity events from a layer that can't be bypassed by call-path — e.g. a Hibernate Interceptor/PostInsert/PostUpdate listener, or the CDC pipeline (TRUNK-6516: Provide CDC mechanism with Debezium #6151). This makes coverage immune to self-invocation and method naming, at the cost of losing the typed service-operation semantics the current advice provides.
A hybrid (advice for typed service events + a DAO/CDC backstop for completeness) may be the most robust.
Scope boundary
Even with all of the above fixed, direct-database writes that bypass the service layer entirely — Liquibase changesets that rewrite indexed columns, raw SQL, bulk admin scripts — can never emit a *ServiceEvent. Those are catchable only by the CDC path (#6151 + the debezium producer). #6084 events should be understood as covering service-layer writes only.
Summary
The
OpenmrsServiceEventAdvicefrom #6084 emits*ServiceEvents only for public service methods whose name matchessave*/create*/void*/unvoid*/retire*/unretire*/purge*and that are invoked through the Spring proxy. A class of service operations mutates indexed clinical data while defeating both conditions, so they emit no event and event-driven consumers (read stores, caches, analytics, FHIR projections) silently go stale. This issue collects the confirmed instances and proposes a systemic fix; the individual high-impact cases are filed separately (#6194, #6195, #6196, #6197) and are instances of the same root pattern.Environment
The two ways a mutation escapes #6084
merge*,discontinue*,transfer*,remove*, bulkstop*,set*are never matched by the advice pointcut.*Internal— even a pattern-named write bypasses the proxy when a service mutates a record viathis.x(...),dao.x(...), or a privatexInternal(...)rather than the proxied public service method.Both defeat the advice because it depends on AspectJ interception of an externally-called, proxied public method.
Confirmed instances (bytecode-verified, 2.9.0-SNAPSHOT)
EncounterService.transferEncountervoidEncounter(...)andsaveEncounter(...)are self-invoked (invokevirtual this, notContext.getEncounterService())patient— the encounter stays under the wrong patient and the transfer is invisibleVisitService.stopVisits(bulk close; the scheduled "close stale visits" task)VisitDAO.saveVisit(...)directlystopDatetime/ active flag — closed visits still appear activePatientService.removeAllergyvoidAllergy(...)PatientService.setAllergies(bulk replace)PatientDAO.saveAllergies(...)+ self-invokedvoidAllergy(...)PatientService.mergePatientsOrderService.discontinueOrder/ order stopstopOrder(...)→saveOrderInternal(...)dateStopped/ active — see #6197Contrast (works correctly):
VisitService.endVisitsetsstopDatetimethen callsContext.getVisitService().saveVisit(...)through the proxy, so it does fire aSaveServiceEvent. This is the pattern the others should follow.OrderService.updateOrderFulfillerStatusalso bypasses the advice but mutates a field most consumers don't track, so it is lower priority.Suggested systemic fix
Rather than patching each method, address the structural cause so future mutators don't silently regress:
Context.getXService().saveX(...)/voidX(...)(asendVisitalready does) instead ofthis.xInternal(...)/dao.x(...), and add the missing pattern-named methods (or have the advice recognisemerge*/discontinue*/transfer*). Lowest-risk, but easy to regress again.Interceptor/PostInsert/PostUpdatelistener, or the CDC pipeline (TRUNK-6516: Provide CDC mechanism with Debezium #6151). This makes coverage immune to self-invocation and method naming, at the cost of losing the typed service-operation semantics the current advice provides.A hybrid (advice for typed service events + a DAO/CDC backstop for completeness) may be the most robust.
Scope boundary
Even with all of the above fixed, direct-database writes that bypass the service layer entirely — Liquibase changesets that rewrite indexed columns, raw SQL, bulk admin scripts — can never emit a
*ServiceEvent. Those are catchable only by the CDC path (#6151 + the debezium producer). #6084 events should be understood as covering service-layer writes only.Related
mergePatientsreassignment + no merge event