|
1 | | -var AWS = require('aws-sdk'); |
2 | | -var Dyno = require('dyno'); |
3 | | -var queue = require('queue-async'); |
4 | | -var crypto = require('crypto'); |
5 | | -var https = require('https'); |
6 | | - |
7 | | -module.exports.replicate = replicate; |
8 | | -module.exports.backup = incrementalBackup; |
9 | | -module.exports.snapshot = require('./s3-snapshot'); |
10 | | -module.exports.agent = new https.Agent({ |
11 | | - keepAlive: true, |
12 | | - maxSockets: Math.ceil(require('os').cpus().length * 16), |
13 | | - keepAliveMsecs: 60000 |
14 | | -}) |
15 | | - |
16 | | -function replicate(event, context, callback) { |
17 | | - var replicaConfig = { |
18 | | - accessKeyId: process.env.ReplicaAccessKeyId || undefined, |
19 | | - secretAccessKey: process.env.ReplicaSecretAccessKey || undefined, |
20 | | - table: process.env.ReplicaTable, |
21 | | - region: process.env.ReplicaRegion, |
22 | | - maxRetries: 1000, |
23 | | - httpOptions: { |
24 | | - timeout: 2000, |
25 | | - agent: module.exports.agent |
26 | | - } |
27 | | - }; |
28 | | - |
29 | | - if (process.env.ReplicaEndpoint) replicaConfig.endpoint = process.env.ReplicaEndpoint; |
30 | | - var replica = new Dyno(replicaConfig); |
31 | | - |
32 | | - var keyAttrs = Object.keys(event.Records[0].dynamodb.Keys); |
33 | | - |
34 | | - var filterer; |
35 | | - if (process.env.TurnoverRole && process.env.TurnoverAt) { |
36 | | - // Filterer function should return true if the record SHOULD be processed |
37 | | - filterer = function(record) { |
38 | | - var created = Number(record.dynamodb.ApproximateCreationDateTime + '000'); |
39 | | - var turnoverAt = Number(process.env.TurnoverAt); |
40 | | - if (process.env.TurnoverRole === 'BEFORE') return created < turnoverAt; |
41 | | - else if (process.env.TurnoverRole === 'AFTER') return created >= turnoverAt; |
42 | | - else return true; |
43 | | - }; |
44 | | - } |
45 | | - |
46 | | - var count = 0; |
47 | | - var allRecords = event.Records.reduce(function(allRecords, change) { |
48 | | - if (filterer && !filterer(change)) return allRecords; |
49 | | - var id = JSON.stringify(change.dynamodb.Keys); |
50 | | - allRecords[id] = allRecords[id] || []; |
51 | | - allRecords[id].push(change); |
52 | | - count++; |
53 | | - return allRecords; |
54 | | - }, {}); |
55 | | - |
56 | | - if (count === 0) { |
57 | | - console.log('No records replicated'); |
58 | | - return callback(); |
59 | | - } |
60 | | - |
61 | | - var params = { RequestItems: {} }; |
62 | | - params.RequestItems[process.env.ReplicaTable] = Object.keys(allRecords).map(function(key) { |
63 | | - var change = allRecords[key].pop(); |
64 | | - if (change.eventName === 'INSERT' || change.eventName === 'MODIFY') { |
65 | | - return { |
66 | | - PutRequest: { Item: Dyno.deserialize(JSON.stringify(change.dynamodb.NewImage)) } |
67 | | - }; |
68 | | - } else if (change.eventName === 'REMOVE') { |
69 | | - return { |
70 | | - DeleteRequest: { Key: Dyno.deserialize(JSON.stringify(change.dynamodb.Keys)) } |
71 | | - } |
72 | | - } |
73 | | - }); |
74 | | - |
75 | | - (function batchWrite(requestSet, attempts) { |
76 | | - requestSet.forEach(function(req) { |
77 | | - if (req) req.on('retry', function(res) { |
78 | | - if (!res.error || !res.httpResponse || !res.httpResponse.headers) return; |
79 | | - if (res.error.name === 'TimeoutError') res.error.retryable = true; |
80 | | - console.log( |
81 | | - '[failed-request] %s | request-id: %s | crc32: %s | items: %j', |
82 | | - res.error.message, |
83 | | - res.httpResponse.headers['x-amzn-requestid'], |
84 | | - res.httpResponse.headers['x-amz-crc32'], |
85 | | - req.params.RequestItems[process.env.ReplicaTable].map(function(req) { |
86 | | - if (req.DeleteRequest) return req.DeleteRequest.Key; |
87 | | - if (req.PutRequest) return keyAttrs.reduce(function(key, k) { |
88 | | - key[k] = req.PutRequest.Item[k]; |
89 | | - return key; |
90 | | - }, {}); |
91 | | - }) |
92 | | - ); |
93 | | - }); |
94 | | - }); |
95 | | - |
96 | | - requestSet.sendAll(100, function(errs, responses, unprocessed) { |
97 | | - attempts++; |
98 | | - |
99 | | - if (errs) { |
100 | | - var messages = errs |
101 | | - .filter(function(err) { return !!err; }) |
102 | | - .map(function(err) { return err.message; }) |
103 | | - .join(' | '); |
104 | | - console.log('[error] %s', messages); |
105 | | - return callback(errs); |
106 | | - } |
107 | | - |
108 | | - if (unprocessed) { |
109 | | - console.log('[retry] attempt %s contained unprocessed items', attempts); |
110 | | - return setTimeout(batchWrite, Math.pow(2, attempts), unprocessed, attempts); |
111 | | - } |
112 | | - |
113 | | - console.log('Replicated ' + count + ' records'); |
114 | | - callback(); |
115 | | - }); |
116 | | - })(replica.batchWriteItemRequests(params), 0); |
117 | | -} |
118 | | - |
119 | | -function incrementalBackup(event, context, callback) { |
120 | | - var params = { |
121 | | - maxRetries: 1000, |
122 | | - httpOptions: { |
123 | | - timeout: 1000, |
124 | | - agent: module.exports.agent |
125 | | - } |
126 | | - }; |
127 | | - |
128 | | - if (process.env.BackupRegion) params.region = process.env.BackupRegion; |
129 | | - |
130 | | - var s3 = new AWS.S3(params); |
131 | | - |
132 | | - var filterer; |
133 | | - if (process.env.TurnoverRole && process.env.TurnoverAt) { |
134 | | - // Filterer function should return true if the record SHOULD be processed |
135 | | - filterer = function(record) { |
136 | | - var created = Number(record.dynamodb.ApproximateCreationDateTime + '000'); |
137 | | - var turnoverAt = Number(process.env.TurnoverAt); |
138 | | - if (process.env.TurnoverRole === 'BEFORE') return created < turnoverAt; |
139 | | - else if (process.env.TurnoverRole === 'AFTER') return created >= turnoverAt; |
140 | | - else return true; |
141 | | - }; |
142 | | - } |
143 | | - |
144 | | - var count = 0; |
145 | | - var allRecords = event.Records.reduce(function(allRecords, action) { |
146 | | - if (filterer && !filterer(action)) return allRecords; |
147 | | - |
148 | | - var id = JSON.stringify(action.dynamodb.Keys); |
149 | | - |
150 | | - allRecords[id] = allRecords[id] || []; |
151 | | - allRecords[id].push(action); |
152 | | - count++; |
153 | | - return allRecords; |
154 | | - }, {}); |
155 | | - |
156 | | - if (count === 0) { |
157 | | - console.log('No records backed up'); |
158 | | - return callback(); |
159 | | - } |
160 | | - |
161 | | - var q = queue(); |
162 | | - |
163 | | - Object.keys(allRecords).forEach(function(key) { |
164 | | - q.defer(backupRecord, allRecords[key]); |
165 | | - }); |
166 | | - |
167 | | - q.awaitAll(function(err) { |
168 | | - if (err) throw err; |
169 | | - callback(); |
170 | | - }); |
171 | | - |
172 | | - function backupRecord(changes, callback) { |
173 | | - var q = queue(1); |
174 | | - |
175 | | - changes.forEach(function(change) { |
176 | | - q.defer(function(next) { |
177 | | - var id = crypto.createHash('md5') |
178 | | - .update(JSON.stringify(change.dynamodb.Keys)) |
179 | | - .digest('hex'); |
180 | | - |
181 | | - var table = change.eventSourceARN.split('/')[1]; |
182 | | - |
183 | | - var params = { |
184 | | - Bucket: process.env.BackupBucket, |
185 | | - Key: [process.env.BackupPrefix, table, id].join('/') |
186 | | - }; |
187 | | - |
188 | | - var req = change.eventName === 'REMOVE' ? 'deleteObject' : 'putObject'; |
189 | | - if (req === 'putObject') params.Body = JSON.stringify(change.dynamodb.NewImage); |
190 | | - |
191 | | - s3[req](params, function(err) { |
192 | | - if (err) console.log( |
193 | | - '[error] %s | %s s3://%s/%s | %s', |
194 | | - JSON.stringify(change.dynamodb.Keys), |
195 | | - req, params.Bucket, params.Key, |
196 | | - err.message |
197 | | - ); |
198 | | - next(err); |
199 | | - }).on('retry', function(res) { |
200 | | - if (!res.error || !res.httpResponse || !res.httpResponse.headers) return; |
201 | | - if (res.error.name === 'TimeoutError') res.error.retryable = true; |
202 | | - console.log( |
203 | | - '[failed-request] request-id: %s | id-2: %s | %s s3://%s/%s | %s', |
204 | | - res.httpResponse.headers['x-amz-request-id'], |
205 | | - res.httpResponse.headers['x-amz-id-2'], |
206 | | - req, params.Bucket, params.Key, |
207 | | - res.error |
208 | | - ); |
209 | | - }); |
210 | | - }); |
211 | | - }); |
212 | | - |
213 | | - q.awaitAll(function(err) { |
214 | | - if (err) return callback(err); |
215 | | - console.log('Backed up ' + count + ' records') |
216 | | - callback(); |
217 | | - }); |
218 | | - } |
219 | | -} |
| 1 | +module.exports = { |
| 2 | + replicate: require('./lib/replicate'), |
| 3 | + backup: require('./lib/incremental-backup'), |
| 4 | + snapshot: require('./lib/s3-snapshot') |
| 5 | +}; |
0 commit comments