Skip to content

Commit 1f4203c

Browse files
Merge pull request #150 from OpenSPP/worktree-perf+phase8-canary-patterns
perf: add canary patterns to skip statistics during bulk operations
2 parents 26b9060 + 8c49d83 commit 1f4203c

11 files changed

Lines changed: 213 additions & 14 deletions

File tree

spp_programs/README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ Dependencies
254254
Changelog
255255
=========
256256

257+
19.0.2.0.9
258+
~~~~~~~~~~
259+
260+
- Add context flags (``skip_registrant_statistics``,
261+
``skip_program_statistics``) to suppress expensive computed field
262+
recomputation during bulk operations
263+
- Add ``refresh_beneficiary_counts()`` on program and
264+
``refresh_statistics()`` on cycle for one-shot recomputation after
265+
bulk operations
266+
- Replace ``bool(rec.program_membership_ids)`` with SQL query in
267+
``_compute_has_members``
268+
257269
19.0.2.0.8
258270
~~~~~~~~~~
259271

spp_programs/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "OpenSPP Programs",
55
"summary": "Manage programs, cycles, beneficiary enrollment, entitlements (cash and in-kind), payments, and fund tracking for social protection.",
66
"category": "OpenSPP/Core",
7-
"version": "19.0.2.0.8",
7+
"version": "19.0.2.0.9",
88
"sequence": 1,
99
"author": "OpenSPP.org",
1010
"website": "https://github.com/OpenSPP/OpenSPP2",

spp_programs/models/cycle.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,16 @@ def _compute_entitlements_count(self):
275275
entitlements_count = self.env["spp.entitlement"].search_count([("cycle_id", "=", rec.id)])
276276
rec.entitlements_count = entitlements_count
277277

278+
def refresh_statistics(self):
279+
"""Refresh all cycle statistics after bulk operations.
280+
281+
Call this after raw SQL inserts that bypass ORM dependency tracking
282+
(e.g. bulk_create_memberships with skip_duplicates=True).
283+
"""
284+
self._compute_members_count()
285+
self._compute_entitlements_count()
286+
self._compute_total_entitlements_count()
287+
278288
@api.depends("entitlement_ids", "inkind_entitlement_ids")
279289
def _compute_total_entitlements_count(self):
280290
if not self.ids:

spp_programs/models/managers/cycle_manager_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ def mark_import_as_done(self, cycle, msg):
326326
cycle.locked_reason = None
327327
cycle.message_post(body=msg)
328328

329-
# Update Statistics
330-
cycle._compute_members_count()
329+
# Refresh statistics after bulk operations
330+
cycle.refresh_statistics()
331331

