Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
1.6.0 (unreleased)
------------------

- #135 Use UnorderedOrdering for PatientFolder to avoid ZODB conflicts
- #133 Remove email invariant and add adapter to toggle email_report
- #134 Compatibility with core#2835 (display Patients in navbar)
- #131 Fix BehaviorRegistrationNotFound on Patient creation via JSON API
Expand Down
25 changes: 25 additions & 0 deletions src/senaite/patient/content/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,29 @@
factory=".analysisrequest.AnalysisRequestSchemaModifier"
provides="archetypes.schemaextender.interfaces.ISchemaModifier"/>

<!--
Use plone.folder's UnorderedOrdering as the IOrdering adapter for the
PatientFolder.

The default DefaultOrdering keeps a `plone.folder.ordered.order`
PersistentList annotation that grows with the number of children:
every `_setObject` calls `notifyAdded` which appends to the list.
PersistentList has no `_p_resolveConflict()`, so two concurrent
appends collide and the conflict propagates to the client through
the publisher's retry loop. On clinical-lab installations the
PatientFolder accumulates one Patient per registered sample, so
every concurrent sample-registration that creates a new Patient
contends on the same annotation. With tens of thousands of children
the cost grows roughly with the number of entries.

Patient listings never depend on the parent folder's manual
ordering — they sort catalog brains. Switching IPatientFolder to
UnorderedOrdering removes the hot-spot at no functional cost.
-->
<adapter
for=".patientfolder.IPatientFolder"
factory="plone.folder.unordered.UnorderedOrdering"
provides="plone.folder.interfaces.IOrdering"
/>

</configure>
2 changes: 1 addition & 1 deletion src/senaite/patient/profiles/default/metadata.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<metadata>
<version>1602</version>
<version>1603</version>
<dependencies>
<dependency>profile-senaite.lims:default</dependency>
</dependencies>
Expand Down
42 changes: 42 additions & 0 deletions src/senaite/patient/upgrade/v01_06_000.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from senaite.patient import logger
from senaite.patient.config import PRODUCT_NAME
from senaite.patient.setuphandlers import display_in_nav
from zope.annotation.interfaces import IAnnotations

version = "1.6.0"
profile = "profile-{0}:default".format(PRODUCT_NAME)
Expand Down Expand Up @@ -76,3 +77,44 @@ def display_patients_navbar(tool):
patients = api.get_portal().patients
display_in_nav(patients)
logger.info("Display Patients in navigation bar [DONE]")


def drop_patientfolder_ordering_annotations(tool):
"""Remove the legacy IOrdering annotations from the PatientFolder.

The PatientFolder now uses `plone.folder.unordered.UnorderedOrdering`
as its `IOrdering` adapter, so the previous default-ordering
annotations are no longer maintained:

- `plone.folder.ordered.order` — a `PersistentList` of every
child id, mutated on every `_setObject` via
`DefaultOrdering.notifyAdded`. With one Patient per registered
sample on a clinical-lab installation, this list grows to many
tens of thousands of entries; because `PersistentList` has no
`_p_resolveConflict()`, every concurrent registration that
creates a new Patient collided on it and the conflict
propagated all the way to the publisher's retry loop.

- `plone.folder.ordered.pos` — companion `OIBTree` mapping
child id -> position. No longer read by anything once the
adapter is unordered.

Removing both annotations after the adapter switch frees the
storage they occupy and removes a stale hot-mutation bucket from
the ZODB cache. The adapter override is what stops new writes
from touching them; this step is hygiene.
"""
logger.info("Dropping IOrdering annotations from PatientFolder ...")
patients = api.get_portal().patients
ann = IAnnotations(patients)
had_order = "plone.folder.ordered.order" in ann
had_pos = "plone.folder.ordered.pos" in ann
if not (had_order or had_pos):
logger.info(
"Dropping IOrdering annotations from PatientFolder [SKIP] "
"(no annotations found)")
return
ann.pop("plone.folder.ordered.order", None)
ann.pop("plone.folder.ordered.pos", None)
patients._p_changed = True
logger.info("Dropping IOrdering annotations from PatientFolder [DONE]")
13 changes: 13 additions & 0 deletions src/senaite/patient/upgrade/v01_06_000.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup">

<genericsetup:upgradeStep
title="Drop ordering annotations from PatientFolder"
description="
Remove the plone.folder.ordered.order PersistentList and
plone.folder.ordered.pos OIBTree annotations from the PatientFolder.
The folder now uses UnorderedOrdering as its IOrdering adapter, so
these annotations are no longer maintained and become a stale ZODB
conflict spot if left in place."
source="1602"
destination="1603"
handler=".v01_06_000.drop_patientfolder_ordering_annotations"
profile="senaite.patient:default"/>

<genericsetup:upgradeStep
title="Display Patients in the navbar"
description="
Expand Down
Loading