@jsreport/jsreport-core 3.10.0 → 3.11.1

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,14 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 3.11.0
286
+
287
+ - log when worker returns bad res.content
288
+ - fix profiler leaks
289
+ - remove settings sync API and avoid loading all items to memory
290
+ - throw weak error when validating duplicated entity
291
+ - ensure we end with profiles with error state when there is server or req timeout
292
+
285
293
  ### 3.10.0
286
294
 
287
295
  - `mainReporter.executeWorkerAction` now supports cancellation with `AbortController.signal`
@@ -74,6 +74,7 @@ async function validateDuplicatedName (reporter, c, doc, originalIdValue, req) {
74
74
 
75
75
  throw reporter.createError(msg, {
76
76
  statusCode: 400,
77
+ weak: true,
77
78
  code: 'DUPLICATED_ENTITY',
78
79
  existingEntity: existingEntity.entity,
79
80
  existingEntityEntitySet: existingEntity.entitySet
@@ -1,9 +1,9 @@
1
1
  const EventEmitter = require('events')
2
- const winston = require('winston')
2
+ const Transport = require('winston-transport')
3
3
  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
7
  module.exports = (reporter) => {
8
8
  reporter.documentStore.registerEntityType('ProfileType', {
9
9
  templateShortid: { type: 'Edm.String', referenceTo: 'templates' },
@@ -12,7 +12,8 @@ module.exports = (reporter) => {
12
12
  state: { type: 'Edm.String' },
13
13
  error: { type: 'Edm.String' },
14
14
  mode: { type: 'Edm.String', schema: { enum: ['full', 'standard', 'disabled'] } },
15
- blobName: { type: 'Edm.String' }
15
+ blobName: { type: 'Edm.String' },
16
+ timeout: { type: 'Edm.Int32' }
16
17
  })
17
18
 
18
19
  reporter.documentStore.registerEntitySet('profiles', {
@@ -23,7 +24,6 @@ module.exports = (reporter) => {
23
24
  const profilersMap = new Map()
24
25
  const profilerOperationsChainsMap = new Map()
25
26
  const profilerRequestMap = new Map()
26
- const profilerLogRequestMap = new Map()
27
27
 
28
28
  function runInProfilerChain (fnOrOptions, req) {
29
29
  if (req.context.profiling.mode === 'disabled') {
@@ -89,7 +89,7 @@ module.exports = (reporter) => {
89
89
  for (const m of events) {
90
90
  if (m.type === 'log') {
91
91
  if (log) {
92
- reporter.logger[m.level](m.message, { ...req, ...m.meta, timestamp: m.timestamp, fromEmitProfile: true })
92
+ reporter.logger[m.level](m.message, { ...req, ...m.meta, timestamp: m.timestamp, logged: true })
93
93
  }
94
94
  } else {
95
95
  lastOperation = m
@@ -167,7 +167,8 @@ module.exports = (reporter) => {
167
167
  _id: reporter.documentStore.generateId(),
168
168
  timestamp: new Date(),
169
169
  state: 'queued',
170
- mode: req.context.profiling.mode
170
+ mode: req.context.profiling.mode,
171
+ timeout: reporter.options.enableRequestReportTimeout && req.options.timeout ? req.options.timeout : reporter.options.reportTimeout
171
172
  }
172
173
 
173
174
  const { pathToFile } = await reporter.writeTempFile((uuid) => `${uuid}.log`, '')
@@ -325,49 +326,40 @@ module.exports = (reporter) => {
325
326
  }
326
327
  })
327
328
 
329
+ // we want to add to profiles also log messages from the main
328
330
  const configuredPreviously = reporter.logger.__profilerConfigured__ === true
329
-
330
331
  if (!configuredPreviously) {
331
- const originalLog = reporter.logger.log
332
-
333
- // we want to catch the original request
334
- reporter.logger.log = function (level, msg, ...splat) {
335
- const [meta] = splat
332
+ // we emit from winston transport, so winston formatters can still format message
333
+ class EmittingProfilesTransport extends Transport {
334
+ log (info, callback) {
335
+ setImmediate(() => {
336
+ this.emit('logged', info)
337
+ })
338
+
339
+ if (info[SPLAT]) {
340
+ const [req] = info[SPLAT]
341
+
342
+ if (req && req.context && req.logged !== true) {
343
+ emitProfiles({
344
+ events: [createProfileMessage({
345
+ type: 'log',
346
+ level: info.level,
347
+ message: info.message,
348
+ previousOperationId: req.context.profiling.lastOperationId
349
+ }, req)],
350
+ log: false
351
+ }, req)
352
+ }
353
+ }
336
354
 
337
- if (typeof meta === 'object' && meta !== null && meta.context?.rootId != null) {
338
- profilerLogRequestMap.set(meta.context.rootId, meta)
355
+ callback()
339
356
  }
340
-
341
- return originalLog.call(this, level, msg, ...splat)
342
357
  }
343
358
 
344
- const mainLogsToProfile = winston.format((info) => {
345
- // propagate the request logs occurring on main to the profile
346
- if (info.rootId != null && info.fromEmitProfile == null && profilerLogRequestMap.has(info.rootId)) {
347
- const req = profilerLogRequestMap.get(info.rootId)
348
-
349
- emitProfiles({
350
- events: [createProfileMessage({
351
- type: 'log',
352
- level: info.level,
353
- message: info.message,
354
- previousOperationId: req.context.profiling.lastOperationId
355
- }, req)],
356
- log: false
357
- }, req)
358
- }
359
-
360
- if (info.fromEmitProfile != null) {
361
- delete info.fromEmitProfile
362
- }
363
-
364
- return info
365
- })
366
-
367
- reporter.logger.format = winston.format.combine(
368
- reporter.logger.format,
369
- mainLogsToProfile()
370
- )
359
+ reporter.logger.add(new EmittingProfilesTransport({
360
+ format: reporter.logger.format,
361
+ level: 'debug'
362
+ }))
371
363
 
372
364
  reporter.logger.__profilerConfigured__ = true
373
365
  }
@@ -410,7 +402,6 @@ module.exports = (reporter) => {
410
402
  profilersMap.clear()
411
403
  profilerOperationsChainsMap.clear()
412
404
  profilerRequestMap.clear()
413
- profilerLogRequestMap.clear()
414
405
  })
415
406
 
416
407
  let profilesCleanupRunning = false
@@ -422,11 +413,13 @@ module.exports = (reporter) => {
422
413
 
423
414
  profilesCleanupRunning = true
424
415
 
425
- let lastRemoveError
416
+ let lastError
426
417
 
427
418
  try {
428
- const profiles = await reporter.documentStore.collection('profiles').find({}).sort({ timestamp: -1 })
429
- const profilesToRemove = profiles.slice(reporter.options.profiler.maxProfilesHistory)
419
+ const profilesToRemove = await reporter.documentStore.collection('profiles')
420
+ .find({}, { _id: 1 }).sort({ timestamp: -1 })
421
+ .skip(reporter.options.profiler.maxProfilesHistory)
422
+ .toArray()
430
423
 
431
424
  for (const profile of profilesToRemove) {
432
425
  if (reporter.closed || reporter.closing) {
@@ -438,7 +431,40 @@ module.exports = (reporter) => {
438
431
  _id: profile._id
439
432
  })
440
433
  } catch (e) {
441
- lastRemoveError = e
434
+ lastError = e
435
+ }
436
+ }
437
+
438
+ const notFinishedProfiles = await reporter.documentStore.collection('profiles')
439
+ .find({ $or: [{ state: 'running' }, { state: 'queued' }] }, { _id: 1, timeout: 1, timestamp: 1 })
440
+ .toArray()
441
+
442
+ for (const profile of notFinishedProfiles) {
443
+ if (reporter.closed || reporter.closing) {
444
+ return
445
+ }
446
+
447
+ if (!profile.timeout) {
448
+ continue
449
+ }
450
+
451
+ const whenShouldBeFinished = profile.timestamp + profile.timeout + reporter.options.reportTimeoutMargin * 2
452
+ if (whenShouldBeFinished < new Date().getTime()) {
453
+ continue
454
+ }
455
+
456
+ try {
457
+ await reporter.documentStore.collection('profiles').update({
458
+ _id: profile._id
459
+ }, {
460
+ $set: {
461
+ state: 'error',
462
+ finishedOn: new Date(),
463
+ error: 'The server did not update the report profile before its timeout. This can happen when the server is unexpectedly stopped.'
464
+ }
465
+ })
466
+ } catch (e) {
467
+ lastError = e
442
468
  }
443
469
  }
444
470
  } catch (e) {
@@ -447,8 +473,8 @@ module.exports = (reporter) => {
447
473
  profilesCleanupRunning = false
448
474
  }
449
475
 
450
- if (lastRemoveError) {
451
- reporter.logger.warn('Profile cleanup failed for some entities, last error:', lastRemoveError)
476
+ if (lastError) {
477
+ reporter.logger.warn('Profile cleanup failed for some entities, last error:', lastError)
452
478
  }
453
479
  }
454
480
 
@@ -461,7 +487,6 @@ module.exports = (reporter) => {
461
487
  profilersMap.delete(req.context.rootId)
462
488
  profilerOperationsChainsMap.delete(req.context.rootId)
463
489
  profilerRequestMap.delete(req.context.rootId)
464
- profilerLogRequestMap.delete(req.context.rootId)
465
490
  return
466
491
  }
467
492
 
@@ -471,7 +496,6 @@ module.exports = (reporter) => {
471
496
  profilersMap.delete(req.context.rootId)
472
497
  profilerOperationsChainsMap.delete(req.context.rootId)
473
498
  profilerRequestMap.delete(req.context.rootId)
474
- profilerLogRequestMap.delete(req.context.rootId)
475
499
  }
476
500
  }, req)
477
501
  }
@@ -378,6 +378,7 @@ class MainReporter extends Reporter {
378
378
  req.context.id = req.context.rootId
379
379
  req.context.reportCounter = ++reportCounter
380
380
  req.context.startTimestamp = new Date().getTime()
381
+ req.options = Object.assign({}, req.options)
381
382
 
382
383
  let worker
383
384
  let workerAborted
@@ -449,6 +450,7 @@ class MainReporter extends Reporter {
449
450
  } catch (err) {
450
451
  await this._handleRenderError(req, res, err).catch((e) => {})
451
452
  } finally {
453
+ this._cleanProfileInRequest(req)
452
454
  if (!workerAborted) {
453
455
  await worker.release(req)
454
456
  }
@@ -477,6 +479,10 @@ class MainReporter extends Reporter {
477
479
 
478
480
  await this.afterRenderListeners.fire(req, res)
479
481
 
482
+ if (!res.content) {
483
+ this.logger.error('Worker didnt return render res.content, returned:' + JSON.stringify(responseResult), req)
484
+ }
485
+
480
486
  res.stream = Readable.from(res.content)
481
487
 
482
488
  this._cleanProfileInRequest(req)
@@ -6,7 +6,7 @@
6
6
  const Request = require('../shared/request')
7
7
 
8
8
  const Settings = module.exports = function () {
9
- this._collection = []
9
+
10
10
  }
11
11
 
12
12
  Settings.prototype.add = function (key, value, req) {
@@ -15,15 +15,9 @@ Settings.prototype.add = function (key, value, req) {
15
15
  value: typeof value !== 'string' ? JSON.stringify(value) : value
16
16
  }
17
17
 
18
- this._collection.push(settingItem)
19
-
20
18
  return this.documentStore.collection('settings').insert(settingItem, localReqWithoutAuthorization(req))
21
19
  }
22
20
 
23
- Settings.prototype.get = function (key) {
24
- return this._collection.find((s) => s.key === key)
25
- }
26
-
27
21
  Settings.prototype.findValue = async function (key, req) {
28
22
  const res = await this.documentStore.collection('settings').find({ key: key }, localReqWithoutAuthorization(req))
29
23
  if (res.length !== 1) {
@@ -36,8 +30,6 @@ Settings.prototype.findValue = async function (key, req) {
36
30
  Settings.prototype.set = function (key, avalue, req) {
37
31
  const value = typeof avalue !== 'string' ? JSON.stringify(avalue) : avalue
38
32
 
39
- this.get(key).value = value
40
-
41
33
  return this.documentStore.collection('settings').update({
42
34
  key: key
43
35
  }, {
@@ -58,8 +50,6 @@ Settings.prototype.addOrSet = async function (key, avalue, req) {
58
50
  Settings.prototype.init = async function (documentStore, authorization) {
59
51
  this.documentStore = documentStore
60
52
 
61
- const incompatibleSettingsToRemove = []
62
-
63
53
  if (authorization != null) {
64
54
  const col = documentStore.collection('settings')
65
55
 
@@ -95,30 +85,6 @@ Settings.prototype.init = async function (documentStore, authorization) {
95
85
  }
96
86
  })
97
87
  }
98
-
99
- const res = await documentStore.collection('settings').find({})
100
-
101
- res.forEach((v) => {
102
- if (typeof v.value !== 'string') {
103
- return this._collection.push({
104
- key: v.key,
105
- value: v.value
106
- })
107
- }
108
-
109
- try {
110
- return this._collection.push({
111
- key: v.key,
112
- value: JSON.parse(v.value)
113
- })
114
- } catch (e) {
115
- incompatibleSettingsToRemove.push(v._id)
116
- }
117
- })
118
-
119
- if (incompatibleSettingsToRemove.length) {
120
- return documentStore.collection('settings').remove({ _id: { $in: incompatibleSettingsToRemove } })
121
- }
122
88
  }
123
89
 
124
90
  Settings.prototype.registerEntity = function (documentStore) {
@@ -13,7 +13,7 @@ module.exports = (level, msg, meta) => {
13
13
 
14
14
  // TODO adding cancel looks bad, its before script is adding req.cancel()
15
15
  // excluding non relevant properties for the log
16
- const newMeta = Object.assign({}, omit(meta, ['rawContent', 'template', 'options', 'data', 'context', 'timestamp', 'cancel']))
16
+ const newMeta = Object.assign({}, omit(meta, ['logged', 'rawContent', 'template', 'options', 'data', 'context', 'timestamp', 'cancel']))
17
17
 
18
18
  if (newMeta.rootId == null && meta.context.rootId != null) {
19
19
  newMeta.rootId = meta.context.rootId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "3.10.0",
3
+ "version": "3.11.1",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -40,7 +40,7 @@
40
40
  "@babel/parser": "7.14.4",
41
41
  "@babel/traverse": "7.12.9",
42
42
  "@colors/colors": "1.5.0",
43
- "@jsreport/advanced-workers": "1.2.3",
43
+ "@jsreport/advanced-workers": "1.2.4",
44
44
  "@jsreport/mingo": "2.4.1",
45
45
  "@jsreport/reap": "0.1.0",
46
46
  "ajv": "6.12.6",