332332
def mark_prepare_entitlement_as_done(self, cycle, msg):
333333
"""Complete the preparation of entitlements.

spp_programs/models/managers/eligibility_manager.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,7 @@ def _import_registrants_async(self, new_beneficiaries, state="draft"):
165165

166166
def mark_import_as_done(self):
167167
self.ensure_one()
168-
self.program_id._compute_eligible_beneficiary_count()
169-
self.program_id._compute_beneficiary_count()
168+
self.program_id.refresh_beneficiary_counts()
170169

171170
self.program_id.is_locked = False
172171
self.program_id.locked_reason = None

spp_programs/models/programs.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,23 @@ def _check_unique_program_name(self):
187187

188188
@api.depends("program_membership_ids")
189189
def _compute_has_members(self):
190+
if self.env.context.get("skip_program_statistics"):
191+
return
192+
if not self.ids:
193+
for rec in self:
194+
rec.has_members = False
195+
return
196+
self.env.cr.execute(
197+
"""
198+
SELECT program_id FROM spp_program_membership
199+
WHERE program_id IN %s
200+
GROUP BY program_id
201+
""",
202+
(tuple(self.ids),),
203+
)
204+
programs_with_members = {row[0] for row in self.env.cr.fetchall()}
190205
for rec in self:
191-
rec.has_members = bool(rec.program_membership_ids)
206+
rec.has_members = rec.id in programs_with_members
192207

193208
@api.depends("compliance_manager_ids", "compliance_manager_ids.manager_ref_id")
194209
def _compute_has_compliance_criteria(self):
@@ -273,6 +288,16 @@ def _compute_beneficiary_count(self):
273288
count = rec.count_beneficiaries(None)["value"]
274289
rec.update({"beneficiaries_count": count})
275290

291+
def refresh_beneficiary_counts(self):
292+
"""Refresh all beneficiary statistics after bulk operations.
293+
294+
Call this after raw SQL inserts that bypass ORM dependency tracking
295+
(e.g. bulk_create_memberships with skip_duplicates=True).
296+
"""
297+
self._compute_beneficiary_count()
298+
self._compute_eligible_beneficiary_count()
299+
self._compute_has_members()
300+
276301
@api.depends("cycle_ids")
277302
def _compute_cycle_count(self):
278303
for rec in self:

spp_programs/models/registrant.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def _compute_total_entitlements_count(self):
4343
@api.depends("program_membership_ids")
4444
def _compute_program_membership_count(self):
4545
"""Batch-efficient program membership count using read_group."""
46+
if self.env.context.get("skip_registrant_statistics"):
47+
return
4648
if not self:
4749
return
4850

@@ -66,6 +68,8 @@ def _compute_program_membership_count(self):
6668
@api.depends("entitlement_ids")
6769
def _compute_entitlements_count(self):
6870
"""Batch-efficient entitlements count using _read_group."""
71+
if self.env.context.get("skip_registrant_statistics"):
72+
return
6973
if not self:
7074
return
7175

@@ -89,6 +93,8 @@ def _compute_entitlements_count(self):
8993
@api.depends("cycle_ids")
9094
def _compute_cycle_count(self):
9195
"""Batch-efficient cycle membership count using _read_group."""
96+
if self.env.context.get("skip_registrant_statistics"):
97+
return
9298
if not self:
9399
return
94100

@@ -112,6 +118,8 @@ def _compute_cycle_count(self):
112118
@api.depends("inkind_entitlement_ids")
113119
def _compute_inkind_entitlements_count(self):
114120
"""Batch-efficient in-kind entitlements count using _read_group."""
121+
if self.env.context.get("skip_registrant_statistics"):
122+
return
115123
if not self:
116124
return
117125

spp_programs/readme/HISTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### 19.0.2.0.9
2+
3+
- Add context flags (`skip_registrant_statistics`, `skip_program_statistics`) to suppress expensive computed field recomputation during bulk operations
4+
- Add `refresh_beneficiary_counts()` on program and `refresh_statistics()` on cycle for one-shot recomputation after bulk operations
5+
- Replace `bool(rec.program_membership_ids)` with SQL query in `_compute_has_members`
6+
17
### 19.0.2.0.8
28

39
- Replace OFFSET pagination with NTILE-based ID-range batching in all async job dispatchers

spp_programs/static/description/index.html

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,19 @@ <h2><a class="toc-backref" href="#toc-entry-1">Changelog</a></h2>
658658
</div>
659659
</div>
660660
<div class="section" id="section-1">
661+
<h1>19.0.2.0.9</h1>
662+
<ul class="simple">
663+
<li>Add context flags (<tt class="docutils literal">skip_registrant_statistics</tt>,
664+
<tt class="docutils literal">skip_program_statistics</tt>) to suppress expensive computed field
665+
recomputation during bulk operations</li>
666+
<li>Add <tt class="docutils literal">refresh_beneficiary_counts()</tt> on program and
667+
<tt class="docutils literal">refresh_statistics()</tt> on cycle for one-shot recomputation after
668+
bulk operations</li>
669+
<li>Replace <tt class="docutils literal">bool(rec.program_membership_ids)</tt> with SQL query in
670+
<tt class="docutils literal">_compute_has_members</tt></li>
671+
</ul>
672+
</div>
673+
<div class="section" id="section-2">
661674
<h1>19.0.2.0.8</h1>
662675
<ul class="simple">
663676
<li>Replace OFFSET pagination with NTILE-based ID-range batching in all
@@ -668,7 +681,7 @@ <h1>19.0.2.0.8</h1>
668681
program and cycle</li>
669682
</ul>
670683
</div>
671-
<div class="section" id="section-2">
684+
<div class="section" id="section-3">
672685
<h1>19.0.2.0.7</h1>
673686
<ul class="simple">
674687
<li>Bulk membership creation using raw SQL INSERT ON CONFLICT DO NOTHING
@@ -677,7 +690,7 @@ <h1>19.0.2.0.7</h1>
677690
<tt class="docutils literal">_add_beneficiaries</tt> with bulk SQL path</li>
678691
</ul>
679692
</div>
680-
<div class="section" id="section-3">
693+
<div class="section" id="section-4">
681694
<h1>19.0.2.0.6</h1>
682695
<ul class="simple">
683696
<li>Remove unused entitlement_base_model.py (dead code, never imported)</li>
@@ -686,34 +699,34 @@ <h1>19.0.2.0.6</h1>
686699
payment, and fund tests (172 → 492 tests)</li>
687700
</ul>
688701
</div>
689-
<div class="section" id="section-4">
702+
<div class="section" id="section-5">
690703
<h1>19.0.2.0.5</h1>
691704
<ul class="simple">
692705
<li>Batch create entitlements and payments instead of one-by-one ORM
693706
creates</li>
694707
</ul>
695708
</div>
696-
<div class="section" id="section-5">
709+
<div class="section" id="section-6">
697710
<h1>19.0.2.0.4</h1>
698711
<ul class="simple">
699712
<li>Fetch fund balance once per approval batch instead of per entitlement</li>
700713
</ul>
701714
</div>
702-
<div class="section" id="section-6">
715+
<div class="section" id="section-7">
703716
<h1>19.0.2.0.3</h1>
704717
<ul class="simple">
705718
<li>Replace cycle computed fields (total_amount, entitlements_count,
706719
approval flags) with SQL aggregation queries</li>
707720
</ul>
708721
</div>
709-
<div class="section" id="section-7">
722+
<div class="section" id="section-8">
710723
<h1>19.0.2.0.2</h1>
711724
<ul class="simple">
712725
<li>Add composite indexes for frequent query patterns on entitlements and
713726
program memberships</li>
714727
</ul>
715728
</div>
716-
<div class="section" id="section-8">
729+
<div class="section" id="section-9">
717730
<h1>19.0.2.0.1</h1>
718731
<ul class="simple">
719732
<li>Replace Python-level uniqueness checks with SQL UNIQUE constraints for
@@ -722,7 +735,7 @@ <h1>19.0.2.0.1</h1>
722735
constraint creation</li>
723736
</ul>
724737
</div>
725-
<div class="section" id="section-9">
738+
<div class="section" id="section-10">
726739
<h1>19.0.2.0.0</h1>
727740
<ul class="simple">
728741
<li>Initial migration to OpenSPP2</li>

spp_programs/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@
3434
from . import test_cycle_auto_approve_fund_check
3535
from . import test_bulk_membership
3636
from . import test_keyset_pagination
37+
from . import test_canary_patterns

0 commit comments

Comments
 (0)