-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
8 changed files
with
289 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
const Promise = require("bluebird"); | ||
const _ = require("lodash"); | ||
const {default: PQueue} = require("p-queue"); | ||
const lineReader = require("line-reader"); | ||
const { getAWSSDK } = require("../lib/aws"); | ||
const { getTopicArn } = require("../lib/sns"); | ||
const { Command, flags } = require("@oclif/command"); | ||
const { checkVersion } = require("../lib/version-check"); | ||
require("colors"); | ||
|
||
class SendToSnsCommand extends Command { | ||
async run() { | ||
const { flags } = this.parse(SendToSnsCommand); | ||
const { topicName, region, profile, filePath, concurrency } = flags; | ||
|
||
global.region = region; | ||
global.profile = profile; | ||
|
||
checkVersion(); | ||
|
||
this.log(`finding the topic [${topicName}] in [${region}]`); | ||
const topicArn = await getTopicArn(topicName); | ||
|
||
this.log("sending messages..."); | ||
console.time("execution time"); | ||
await sendMessages(filePath, topicArn, concurrency); | ||
|
||
this.log("all done!"); | ||
console.timeEnd("execution time"); | ||
} | ||
} | ||
|
||
SendToSnsCommand.description = | ||
"Sends each line in the specified file as a message to a SNS topic"; | ||
SendToSnsCommand.flags = { | ||
topicName: flags.string({ | ||
char: "n", | ||
description: "name of the SNS topic, e.g. my-topic-dev", | ||
required: true | ||
}), | ||
region: flags.string({ | ||
char: "r", | ||
description: "AWS region, e.g. us-east-1", | ||
required: true | ||
}), | ||
profile: flags.string({ | ||
char: "p", | ||
description: "AWS CLI profile name", | ||
required: false | ||
}), | ||
filePath: flags.string({ | ||
char: "f", | ||
description: "path to the file", | ||
required: true | ||
}), | ||
concurrency: flags.integer({ | ||
char: "c", | ||
description: "how many concurrent pollers to run", | ||
required: false, | ||
default: 10 | ||
}) | ||
}; | ||
|
||
const sendMessages = (filePath, topicArn, concurrency) => { | ||
const AWS = getAWSSDK(); | ||
const SNS = new AWS.SNS(); | ||
const queue = new PQueue({ concurrency }); | ||
|
||
let processedCount = 0; | ||
|
||
const printProgress = (count, last = false) => { | ||
process.stdout.clearLine(); | ||
process.stdout.cursorTo(0); | ||
process.stdout.write(`sent ${count} messages`); | ||
|
||
if (last) { | ||
process.stdout.write("\n"); | ||
} | ||
}; | ||
|
||
const publish = async (line) => { | ||
try { | ||
await SNS.publish({ | ||
Message: line, | ||
TopicArn: topicArn | ||
}).promise(); | ||
} catch (err) { | ||
console.log(`\n${err.message.bold.bgWhite.red}`); | ||
console.log(line); | ||
} | ||
}; | ||
|
||
const add = (line, last = false) => { | ||
queue.add(() => publish(line)); | ||
processedCount += 1; | ||
printProgress(processedCount, last); | ||
}; | ||
|
||
return new Promise((resolve) => { | ||
lineReader.eachLine(filePath, function(line, last, cb) { | ||
if (_.isEmpty(line)) { | ||
cb(); | ||
} else if (last) { | ||
add(line, true); | ||
queue.onEmpty().then(() => { | ||
cb(); | ||
resolve(); | ||
}); | ||
} else if (processedCount % 100 === 0) { | ||
// to avoid overloading the queue and run of memory, | ||
// also, to avoid throttling as well, | ||
// wait for the queue to empty every after 100 messages | ||
queue.onEmpty().then(() => { | ||
add(line); | ||
cb(); | ||
}); | ||
} else { | ||
add(line); | ||
cb(); | ||
} | ||
}); | ||
}); | ||
}; | ||
|
||
module.exports = SendToSnsCommand; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
const { getAWSSDK } = require("../lib/aws"); | ||
|
||
const getTopicArn = async topicName => { | ||
const AWS = getAWSSDK(); | ||
const SNS = new AWS.SNS(); | ||
const loop = async nextToken => { | ||
const resp = await SNS.listTopics({ | ||
NextToken: nextToken | ||
}).promise(); | ||
|
||
const matchingTopic = resp.Topics.find(x => x.TopicArn.endsWith(":" + topicName)); | ||
if (matchingTopic) { | ||
return matchingTopic.TopicArn; | ||
} | ||
|
||
if (resp.NextToken) { | ||
return await loop(resp.NextToken); | ||
} else { | ||
throw new Error(`cannot find the SNS topic [${topicName}]!`); | ||
} | ||
}; | ||
|
||
return loop(); | ||
}; | ||
|
||
module.exports = { | ||
getTopicArn | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
const _ = require("lodash"); | ||
const {expect, test} = require("@oclif/test"); | ||
const AWS = require("aws-sdk"); | ||
|
||
const mockListTopics = jest.fn(); | ||
AWS.SNS.prototype.listTopics = mockListTopics; | ||
const mockPublish = jest.fn(); | ||
AWS.SNS.prototype.publish = mockPublish; | ||
|
||
const consoleLog = jest.fn(); | ||
console.log = consoleLog; | ||
process.stdout.clearLine = jest.fn(); | ||
process.stdout.cursorTo = jest.fn(); | ||
|
||
beforeEach(() => { | ||
mockListTopics.mockReset(); | ||
mockPublish.mockReset(); | ||
consoleLog.mockReset(); | ||
|
||
mockListTopics.mockReturnValue({ | ||
promise: () => Promise.resolve({ | ||
Topics: [{ TopicArn: "arn:aws:sns:us-east-1:12345:my-topic" }] | ||
}) | ||
}); | ||
}); | ||
|
||
describe("send-to-sns", () => { | ||
describe("when there are no failures", () => { | ||
beforeEach(() => { | ||
givenPublishAlwaysReturns(); | ||
}); | ||
|
||
test | ||
.stdout() | ||
.command(["send-to-sns", "-n", "my-topic", "-r", "us-east-1", "-f", "test/test_sns_input.txt"]) | ||
.it("sends all the file's content to sns", ctx => { | ||
expect(ctx.stdout).to.contain("all done!"); | ||
|
||
// there's a total of 5 messages | ||
expect(mockPublish.mock.calls).to.have.lengthOf(5); | ||
const messages = _ | ||
.flatMap(mockPublish.mock.calls, calls => calls) | ||
.map(x => x.Message); | ||
expect(messages).to.have.lengthOf(5); | ||
_.range(1, 6).forEach(n => { | ||
expect(messages).to.contain(`message ${n}`); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("when there are failures", () => { | ||
beforeEach(() => { | ||
givenPublishFails(new Error("boom!")); | ||
givenPublishAlwaysReturns(); | ||
}); | ||
|
||
test | ||
.stdout() | ||
.command(["send-to-sns", "-n", "my-topic", "-r", "us-east-1", "-f", "test/test_sns_input.txt"]) | ||
.it("reports the failed messages", ctx => { | ||
expect(ctx.stdout).to.contain("all done!"); | ||
|
||
// there's a total of 5 messages | ||
expect(mockPublish.mock.calls).to.have.lengthOf(5); | ||
const messages = _ | ||
.flatMap(mockPublish.mock.calls, calls => calls) | ||
.map(x => x.Message); | ||
expect(messages).to.have.lengthOf(5); | ||
|
||
const logMessages = _.flatMap(consoleLog.mock.calls, call => call).join("\n"); | ||
expect(logMessages).to.contain("boom!"); | ||
}); | ||
}); | ||
}); | ||
|
||
function givenPublishAlwaysReturns() { | ||
mockPublish.mockReturnValue({ | ||
promise: () => Promise.resolve({}) | ||
}); | ||
}; | ||
|
||
function givenPublishFails(error) { | ||
mockPublish.mockReturnValueOnce({ | ||
promise: () => Promise.reject(error) | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
message 1 | ||
message 2 | ||
message 3 | ||
message 4 | ||
|
||
message 5 |