Skip to content

Commit

Permalink
Remove Interval Turnover
Browse files Browse the repository at this point in the history
  • Loading branch information
kennsippell committed Dec 18, 2024
1 parent 58a5e7d commit 1a0d7a0
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 263 deletions.
83 changes: 25 additions & 58 deletions shared-libs/rules-engine/src/provider-wireup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const refreshRulesEmissions = require('./refresh-rules-emissions');
const rulesEmitter = require('./rules-emitter');
const rulesStateStore = require('./rules-state-store');
const updateTemporalStates = require('./update-temporal-states');
const calendarInterval = require('@medic/calendar-interval');

let wireupOptions;

Expand All @@ -31,7 +30,7 @@ module.exports = {
* @param {number} settings.monthStartDate reporting interval start date
* @param {Object} userDoc User's hydrated contact document
*/
initialize: (provider, settings) => {
initialize: async (provider, settings) => {
const isEnabled = rulesEmitter.initialize(settings);
if (!isEnabled) {
return Promise.resolve();
Expand All @@ -40,26 +39,21 @@ module.exports = {
const { enableTasks=true, enableTargets=true } = settings;
wireupOptions = { enableTasks, enableTargets };

return provider
.existingRulesStateStore()
.then(existingStateDoc => {
if (!rulesEmitter.isLatestNoolsSchema()) {
throw Error('Rules Engine: Updates to the nools schema are required');
}
const existingStateDoc = await provider.existingRulesStateStore();
if (!rulesEmitter.isLatestNoolsSchema()) {
throw Error('Rules Engine: Updates to the nools schema are required');
}

const contactClosure = updatedState => provider.stateChangeCallback(
existingStateDoc,
{ rulesStateStore: updatedState }
);
const needsBuilding = rulesStateStore.load(existingStateDoc.rulesStateStore, settings, contactClosure);
return handleIntervalTurnover(provider, settings).then(() => {
if (!needsBuilding) {
return;
}

rulesStateStore.build(settings, contactClosure);
});
});
const contactClosure = updatedState => provider.stateChangeCallback(
existingStateDoc,
{ rulesStateStore: updatedState }
);
const needsBuilding = rulesStateStore.load(existingStateDoc.rulesStateStore, settings, contactClosure);
if (!needsBuilding) {
return;
}

rulesStateStore.build(settings, contactClosure);
},

/**
Expand Down Expand Up @@ -273,20 +267,18 @@ const refreshRulesEmissionForContacts = (provider, calculationTimestamp, contact
});
};

return handleIntervalTurnover(provider, { monthStartDate: rulesStateStore.getMonthStartDate() }).then(() => {
if (contactIds) {
return refreshForKnownContacts(calculationTimestamp, contactIds);
}
if (contactIds) {
return refreshForKnownContacts(calculationTimestamp, contactIds);
}

// If the contact state store does not contain all contacts, build up that list (contact doc ids + headless ids in
// reports/tasks)
if (!rulesStateStore.hasAllContacts()) {
return refreshForAllContacts(calculationTimestamp);
}
// If the contact state store does not contain all contacts, build up that list (contact doc ids + headless ids in
// reports/tasks)
if (!rulesStateStore.hasAllContacts()) {
return refreshForAllContacts(calculationTimestamp);
}

// Once the contact state store has all contacts, trust it and only refresh those marked dirty
return refreshForKnownContacts(calculationTimestamp, rulesStateStore.getContactIds());
});
// Once the contact state store has all contacts, trust it and only refresh those marked dirty
return refreshForKnownContacts(calculationTimestamp, rulesStateStore.getContactIds());
};

const storeTargetsDoc = (provider, aggregate, updatedTargets) => {
Expand All @@ -305,28 +297,3 @@ const storeTargetsDoc = (provider, aggregate, updatedTargets) => {
);
};

// This function takes the last saved state (which may be stale) and generates the targets doc for the corresponding
// reporting interval (that includes the date when the state was calculated).
// We don't recalculate the state prior to this because we support targets that count events infinitely to emit `now`,
// which means that they would all be excluded from the emission filter (being outside the past reporting interval).
// https://github.com/medic/cht-core/issues/6209
const handleIntervalTurnover = async (provider, { monthStartDate }) => {
if (!rulesStateStore.isLoaded() || !wireupOptions.enableTargets) {
return Promise.resolve();
}

const stateCalculatedAt = rulesStateStore.stateLastUpdatedAt();
if (!stateCalculatedAt) {
return Promise.resolve();
}

const currentInterval = calendarInterval.getCurrent(monthStartDate);
// 4th parameter of isBetween represents inclusivity. By default or using ( is exclusive, [ is inclusive
if (moment(stateCalculatedAt).isBetween(currentInterval.start, currentInterval.end, null, '[]')) {
return Promise.resolve();
}

const filterInterval = calendarInterval.getInterval(monthStartDate, stateCalculatedAt);
const aggregate = await rulesStateStore.getTargetAggregates(filterInterval);
return storeTargetsDoc(provider, aggregate, true);
};
77 changes: 2 additions & 75 deletions shared-libs/rules-engine/test/integration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const sinon = require('sinon');

const RulesEngine = require('../src');
const rulesEmitter = require('../src/rules-emitter');
const calendarInterval = require('@medic/calendar-interval');

const { expect } = chai;
chai.use(chaiExclude);
Expand Down Expand Up @@ -259,18 +258,10 @@ describe(`Rules Engine Integration Tests`, () => {
clock.setSystemTime(TEST_START + MS_IN_DAY * 39);
const monthLater = await rulesEngine.fetchTasksFor(['patient']);
expect(monthLater).to.have.property('length', 0);
expect(db.bulkDocs.callCount).to.eq(5);

// interval turnover
expect(db.bulkDocs.args[3][0].docs[0]).to.deep.include({
_id: `target~${TARGET_INTERVAL}~user~org.couchdb.user:username`,
type: 'target',
owner: 'user',
reporting_period: TARGET_INTERVAL,
});
expect(db.bulkDocs.callCount).to.eq(4);

const dateNext = moment(TEST_START + MS_IN_DAY * 39).format('YYYY-MM');
expect(db.bulkDocs.args[4][0].docs[0]).to.deep.include({
expect(db.bulkDocs.args[3][0].docs[0]).to.deep.include({
_id: `target~${dateNext}~user~org.couchdb.user:username`,
type: 'target',
owner: 'user',
Expand Down Expand Up @@ -692,70 +683,6 @@ describe(`Rules Engine Integration Tests`, () => {
pass: 1,
});
});

it('targets on interval turnover only recalculates targets when interval changes', async () => {
const targetsSaved = () => {
const targets = [];
db.bulkDocs.args.forEach(([docs]) => {
if (!docs) {
return;
}

if (docs && docs.docs) {
docs = docs.docs;
}
docs.forEach(doc => doc._id.startsWith('target') && targets.push(doc));
});

return targets;
};

clock.setSystemTime(TEST_START);
const patientContact2 = Object.assign({}, patientContact, { _id: 'patient2', patient_id: 'patient_id2', });
const pregnancyRegistrationReport2 = Object.assign(
{},
pregnancyRegistrationReport,
{
_id: 'pregReg2',
fields: { lmp_date_8601: TEST_START, patient_id: patientContact2.patient_id },
reported_date: TEST_START+1
},
);
await db.bulkDocs([patientContact, patientContact2, pregnancyRegistrationReport, pregnancyRegistrationReport2]);
await rulesEngine.updateEmissionsFor(['patient']);
// we're in THE_FUTURE and our state is fresh

sinon.spy(db, 'bulkDocs');
sinon.spy(db, 'query');
const targets = await fetchTargets();
expect(db.query.callCount).to.eq(expectedQueriesForAllFreshData.length);
expect(targets[['pregnancy-registrations-this-month']].value).to.deep.eq({
total: 2,
pass: 2,
});
expect(targetsSaved().length).to.equal(1);

const sameTargets = await fetchTargets();
expect(db.query.callCount).to.eq(expectedQueriesForAllFreshData.length);
expect(sameTargets).to.deep.eq(targets);
expect(targetsSaved().length).to.equal(1);

// fast forward one month
clock.tick(moment(TEST_START).add(1, 'month').diff(moment(TEST_START)) + 2);
const newTargets = await fetchTargets(calendarInterval.getCurrent());
expect(newTargets[['pregnancy-registrations-this-month']].value).to.deep.eq({
total: 0,
pass: 0,
});
const savedTargets = targetsSaved();
expect(savedTargets.length).to.equal(3);

const firstTargetInterval = calendarInterval.getInterval(1, TEST_START);
const secondTargetInterval = calendarInterval.getCurrent();
expect(savedTargets[0].reporting_period).to.equal(moment(firstTargetInterval.end).format('YYYY-MM'));
expect(savedTargets[1].reporting_period).to.equal(moment(firstTargetInterval.end).format('YYYY-MM'));
expect(savedTargets[2].reporting_period).to.equal(moment(secondTargetInterval.end).format('YYYY-MM'));
});
});
}

Expand Down
Loading

0 comments on commit 1a0d7a0

Please sign in to comment.