@jsreport/jsreport-core 3.11.1 → 3.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -282,6 +282,21 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 3.11.3
286
+
287
+ - update vm2 to fix security issue
288
+ - automatically disable full profiling after some time to avoid performance degradation
289
+ - improvements to full profile serialization (prevent blocking)
290
+ - fix profiles cleaning and calculate timeout in beforeRender
291
+
292
+ ### 3.11.2
293
+
294
+ - add `options.onReqReady` to be able to receive the parsed req values
295
+
296
+ ### 3.11.1
297
+
298
+ - fix error when trying to read `req.options` on reporter main code when `enableRequestReportTimeout` is enabled
299
+
285
300
  ### 3.11.0
286
301
 
287
302
  - log when worker returns bad res.content
@@ -176,6 +176,16 @@ module.exports.getRootSchemaOptions = () => ({
176
176
  type: 'string',
177
177
  default: 'standard'
178
178
  },
179
+ fullModeDurationCheckInterval: {
180
+ type: ['string', 'number'],
181
+ '$jsreport-acceptsDuration': true,
182
+ default: '10m'
183
+ },
184
+ fullModeDuration: {
185
+ type: ['string', 'number'],
186
+ '$jsreport-acceptsDuration': true,
187
+ default: '4h'
188
+ },
179
189
  maxProfilesHistory: {
180
190
  type: 'number',
181
191
  default: 1000
@@ -185,6 +195,11 @@ module.exports.getRootSchemaOptions = () => ({
185
195
  '$jsreport-acceptsDuration': true,
186
196
  default: '1m'
187
197
  },
198
+ maxUnallocatedProfileAge: {
199
+ type: ['string', 'number'],
200
+ '$jsreport-acceptsDuration': true,
201
+ default: '24h'
202
+ },
188
203
  maxDiffSize: {
189
204
  type: ['string', 'number'],
190
205
  '$jsreport-acceptsSize': true,
@@ -4,6 +4,9 @@ const extend = require('node.extend.without.arrays')
4
4
  const generateRequestId = require('../shared/generateRequestId')
5
5
  const fs = require('fs/promises')
6
6
  const { SPLAT } = require('triple-beam')
7
+ const promisify = require('util').promisify
8
+ const stringifyAsync = promisify(require('yieldable-json').stringifyAsync)
9
+
7
10
  module.exports = (reporter) => {
8
11
  reporter.documentStore.registerEntityType('ProfileType', {
9
12
  templateShortid: { type: 'Edm.String', referenceTo: 'templates' },
@@ -104,8 +107,11 @@ module.exports = (reporter) => {
104
107
  req.context.profiling.lastOperation = lastOperation
105
108
  }
106
109
 
107
- runInProfilerChain(() => {
108
- return fs.appendFile(req.context.profiling.logFilePath, Buffer.from(events.map(m => JSON.stringify(m)).join('\n') + '\n'))
110
+ runInProfilerChain(async () => {
111
+ const stringifiedMessages = req.context.mode === 'full'
112
+ ? await Promise.all(events.map(m => stringifyAsync(m)))
113
+ : events.map(m => JSON.stringify(m))
114
+ await fs.appendFile(req.context.profiling.logFilePath, Buffer.from(stringifiedMessages.join('\n') + '\n'))
109
115
  }, req)
110
116
  }
111
117
 
@@ -167,8 +173,7 @@ module.exports = (reporter) => {
167
173
  _id: reporter.documentStore.generateId(),
168
174
  timestamp: new Date(),
169
175
  state: 'queued',
170
- mode: req.context.profiling.mode,
171
- timeout: reporter.options.enableRequestReportTimeout && req.options.timeout ? req.options.timeout : reporter.options.reportTimeout
176
+ mode: req.context.profiling.mode
172
177
  }
173
178
 
174
179
  const { pathToFile } = await reporter.writeTempFile((uuid) => `${uuid}.log`, '')
@@ -204,7 +209,9 @@ module.exports = (reporter) => {
204
209
 
205
210
  reporter.beforeRenderListeners.add('profiler', async (req, res) => {
206
211
  const update = {
207
- state: 'running'
212
+ state: 'running',
213
+ // the timeout needs to be calculated later here, because the req.options.timeout isnt yet parsed in beforeRenderWorkerAllocatedListeners
214
+ timeout: reporter.options.enableRequestReportTimeout && req.options.timeout ? req.options.timeout : reporter.options.reportTimeout
208
215
  }
209
216
 
210
217
  // we set the request here because this listener will container the req which
@@ -365,6 +372,7 @@ module.exports = (reporter) => {
365
372
  }
366
373
 
367
374
  let profilesCleanupInterval
375
+ let fullModeDurationCheckInterval
368
376
 
369
377
  reporter.initializeListeners.add('profiler', async () => {
370
378
  reporter.documentStore.collection('profiles').beforeRemoveListeners.add('profiles', async (query, req) => {
@@ -377,13 +385,21 @@ module.exports = (reporter) => {
377
385
  }
378
386
  })
379
387
 
388
+ // exposing it to jo for override
380
389
  function profilesCleanupExec () {
381
390
  return reporter._profilesCleanup()
382
391
  }
383
392
 
393
+ function fullModeDurationCheckExec () {
394
+ return reporter._profilesFullModeDurationCheck()
395
+ }
396
+
384
397
  profilesCleanupInterval = setInterval(profilesCleanupExec, reporter.options.profiler.cleanupInterval)
385
398
  profilesCleanupInterval.unref()
386
399
 
400
+ fullModeDurationCheckInterval = setInterval(fullModeDurationCheckExec, reporter.options.profiler.fullModeDurationCheckInterval)
401
+ fullModeDurationCheckInterval.unref()
402
+
387
403
  await reporter._profilesCleanup()
388
404
  })
389
405
 
@@ -392,6 +408,10 @@ module.exports = (reporter) => {
392
408
  clearInterval(profilesCleanupInterval)
393
409
  }
394
410
 
411
+ if (fullModeDurationCheckInterval) {
412
+ clearInterval(fullModeDurationCheckInterval)
413
+ }
414
+
395
415
  for (const key of profilerOperationsChainsMap.keys()) {
396
416
  const profileAppendPromise = profilerOperationsChainsMap.get(key)
397
417
  if (profileAppendPromise) {
@@ -445,11 +465,28 @@ module.exports = (reporter) => {
445
465
  }
446
466
 
447
467
  if (!profile.timeout) {
468
+ // we can calculate profile timeout only after worker parses request and req.options.timeout is calculated
469
+ // if the timeout isnt calculated we error orphans that hangs for very long time before worker gets allocated and parses req
470
+ if ((profile.timestamp.getTime() + reporter.options.profiler.maxUnallocatedProfileAge) < new Date().getTime()) {
471
+ try {
472
+ await reporter.documentStore.collection('profiles').update({
473
+ _id: profile._id
474
+ }, {
475
+ $set: {
476
+ state: 'error',
477
+ finishedOn: new Date(),
478
+ error: `The request wasn't parsed before ${reporter.options.profiler.maxUnallocatedProfileAge}ms. This can happen when the server is unexpectedly stopped.`
479
+ }
480
+ })
481
+ } catch (e) {
482
+ lastError = e
483
+ }
484
+ }
448
485
  continue
449
486
  }
450
487
 
451
- const whenShouldBeFinished = profile.timestamp + profile.timeout + reporter.options.reportTimeoutMargin * 2
452
- if (whenShouldBeFinished < new Date().getTime()) {
488
+ const whenShouldBeFinished = profile.timestamp.getTime() + profile.timeout + reporter.options.reportTimeoutMargin * 2
489
+ if (whenShouldBeFinished > new Date().getTime()) {
453
490
  continue
454
491
  }
455
492
 
@@ -478,6 +515,29 @@ module.exports = (reporter) => {
478
515
  }
479
516
  }
480
517
 
518
+ reporter._profilesFullModeDurationCheck = async function () {
519
+ try {
520
+ if (reporter.options.profiler.defaultMode === 'full') {
521
+ return
522
+ }
523
+ const profiler = await reporter.documentStore.collection('settings').findOne({ key: 'profiler' })
524
+ if (profiler == null || (profiler.modificationDate.getTime() + reporter.options.profiler.fullModeDuration) > new Date().getTime()) {
525
+ return
526
+ }
527
+
528
+ const profilerValue = JSON.parse(profiler.value)
529
+
530
+ if (profilerValue.mode !== 'full') {
531
+ return
532
+ }
533
+
534
+ reporter.logger.info('Switching full mode profiling back to standard to avoid performance degradation.')
535
+ await reporter.settings.addOrSet('profiler', { mode: 'standard' })
536
+ } catch (e) {
537
+ reporter.logger.warn('Failed to change profiling mode', e)
538
+ }
539
+ }
540
+
481
541
  return function cleanProfileInRequest (req) {
482
542
  // - req.context.profiling is empty only on an early error
483
543
  // that happens before setting the profiler.
@@ -417,6 +417,7 @@ class MainReporter extends Reporter {
417
417
  }
418
418
 
419
419
  req = Request(req)
420
+ options.onReqReady?.(req)
420
421
 
421
422
  // TODO: we will probably validate in the thread
422
423
  if (this.entityTypeValidator.getSchema('TemplateType') != null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.11.1",
3
+ "version": "3.11.3",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -71,9 +71,10 @@
71
71
  "triple-beam": "1.3.0",
72
72
  "unset-value": "1.0.0",
73
73
  "uuid": "8.3.2",
74
- "vm2": "3.9.11",
74
+ "vm2": "3.9.17",
75
75
  "winston": "3.8.1",
76
- "winston-transport": "4.5.0"
76
+ "winston-transport": "4.5.0",
77
+ "yieldable-json": "2.0.1"
77
78
  },
78
79
  "devDependencies": {
79
80
  "mocha": "9.2.2",