Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stagger Twilio outgoing messages so they aren't all sent at the same time. #72

Open
mark-meyer opened this issue Jun 26, 2018 · 5 comments

Comments

@mark-meyer
Copy link
Member

Twilio limits the number of concurrent requests to 100 and dequeues them at a rate of approximately 1/second. If we need to send out more than 100 reminders on a given day we will need to makes sure we limit the rate we send them. This shouldn't be an issue until courtbot's use increases.

@mfrederickson
Copy link

It might be easiest to send chunks of 100-- we could ORDER the findReminders by hearings.date so the earlier ones get sent first, and tack on a LIMIT 100, then in the runner, loop (with a 120s sleep in between?) until the sendReminders return is empty. This should provide intermittent results to the log file also.

Trickling/slowing down each entry into their queue probably wouldn't be a good idea since you'd have to wait for the whole thing to finish before you could process the results (logging) and it would have to process a lot (theoretically) of pending promises (via the Promises.all... in the sendReminders) instead of a max of 100 or so.

@mark-meyer
Copy link
Member Author

Thanks @mfrederickson, I think this is a good idea. It will probably be quite a while before we are sending more than a hundred reminders for a given day, so this will allow it to run more-or-less as is until that happens.

@whardier
Copy link

whardier commented Aug 12, 2018 via email

@mark-meyer
Copy link
Member Author

Hey @whardier — that's an cool idea. The app is designed to run on Heroku, so I'm not sure that's an option, although my Heroku knowledge isn't very deep.

@mfrederickson I have an implementation of you idea that controls the flow with async/await and recursively calls itself until there are no more requests to send. It works and catches error properly, but it is a little challenging to write a test for. Here the basic idea:

runners/sendReminders.ks

module.exports.timeOutControl = (time) => new Promise(resolve => setTimeout(resolve, time))

async function sendBatch(batch = 0) {
    let reminders = await runnerScript()
    if (reminders.length <= 0) {
        if (batch === 0) {  // only log sending 0 reminders if it's the only batch 
            await runner_log.sent({action: 'send_reminder', data: reminders})
        }
        return 
    }
    await runner_log.sent({action: 'send_reminder', data: reminders})
    // call with module.exports to allow stubbing in tests
    await module.exports.timeOutControl(1000 * 120) 
    await sendBatch(batch + 1)
}

if (require.main === module) {
    sendBatch()
    .then(() => manager.knex.destroy())
    .catch((err) => {
        manager.knex.destroy()
        log.error(err)
    });
}

After a little head scratching I realized you can write decent tests by stubbing messages.send and the timeOutControl function making it immediately resolve so you don't need to depend on timeouts in the test. You can test with something like:

messageStub = sinon.stub(messages, 'send')
timeoutStub = sinon.stub(sendReminderRunner, 'timeOutControl')

it("sends a batch of 100 followed by a batch of 50", function(){
        messageStub.resolves(true)
        timeoutStub.onFirstCall().callsFake(()=>{
            sinon.assert.callCount(messageStub, 100)
            return Promise.resolve()
        })
        timeoutStub.onSecondCall().callsFake(()=>{
            sinon.assert.callCount(messageStub, 150)
            return Promise.resolve()
        })
        return sendReminderRunner.sendBatch()
        .then(() => sinon.assert.callCount(timeoutStub, 2))   
    });

One of the side effects of doing this is that we end up with multiple rows in the logs (one for each batch), but I don't think that's a problem. The log interface summarizes the logs from the day so everything still works.

I can push a branch with this if nobody thinks there's something horrible in this I've overlooked.

@mfrederickson
Copy link

The code looks good to me, though I didn't follow all the test code (it's been a while).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants