Verified Commit 54cccd4d authored by aguestuser's avatar aguestuser

[215][wip] rename and simplify recycleRequest repository

* rename from `recycleablePhoneNumberRepository` to `recycleRequestRepository`
* only expose one method: `requestToRecyle`, which finds or creates a
  recycle request for a given phone number
parent 6a4725ba
const app = require('../../../app')
// (string) -> Promise<{ recycleRequest: RecycleRequest, wasCreated: boolean }>
const requestToRecycle = phoneNumber =>
app.db.recycleRequest
.findOrCreate({ where: { phoneNumber } })
.then(([recycleRequest, wasCreated]) => ({
recycleRequest,
wasCreated,
}))
/**
* RECYCLING HELPER FUNCTIONS
*/
// const {
// job: { recyclePhoneNumberInterval, recycleGracePeriod },
// } = require('../../config')
//
// // (String) -> Promise
// const dequeue = channelPhoneNumber =>
// app.db.recycleRequest.destroy({ where: { channelPhoneNumber } })
//
// const recyclePhoneNumbers = async () => {
// const recycleablePhoneNumbers = await app.db.recycleRequest.findAll({})
// // get/await recycleablePhoneNumbers => messageCounts
// // find messageCount based on channelPhoneNumber
// // dequeue recycleableNumbers that were used within recycleDelay window
// recycleablePhoneNumbers
// .filter(usedRecently)
// .forEach(async ({ channelPhoneNumber }) => await dequeue(channelPhoneNumber))
//
// // recycle channel if enqueued before recycleDelay window
// recycleablePhoneNumbers.filter(enqueuedAwhileAgo).forEach(async ({ channelPhoneNumber }) => {
// await dequeue(channelPhoneNumber)
// await recycle(channelPhoneNumber)
// })
// }
//
// // (Object) -> boolean
// const enqueuedAwhileAgo = ({ createdAt }) => {
// // difference between now and grace period
// const recycleDelayWindow = moment().subtract(recycleGracePeriod)
// return moment(createdAt).diff(recycleDelayWindow) < 0
// }
//
// // (Object) -> boolean
// const usedRecently = async ({ channelPhoneNumber }) => {
// const channel = await channelRepository.findDeep(channelPhoneNumber)
//
// const lastUsed = moment(channel.messageCount.updatedAt)
// const recycleDelayWindow = moment().subtract(recycleGracePeriod)
// return lastUsed.diff(recycleDelayWindow) > 0
// }
module.exports = { requestToRecycle }
const app = require('../..')
const moment = require('moment')
const { repeatEvery, loggerOf } = require('../../util')
const logger = loggerOf('db|recycleablePhoneNumberRepository')
const {
job: { recyclePhoneNumberInterval, recycleGracePeriod },
} = require('../../config')
const channelRepository = require('./channel')
const { recycle } = require('../../registrar/phoneNumber/recycle')
const findAll = () => app.db.recycleablePhoneNumber.findAll({})
// (String) -> Promise
const enqueue = channelPhoneNumber =>
app.db.recycleablePhoneNumber.create({
channelPhoneNumber,
whenEnqueued: new Date().toISOString(),
})
// (String) -> Promise
const dequeue = channelPhoneNumber =>
app.db.recycleablePhoneNumber.destroy({ where: { channelPhoneNumber } })
// (String) -> Promise
const findByPhoneNumber = channelPhoneNumber =>
app.db.recycleablePhoneNumber.findOne({ where: { channelPhoneNumber } })
// launches job to recycle recycleable numbers
const checkForRecycleablePhoneNumbers = () =>
repeatEvery(() => recyclePhoneNumbers().catch(logger.error), recyclePhoneNumberInterval)
/**
* RECYCLING HELPER FUNCTIONS
*/
const recyclePhoneNumbers = async () => {
const recycleablePhoneNumbers = await app.db.recycleablePhoneNumber.findAll({})
// get/await recycleablePhoneNumbers => messageCounts
// find messageCount based on channelPhoneNumber
// dequeue recycleableNumbers that were used within recycleDelay window
recycleablePhoneNumbers
.filter(usedRecently)
.forEach(async ({ channelPhoneNumber }) => await dequeue(channelPhoneNumber))
// recycle channel if enqueued before recycleDelay window
recycleablePhoneNumbers.filter(enqueuedAwhileAgo).forEach(async ({ channelPhoneNumber }) => {
await dequeue(channelPhoneNumber)
await recycle(channelPhoneNumber)
})
}
// (Object) -> boolean
const enqueuedAwhileAgo = ({ createdAt }) => {
// difference between now and grace period
const recycleDelayWindow = moment().subtract(recycleGracePeriod)
return moment(createdAt).diff(recycleDelayWindow) < 0
}
// (Object) -> boolean
const usedRecently = async ({ channelPhoneNumber }) => {
const channel = await channelRepository.findDeep(channelPhoneNumber)
const lastUsed = moment(channel.messageCount.updatedAt)
const recycleDelayWindow = moment().subtract(recycleGracePeriod)
return lastUsed.diff(recycleDelayWindow) > 0
}
module.exports = { findAll, enqueue, dequeue, findByPhoneNumber, checkForRecycleablePhoneNumbers }
......@@ -4,7 +4,7 @@ const inviteRepository = require('./db/repositories/invite')
const smsSenderRepository = require('./db/repositories/smsSender')
const hotlineMessageRepository = require('./db/repositories/hotlineMessage')
const diagnostics = require('./diagnostics')
const recycleablePhoneNumberRepository = require('./db/repositories/recycleablePhoneNumber')
// const recycleRequestRepository = require('./db/repositories/recycleRequest')
const run = async () => {
logger.log('--- Running startup jobs...')
......@@ -29,9 +29,9 @@ const run = async () => {
inviteRepository.launchInviteDeletionJob()
logger.log('----- Launched data cleaning jobs.')
logger.log('----- Launching job to check for recycleable numbers...')
recycleablePhoneNumberRepository.checkForRecycleablePhoneNumbers()
logger.log('----- Launched recycleable numbers job')
// logger.log('----- Launching job to check for recycleable numbers...')
// recycleRequestRepository.checkForRecycleablePhoneNumbers()
// logger.log('----- Launched recycleable numbers job')
logger.log('---- Launching healthcheck job...')
diagnostics.launchHealthcheckJob()
......
......@@ -2,6 +2,7 @@ const { pick } = require('lodash')
const { statuses } = require('../../db/models/phoneNumber')
const channelRepository = require('../../db/repositories/channel')
const signal = require('../../signal')
const { messagesIn } = require('../../dispatcher/strings/messages')
const { sdMessageOf } = require('../../signal/constants')
const {
twilio: { accountSid, authToken, smsEndpoint },
......@@ -30,20 +31,33 @@ const errorStatus = (error, phoneNumber) => ({
const extractStatus = phoneNumberInstance =>
pick(phoneNumberInstance, ['status', 'phoneNumber', 'twilioSid'])
// (Database, Socket, Channel, String, String) -> Promise<void>
// (Database, Socket, Channel, String, String) -> Promise<Array<string>>
const notifyMembersExcept = async (channel, message, sender) => {
if (channel == null) return
const memberPhoneNumbers = channelRepository.getMemberPhoneNumbersExcept(channel, [sender])
await signal.broadcastMessage(memberPhoneNumbers, sdMessageOf(channel, message))
}
// (DB, Socket, String) -> Promise<void>
// (string) -> Promise<Array<string>>
const notifyMaintainers = async message => {
const adminChannel = await channelRepository.findDeep(supportPhoneNumber)
const adminPhoneNumbers = channelRepository.getAdminPhoneNumbers(adminChannel)
await signal.broadcastMessage(adminPhoneNumbers, sdMessageOf(adminChannel, message))
}
// (string, string) -> Promise<Array<string>>
const notifyAdmins = async (channel, notificationKey) => {
const recipients = await channelRepository.getAdminMemberships(channel)
return Promise.all(
recipients.map(recipient =>
signal.sendMessage(
recipient.memberPhoneNumber,
sdMessageOf(channel, messagesIn(recipient.language).notifications[notificationKey]),
),
),
)
}
// (DB, Socket, ChannelInstance, String) -> Promise<void>
const destroyChannel = async (channel, tx) => {
if (channel == null) return
......@@ -64,6 +78,7 @@ module.exports = {
statuses,
errorStatus,
extractStatus,
notifyAdmins,
notifyMaintainers,
notifyMembersExcept,
destroyChannel,
......
import { describe, it, before, after, beforeEach, afterEach } from 'mocha'
import { expect } from 'chai'
import sinon from 'sinon'
import { genPhoneNumber } from '../../../support/factories/phoneNumber'
import app from '../../../../app'
import testApp from '../../../support/testApp'
import dbService from '../../../../app/db'
import recycleService from '../../../../app/registrar/phoneNumber/recycle'
import recycleRequestRepository from '../../../../app/db/repositories/recycleRequest'
// const {
// job: { recyclePhoneNumberInterval, recycleGracePeriod },
// } = require('../../../../app/config')
describe('recycleablePhoneNumber repository', () => {
const phoneNumber = genPhoneNumber()
let db, recycleRequestCount, recycleStub
before(async () => (db = (await app.run({ ...testApp, db: dbService })).db))
beforeEach(
async () => (recycleStub = sinon.stub(recycleService, 'recycle').returns(Promise.resolve())),
)
afterEach(async () => {
await app.db.recycleRequest.destroy({ where: {} })
sinon.restore()
})
after(async () => await app.stop())
describe('issuing a recycle request', () => {
describe('if recycle request does not yet exist', () => {
beforeEach(async () => (recycleRequestCount = await db.recycleRequest.count()))
it('creates new request and flags that it was created', async () => {
const { recycleRequest, wasCreated } = await recycleRequestRepository.requestToRecycle(
phoneNumber,
)
expect(wasCreated).to.eql(true)
expect(recycleRequest.phoneNumber).to.eql(phoneNumber)
expect(await app.db.recycleRequest.count()).to.eql(recycleRequestCount + 1)
})
})
describe('if recycle request already exists for the phoneNumber', () => {
beforeEach(async () => {
await app.db.recycleRequest.create({ phoneNumber })
recycleRequestCount = await db.recycleRequest.count()
})
it('does not create new request but returns existing request', async () => {
const { recycleRequest, wasCreated } = await recycleRequestRepository.requestToRecycle(
phoneNumber,
)
expect(wasCreated).to.eql(false)
expect(recycleRequest.phoneNumber).to.eql(phoneNumber)
expect(await app.db.recycleRequest.count()).to.eql(recycleRequestCount)
})
})
})
// xdescribe('recyclePhoneNumbers', () => {
// const now = moment()
// const reclaimedPhoneNumber = genPhoneNumber()
// const recycledPhoneNumber = genPhoneNumber()
// const pendingPhoneNumber = genPhoneNumber()
//
// const reclaimedRecycleablePhoneNumber = {
// phoneNumber: reclaimedPhoneNumber,
// whenEnqueued: now.toISOString(),
// createdAt: now.subtract(recycleGracePeriod / 2, 'millis').toISOString(),
// }
// const recycledRecycleablePhoneNumber = {
// phoneNumber: genPhoneNumber(),
// whenEnqueued: now.toISOString(),
// createdAt: now.subtract(recycleGracePeriod + 1, 'millis').toISOString(),
// }
// const pendingRecycleablePhoneNumber = {
// phoneNumber: genPhoneNumber(),
// whenEnqueued: now.toISOString(),
// createdAt: now.subtract(recycleGracePeriod / 2, 'millis').toISOString(),
// }
//
// const reclaimedMessageCount = messageCountFactory({
// phoneNumber: reclaimedPhoneNumber,
// updatedAt: now.toISOString(),
// })
// const recycledMessageCount = messageCountFactory({
// phoneNumber: recycledPhoneNumber,
// updatedAt: now.subtract(recycleGracePeriod + 2, 'millis'),
// })
// const pendingMessageCount = messageCountFactory({
// phoneNumber: pendingPhoneNumber,
// updatedAt: now.subtract(recycleGracePeriod + 2, 'millis'),
// })
//
// beforeEach(async () => {
// ;[
// reclaimedRecycleablePhoneNumber,
// recycledRecycleablePhoneNumber,
// pendingRecycleablePhoneNumber,
// ].map(x => db.recycleablePhoneNumber.create(x))
// ;[reclaimedMessageCount, recycledMessageCount, pendingMessageCount].map(x =>
// db.messageCount.create(x),
// )
// })
//
// describe('a recycleablePhoneNumber that has been used after being enqueued', () => {
// it('is dequeued', async () => {
// expect(
// await db.recycleablePhoneNumber.findOne({
// where: {
// phoneNumber: reclaimedPhoneNumber,
// },
// }),
// ).to.eql(null)
// })
// it('is not recycled', () => {
// expect(recycleStub.callCount).to.eql(0)
// })
// })
//
// describe('a recycleablePhoneNumbers whose grace period has expired', () => {
// it('is dequeued', async () => {
// expect(
// (await db.recycleablePhoneNumber.findOne({
// where: {
// phoneNumber: recycledRecycleablePhoneNumber,
// },
// })).phoneNumber,
// ).to.eql(null)
// })
// it('it is recycled', () => {
// expect(recycleStub.callCount).to.eql(1)
// expect(recycleStub.getCall(0).args).to.eql([recycledRecycleablePhoneNumber])
// })
// })
//
// describe('a recycleablePhoneNumber that has not been used and whose grace period has expired', () => {
// it('is not dequeued', async () => {
// expect(
// (await db.recycleablePhoneNumber.findOne({
// where: {
// phoneNumber: pendingPhoneNumber,
// },
// })).phoneNumber,
// ).to.eql(pendingPhoneNumber)
// })
// it('is not recycled', () => {
// expect(recycleStub.callCount).to.eql(0)
// })
// })
// })
})
import { describe, it, before, after, beforeEach } from 'mocha'
import { expect } from 'chai'
import sinon from 'sinon'
import { genPhoneNumber } from '../../../support/factories/phoneNumber'
import recycleablePhoneNumberRepository from '../../../../app/db/repositories/recycleablePhoneNumber'
import app from '../../../../app'
import testApp from '../../../support/testApp'
import dbService from '../../../../app/db'
import recycleService from '../../../../app/registrar/phoneNumber/recycle'
import { messageCountFactory } from '../../../support/factories/messageCount'
import moment from 'moment'
const {
job: { recyclePhoneNumberInterval, recycleGracePeriod },
} = require('../../../../app/config')
describe('recycleablePhoneNumber repository', () => {
let db, recycleablePhoneNumberCount, channelPhoneNumber, recycleStub
before(async () => {
db = (await app.run({ ...testApp, db: dbService })).db
channelPhoneNumber = genPhoneNumber()
})
beforeEach(async () => {
recycleablePhoneNumberCount = await db.recycleablePhoneNumber.count()
recycleStub = sinon.stub(recycleService, 'recycle').returns(Promise.resolve())
})
after(async () => await app.stop())
it('enqueues a channel for recycling', async () => {
let enqueuedChannel = await recycleablePhoneNumberRepository.enqueue(channelPhoneNumber)
expect(enqueuedChannel.channelPhoneNumber).to.eql(channelPhoneNumber)
expect(await db.recycleablePhoneNumber.count()).to.eql(recycleablePhoneNumberCount + 1)
})
it('dequeues a channel for recycling', async () => {
await recycleablePhoneNumberRepository.dequeue(channelPhoneNumber)
expect(await db.recycleablePhoneNumber.count()).to.eql(recycleablePhoneNumberCount - 1)
})
describe('recyclePhoneNumbers', () => {
const now = moment()
const reclaimedPhoneNumber = genPhoneNumber()
const recycledPhoneNumber = genPhoneNumber()
const pendingPhoneNumber = genPhoneNumber()
const reclaimedRecycleablePhoneNumber = {
channelPhoneNumber: reclaimedPhoneNumber,
whenEnqueued: now.toISOString(),
createdAt: now.subtract(recycleGracePeriod / 2, 'millis').toISOString(),
}
const recycledRecycleablePhoneNumber = {
channelPhoneNumber: genPhoneNumber(),
whenEnqueued: now.toISOString(),
createdAt: now.subtract(recycleGracePeriod + 1, 'millis').toISOString(),
}
const pendingRecycleablePhoneNumber = {
channelPhoneNumber: genPhoneNumber(),
whenEnqueued: now.toISOString(),
createdAt: now.subtract(recycleGracePeriod / 2, 'millis').toISOString(),
}
const reclaimedMessageCount = messageCountFactory({
channelPhoneNumber: reclaimedPhoneNumber,
updatedAt: now.toISOString(),
})
const recycledMessageCount = messageCountFactory({
channelPhoneNumber: recycledPhoneNumber,
updatedAt: now.subtract(recycleGracePeriod + 2, 'millis'),
})
const pendingMessageCount = messageCountFactory({
channelPhoneNumber: pendingPhoneNumber,
updatedAt: now.subtract(recycleGracePeriod + 2, 'millis'),
})
beforeEach(async () => {
;[
reclaimedRecycleablePhoneNumber,
recycledRecycleablePhoneNumber,
pendingRecycleablePhoneNumber,
].map(x => db.recycleablePhoneNumber.create(x))
;[reclaimedMessageCount, recycledMessageCount, pendingMessageCount].map(x =>
db.messageCount.create(x),
)
})
describe('a recycleablePhoneNumber that has been used after being enqueued', () => {
it('is dequeued', async () => {
expect(
await db.recycleablePhoneNumber.findOne({
where: {
channelPhoneNumber: reclaimedPhoneNumber,
},
}),
).to.eql(null)
})
it('is not recycled', () => {
expect(recycleStub.callCount).to.eql(0)
})
})
describe('a recycleablePhoneNumbers whose grace period has expired', () => {
it('is dequeued', async () => {
expect(
(await db.recycleablePhoneNumber.findOne({
where: {
channelPhoneNumber: recycledRecycleablePhoneNumber,
},
})).channelPhoneNumber,
).to.eql(null)
})
it('it is recycled', () => {
expect(recycleStub.callCount).to.eql(1)
expect(recycleStub.getCall(0).args).to.eql([recycledRecycleablePhoneNumber])
})
})
describe('a recycleablePhoneNumber that has not been used and whose grace period has expired', () => {
it('is not dequeued', async () => {
expect(
(await db.recycleablePhoneNumber.findOne({
where: {
channelPhoneNumber: pendingPhoneNumber,
},
})).channelPhoneNumber,
).to.eql(pendingPhoneNumber)
})
it('is not recycled', () => {
expect(recycleStub.callCount).to.eql(0)
})
})
})
})
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment