@live-change/backup-service 0.9.209 → 0.9.212

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.
Files changed (3) hide show
  1. package/clear.js +31 -0
  2. package/index.js +130 -30
  3. package/package.json +5 -5
package/clear.js CHANGED
@@ -97,4 +97,35 @@ export async function removeOldCommands(before, options) {
97
97
  if(options.delay) await new Promise(resolve => setTimeout(resolve, options.delay))
98
98
  more = deleteStats.count >= bucket
99
99
  }
100
+ }
101
+
102
+ const clearEventReportsQuery = `${async (input, output, { indexName, idPrefix, bucket, before }) => {
103
+ const indexReader = await input.index(indexName)
104
+ const tableWriter = await output.table('eventReports')
105
+ const pointers = await indexReader.rangeGet({ limit: bucket, lt: before })
106
+ for(const pointer of pointers) {
107
+ await tableWriter.delete(idPrefix + pointer.to)
108
+ }
109
+ await output.put({ count: pointers.length })
110
+ }}`
111
+
112
+ async function removeOldEventReportsForIndex(indexName, idPrefix, before, options) {
113
+ const bucket = options?.bucket || 128
114
+ let more = true
115
+ while(more) {
116
+ const queryResult = await app.dao.request(['database', 'query'], app.databaseName, clearEventReportsQuery,
117
+ { indexName, idPrefix, bucket, before })
118
+ const deleteStats = queryResult[0]
119
+ if(options?.delay) await new Promise(resolve => setTimeout(resolve, options.delay))
120
+ more = deleteStats.count >= bucket
121
+ }
122
+ }
123
+
124
+ export async function removeOldEventReports(commandBefore, triggerBefore, options = {}) {
125
+ if(!app.splitCommands && commandBefore) {
126
+ await removeOldEventReportsForIndex('commands_byTimestamp', 'command_', commandBefore, options)
127
+ }
128
+ if(!app.splitTriggers && triggerBefore) {
129
+ await removeOldEventReportsForIndex('triggers_byTimestamp', 'trigger_', triggerBefore, options)
130
+ }
100
131
  }
package/index.js CHANGED
@@ -12,13 +12,15 @@ import { createBackup, createBackupLogger, currentBackupPath, removeOldBackups }
12
12
  import { restoreBackup } from './restore.js'
13
13
  import {
14
14
  getLastEventId, removeOldEvents, removeOldOpLogs,
15
- getLastCommandTimestamp, getLastTriggerTimestamp, removeOldTriggers, removeOldCommands
15
+ getLastCommandTimestamp, getLastTriggerTimestamp, removeOldTriggers, removeOldCommands,
16
+ removeOldEventReports
16
17
  } from './clear.js'
17
18
 
18
19
  import progress from "progress-stream"
19
20
 
20
21
  const TWENTY_FOUR_HOURS = 24 * 3600 * 1000
21
22
  const TEN_MINUTES = 10 * 60 * 1000
23
+ const DEFAULT_OP_LOG_RETENTION_MS = 60 * 60 * 1000
22
24
 
23
25
  let currentBackup = null
24
26
 
@@ -43,7 +45,7 @@ if(backupServerUsername && backupServerPassword ) {
43
45
  users[backupServerUsername] = backupServerPassword
44
46
  }
45
47
 
46
- if(Object.keys(users) > 0) {
48
+ if(Object.keys(users).length > 0) {
47
49
  expressApp.use(basicAuth({
48
50
  users,
49
51
  challenge: true,
@@ -51,6 +53,87 @@ if(Object.keys(users) > 0) {
51
53
  }))
52
54
  }
53
55
 
56
+ function resolveRetentionMs(option) {
57
+ const value = config[option]
58
+ if(Number.isInteger(value)) return value
59
+ if(value === true && Number.isInteger(config.retentionMs)) return config.retentionMs
60
+ if(value !== false && Number.isInteger(config.retentionMs)) return config.retentionMs
61
+ return null
62
+ }
63
+
64
+ function shouldClear(option) {
65
+ const value = config[option]
66
+ if(value === false) return false
67
+ if(Number.isInteger(value)) return true
68
+ if(value === true) return true
69
+ if(Number.isInteger(config.retentionMs)) return true
70
+ return false
71
+ }
72
+
73
+ function resolveOpLogRetentionMs() {
74
+ if(Number.isInteger(config.opLogRetentionMs)) return config.opLogRetentionMs
75
+ if(config.clearOpLogs === true && Number.isInteger(config.retentionMs)) return config.retentionMs
76
+ if(Number.isInteger(config.clearOpLogs)) return config.clearOpLogs
77
+ return DEFAULT_OP_LOG_RETENTION_MS
78
+ }
79
+
80
+ function resolveEventReportsRetentionMs() {
81
+ if(Number.isInteger(config.clearEventReports)) return config.clearEventReports
82
+ return resolveRetentionMs('clearEventReports')
83
+ ?? resolveRetentionMs('clearCommands')
84
+ ?? resolveRetentionMs('clearTriggers')
85
+ }
86
+
87
+ function computeTimestampCutoff(lastTimestamp, retentionMs) {
88
+ if(retentionMs === null) return null
89
+ const date = new Date(lastTimestamp)
90
+ if(!Number.isInteger(date.getTime())) return null
91
+ return new Date(date.getTime() - retentionMs).toISOString()
92
+ }
93
+
94
+ function computeCutoffs({ lastEventId, lastTriggerTimestamp, lastCommandTimestamp }) {
95
+ const eventsRetention = resolveRetentionMs('clearEvents')
96
+ let eventsBefore = lastEventId
97
+ if(shouldClear('clearEvents') && eventsRetention !== null && lastEventId) {
98
+ const ts = (+(lastEventId.split(':')[0]) - eventsRetention)
99
+ eventsBefore = ts.toFixed(0).padStart(16, '0') + ':'
100
+ } else if(!shouldClear('clearEvents')) {
101
+ eventsBefore = null
102
+ }
103
+
104
+ const triggersRetention = resolveRetentionMs('clearTriggers')
105
+ let triggersBefore = lastTriggerTimestamp
106
+ if(shouldClear('clearTriggers')) {
107
+ triggersBefore = computeTimestampCutoff(lastTriggerTimestamp, triggersRetention)
108
+ } else {
109
+ triggersBefore = null
110
+ }
111
+
112
+ const commandsRetention = resolveRetentionMs('clearCommands')
113
+ let commandsBefore = lastCommandTimestamp
114
+ if(shouldClear('clearCommands')) {
115
+ commandsBefore = computeTimestampCutoff(lastCommandTimestamp, commandsRetention)
116
+ } else {
117
+ commandsBefore = null
118
+ }
119
+
120
+ const eventReportsRetention = resolveEventReportsRetentionMs()
121
+ let eventReportsCommandsBefore = null
122
+ let eventReportsTriggersBefore = null
123
+ if(shouldClear('clearEventReports') && eventReportsRetention !== null) {
124
+ eventReportsCommandsBefore = computeTimestampCutoff(lastCommandTimestamp, eventReportsRetention)
125
+ eventReportsTriggersBefore = computeTimestampCutoff(lastTriggerTimestamp, eventReportsRetention)
126
+ }
127
+
128
+ return {
129
+ eventsBefore,
130
+ triggersBefore,
131
+ commandsBefore,
132
+ eventReportsCommandsBefore,
133
+ eventReportsTriggersBefore,
134
+ opLogRetentionMs: shouldClear('clearOpLogs') ? resolveOpLogRetentionMs() : null
135
+ }
136
+ }
54
137
 
55
138
  function doBackup() {
56
139
  console.log("DOING BACKUP!")
@@ -72,38 +155,55 @@ function doBackup() {
72
155
  console.log("Last trigger timestamp:", lastTriggerTimestamp)
73
156
  console.log("Last command timestamp:", lastCommandTimestamp)
74
157
 
75
- if(Number.isInteger(config.clearEvents) && lastEventId) {
76
- const ts = (+(lastEventId.split(':')[0]) - config.clearEvents)
77
- console.log("Clear events before", new Date(ts))
78
- lastEventId = ts.toFixed(0).padStart(16, '0')+':'
79
- }
80
- if(Number.isInteger(config.clearTriggers) && Number.isInteger(new Date(lastTriggerTimestamp).getTime())) {
81
- const date = new Date(lastTriggerTimestamp)
82
- const old = new Date(date.getTime() - config.clearTriggers)
83
- console.log("Clear triggers before", old)
84
- lastTriggerTimestamp = old.toISOString()
158
+ const cutoffs = computeCutoffs({ lastEventId, lastTriggerTimestamp, lastCommandTimestamp })
159
+
160
+ if(cutoffs.eventsBefore) console.log("Clear events before", new Date(+(cutoffs.eventsBefore.split(':')[0])))
161
+ if(cutoffs.triggersBefore) console.log("Clear triggers before", cutoffs.triggersBefore)
162
+ if(cutoffs.commandsBefore) console.log("Clear commands before", cutoffs.commandsBefore)
163
+ if(cutoffs.eventReportsCommandsBefore) {
164
+ console.log("Clear eventReports (commands) before", cutoffs.eventReportsCommandsBefore)
85
165
  }
86
- if(Number.isInteger(config.clearCommands) && Number.isInteger(new Date(lastCommandTimestamp).getTime())) {
87
- const date = new Date(lastCommandTimestamp)
88
- const old = new Date(date.getTime() - config.clearCommands)
89
- console.log("Clear commands before", old)
90
- lastCommandTimestamp = old.toISOString()
166
+ if(cutoffs.eventReportsTriggersBefore) {
167
+ console.log("Clear eventReports (triggers) before", cutoffs.eventReportsTriggersBefore)
91
168
  }
92
169
 
93
170
  console.log("====== CLEARING:")
94
- console.log("Last remaining event id:", lastEventId)
95
- console.log("Last remaining trigger timestamp:", lastTriggerTimestamp)
96
- console.log("Last remaining command timestamp:", lastCommandTimestamp)
97
-
98
- if(config.clearCommands) await removeOldCommands(lastCommandTimestamp, { delay: 10 })
99
- if(config.clearTriggers) await removeOldTriggers(lastTriggerTimestamp, { delay: 10 })
100
- if(config.clearEvents) await removeOldEvents(lastEventId)
101
- if(config.clearOpLogs) await removeOldOpLogs()
171
+ console.log("Last remaining event id:", cutoffs.eventsBefore)
172
+ console.log("Last remaining trigger timestamp:", cutoffs.triggersBefore)
173
+ console.log("Last remaining command timestamp:", cutoffs.commandsBefore)
174
+
175
+ if(shouldClear('clearEventReports')) {
176
+ await backupLog('clearEventReports start')
177
+ await removeOldEventReports(
178
+ cutoffs.eventReportsCommandsBefore,
179
+ cutoffs.eventReportsTriggersBefore,
180
+ { delay: 10 }
181
+ )
182
+ await backupLog('clearEventReports done')
183
+ }
184
+
185
+ if(shouldClear('clearCommands') && cutoffs.commandsBefore) {
186
+ await removeOldCommands(cutoffs.commandsBefore, { delay: 10 })
187
+ }
188
+ if(shouldClear('clearTriggers') && cutoffs.triggersBefore) {
189
+ await removeOldTriggers(cutoffs.triggersBefore, { delay: 10 })
190
+ }
191
+ if(shouldClear('clearEvents') && cutoffs.eventsBefore) {
192
+ await removeOldEvents(cutoffs.eventsBefore)
193
+ }
194
+ if(shouldClear('clearOpLogs') && cutoffs.opLogRetentionMs !== null) {
195
+ await removeOldOpLogs(cutoffs.opLogRetentionMs)
196
+ }
102
197
  })
103
198
  }
104
- currentBackup.promise.then(done => {
199
+ currentBackup.promise.then(async () => {
105
200
  console.log("BACKUP CREATED!")
106
- fs.promises.writeFile(backupsDir + '/latest.txt', filename+'.tar.gz')
201
+ await fs.promises.writeFile(backupsDir + '/latest.txt', filename+'.tar.gz')
202
+ currentBackup = null
203
+ }).catch(async err => {
204
+ const detail = err && err.stack ? err.stack : String(err)
205
+ console.error("BACKUP FAILED:", err)
206
+ await backupLog(`backup failed: ${detail}`)
107
207
  currentBackup = null
108
208
  })
109
209
  return currentBackup
@@ -114,7 +214,7 @@ expressApp.get('/backup/doBackup', async (req, res) => {
114
214
  res.status(200).send('Backup in progress: ' + currentBackup.filename)
115
215
  return
116
216
  }
117
- await doBackup()
217
+ await queue.add(() => doBackup())
118
218
  res.status(200).send('Creating backup: '+currentBackup.filename)
119
219
  })
120
220
 
@@ -195,7 +295,7 @@ definition.afterStart(() => {
195
295
  console.log('Service init backup:')
196
296
  await queue.add(() => doBackup())
197
297
  console.log('Service init backup completed')
198
- }, 1*60*1000) //TEN_MINUTES)
298
+ }, TEN_MINUTES)
199
299
 
200
300
  setInterval(async () => {
201
301
  console.log('Daily backup:')
@@ -204,4 +304,4 @@ definition.afterStart(() => {
204
304
  }, TWENTY_FOUR_HOURS)
205
305
  })
206
306
 
207
- })
307
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/backup-service",
3
- "version": "0.9.209",
3
+ "version": "0.9.212",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -9,14 +9,14 @@
9
9
  "author": "Michał Łaszczewski <michal@emikse.com>",
10
10
  "license": "BSD-3-Clause",
11
11
  "dependencies": {
12
- "@live-change/db-client": "^0.9.209",
13
- "@live-change/framework": "^0.9.209",
12
+ "@live-change/db-client": "^0.9.212",
13
+ "@live-change/framework": "^0.9.212",
14
14
  "express": "^4.18.2",
15
15
  "express-basic-auth": "^1.2.1",
16
16
  "fs-extra": "^11.1.1",
17
- "p-queue": "8.1.0",
17
+ "p-queue": "^8.1.1",
18
18
  "progress-stream": "^2.0.0"
19
19
  },
20
- "gitHead": "e1f934934d5d8a119f7ed27aa97bc6b9e9791719",
20
+ "gitHead": "40268236c57d5f123c1e8f1815c310464d043d52",
21
21
  "type": "module"
22
22
  }