Add ability to truncate large prediction arrays in device status data

Introduce logic to truncate prediction arrays (IOB, COB, UAM, ZT) in devicestatus documents when they exceed a configured maximum size, preventing potential issues with large document handling in MongoDB. This includes updates to bootevent, devicestatus module, environment configuration, and new tests to verify truncation behavior.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: daa945f9-0872-4250-9868-a1245067293b
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: baff25d9-e915-44d9-87f0-f7583f443a30
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
bewest
2026-01-19 13:14:46 -08:00
committed by Ben West
parent ffc345715f
commit 6b68f920ac
5 changed files with 155 additions and 7 deletions
+1 -1
View File
@@ -219,7 +219,7 @@ function boot (env, language) {
ctx.activity = require('./activity')(env, ctx);
ctx.entries = require('./entries')(env, ctx);
ctx.treatments = require('./treatments')(env, ctx);
ctx.devicestatus = require('./devicestatus')(env.devicestatus_collection, ctx);
ctx.devicestatus = require('./devicestatus')(env, ctx);
ctx.profile = require('./profile')(env.profile_collection, ctx);
ctx.food = require('./food')(env, ctx);
ctx.pebble = require('./pebble')(env, ctx);
+34 -1
View File
@@ -4,7 +4,38 @@ var moment = require('moment');
var find_options = require('./query');
var async = require('async');
function storage (collection, ctx) {
function truncatePredictions (obj, maxSize) {
if (!maxSize || maxSize <= 0) return obj;
if (obj && obj.openaps && obj.openaps.suggested && obj.openaps.suggested.predBGs) {
var predBGs = obj.openaps.suggested.predBGs;
var predictionTypes = ['IOB', 'COB', 'UAM', 'ZT'];
predictionTypes.forEach(function(type) {
if (Array.isArray(predBGs[type]) && predBGs[type].length > maxSize) {
predBGs[type] = predBGs[type].slice(0, maxSize);
}
});
}
if (obj && obj.openaps && obj.openaps.enacted && obj.openaps.enacted.predBGs) {
var enactedPredBGs = obj.openaps.enacted.predBGs;
var predictionTypes = ['IOB', 'COB', 'UAM', 'ZT'];
predictionTypes.forEach(function(type) {
if (Array.isArray(enactedPredBGs[type]) && enactedPredBGs[type].length > maxSize) {
enactedPredBGs[type] = enactedPredBGs[type].slice(0, maxSize);
}
});
}
return obj;
}
function storage (env, ctx) {
var collection = env.devicestatus_collection;
var predictionsMaxSize = env.predictionsMaxSize || null;
function create (statuses, fn) {
@@ -22,6 +53,8 @@ function storage (collection, ctx) {
obj.created_at = d.toISOString();
obj.utcOffset = d.utcOffset();
obj = truncatePredictions(obj, predictionsMaxSize);
api().insertOne(obj, function(err, insertResult) {
if (err) {
console.log('Error inserting the device status object', err.message);
+8
View File
@@ -130,6 +130,14 @@ function setStorage () {
env.food_collection = readENV('MONGO_FOOD_COLLECTION', 'food');
env.activity_collection = readENV('MONGO_ACTIVITY_COLLECTION', 'activity');
var predictionsMaxSizeEnv = readENV('PREDICTIONS_MAX_SIZE', null);
if (predictionsMaxSizeEnv !== null) {
var parsed = parseInt(predictionsMaxSizeEnv, 10);
env.predictionsMaxSize = !isNaN(parsed) && parsed > 0 ? parsed : 288;
} else {
env.predictionsMaxSize = null;
}
var DB = { url: null, collection: null }
, DB_URL = DB.url ? DB.url : env.storageURI
, DB_COLLECTION = DB.collection ? DB.collection : env.entries_collection;
+105
View File
@@ -349,6 +349,14 @@ describe('v1 API Partial Failures and Edge Cases', function() {
});
describe('Large Document Handling', function() {
this.timeout(120000);
beforeEach(function(done) {
// Clear devicestatus before each test
self.ctx.devicestatus.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function() {
done();
});
});
it('devicestatus with large prediction arrays is inserted successfully', function(done) {
// SPEC: OpenAPS devicestatus can have large prediction arrays
@@ -388,6 +396,103 @@ describe('v1 API Partial Failures and Edge Cases', function() {
}
});
});
it('predictions are truncated when PREDICTIONS_MAX_SIZE is set', function(done) {
// SPEC: When PREDICTIONS_MAX_SIZE env var is set, prediction arrays
// exceeding that size should be truncated to prevent MongoDB issues
// with excessively large documents
// Store original value
const originalPredictionsMaxSize = self.env.predictionsMaxSize;
// Set truncation limit to 288 (24 hours of 5-min readings)
self.env.predictionsMaxSize = 288;
// Need to reinitialize devicestatus to pick up new setting
const devicestatusStorage = require('../lib/server/devicestatus');
self.ctx.devicestatus = devicestatusStorage(self.env, self.ctx);
// Create devicestatus with 350 predictions (exceeds 288 limit)
const deviceStatus = {
device: 'truncation-test',
created_at: new Date().toISOString(),
openaps: {
suggested: {
predBGs: {
IOB: Array.from({ length: 350 }, (_, i) => 120 - i * 0.1),
COB: Array.from({ length: 350 }, (_, i) => 120 - i * 0.05),
UAM: Array.from({ length: 350 }, (_, i) => 120 + i * 0.02),
ZT: Array.from({ length: 350 }, (_, i) => 120 - i * 0.08)
}
}
}
};
self.ctx.devicestatus.create([deviceStatus], function(err, result) {
should.not.exist(err);
should.exist(result);
result.should.be.instanceof(Array);
result.length.should.equal(1);
// Verify truncation occurred
const savedPredBGs = result[0].openaps.suggested.predBGs;
savedPredBGs.IOB.length.should.equal(288, 'IOB should be truncated to 288');
savedPredBGs.COB.length.should.equal(288, 'COB should be truncated to 288');
savedPredBGs.UAM.length.should.equal(288, 'UAM should be truncated to 288');
savedPredBGs.ZT.length.should.equal(288, 'ZT should be truncated to 288');
console.log(` ✓ Predictions truncated from 350 to 288 elements`);
// Restore original value
self.env.predictionsMaxSize = originalPredictionsMaxSize;
done();
});
});
it('predictions are NOT truncated when PREDICTIONS_MAX_SIZE is not set', function(done) {
// SPEC: Without PREDICTIONS_MAX_SIZE env var, prediction arrays
// should be preserved at their original size
// Ensure truncation is disabled
self.env.predictionsMaxSize = null;
// Reinitialize devicestatus to pick up setting
const devicestatusStorage = require('../lib/server/devicestatus');
self.ctx.devicestatus = devicestatusStorage(self.env, self.ctx);
// Create devicestatus with small predictions (100 elements)
const deviceStatus = {
device: 'no-truncation-test',
created_at: new Date().toISOString(),
openaps: {
suggested: {
predBGs: {
IOB: Array.from({ length: 100 }, (_, i) => 120 - i * 0.1),
COB: Array.from({ length: 100 }, (_, i) => 120 - i * 0.05)
}
}
}
};
self.ctx.devicestatus.create([deviceStatus], function(err, result) {
should.not.exist(err);
should.exist(result);
result.should.be.instanceof(Array);
result.length.should.equal(1);
// Verify NO truncation occurred
const savedPredBGs = result[0].openaps.suggested.predBGs;
savedPredBGs.IOB.length.should.equal(100, 'IOB should remain at 100');
savedPredBGs.COB.length.should.equal(100, 'COB should remain at 100');
console.log(` ✓ Predictions preserved at original 100 elements (no truncation)`);
done();
});
});
});
describe('Validation Error Handling', function() {
+7 -5
View File
@@ -155,16 +155,18 @@ module.exports = {
openaps: {
suggested: {
predBGs: {
IOB: Array.from({ length: 1000 }, (_, i) => 120 - i * 0.1),
COB: Array.from({ length: 1000 }, (_, i) => 120 - i * 0.05),
UAM: Array.from({ length: 1000 }, (_, i) => 120 + i * 0.02),
ZT: Array.from({ length: 1000 }, (_, i) => 120 - i * 0.08)
IOB: Array.from({ length: 350 }, (_, i) => 120 - i * 0.1),
COB: Array.from({ length: 350 }, (_, i) => 120 - i * 0.05),
UAM: Array.from({ length: 350 }, (_, i) => 120 + i * 0.02),
ZT: Array.from({ length: 350 }, (_, i) => 120 - i * 0.08)
}
}
}
},
originalArrayLength: 350,
bsonSizeNote: 'Test that predictions arrays do not exceed 16MB BSON limit',
expectedBehavior: 'Insert should succeed - typical devicestatus is well under limit'
expectedBehavior: 'Insert should succeed - typical devicestatus is well under limit',
truncationNote: 'With PREDICTIONS_MAX_SIZE=288, arrays will be truncated to 288 elements'
},
connectionFailureMidBatch: {