Verified Commit 46ad6fe2 authored by aguestuser's avatar aguestuser

[215] add recycleRequestRepository.classifyMatureRecycleRequests

* find all the requests issued before the start of the grace period, indicating
  channels which should be considered for recycling (b/c their grace
  period has passed)
* make lists of redeemed and unredeemed channel phone numbers, where
  "redeemed" channels have been used since the start of the grace
  period, and thus should not be recycled
* return them to the caller for processing (ie: we will defer actual
  recycling to the `registrar` level)
parent fe248571
const { Op } = require('sequelize')
const moment = require('moment')
const app = require('../../../app')
const phoneNumberRegistrar = require('../../registrar/phoneNumber')
const { loggerOf } = require('../../util')
const { repeatEvery } = require('../../util')
const { mapInvoke } = require('lodash')
const util = require('../../util')
const { map, partition } = require('lodash')
const {
job: { recycleInterval, recycleGracePeriod },
job: { recycleGracePeriod },
} = require('../../config')
const logger = loggerOf('repository.recycleRequest')
// (string) -> Promise<{ recycleRequest: RecycleRequest, wasCreated: boolean }>
const requestToRecycle = phoneNumber =>
app.db.recycleRequest
......@@ -20,38 +15,37 @@ const requestToRecycle = phoneNumber =>
wasCreated,
}))
const processRecycleRequests = async () => {
// admins have a "grace period" of 1 day to use channels before they are recycled
const gracePeriodStart = moment().subtract(recycleGracePeriod, 'ms')
// (Array<string>) -> Promise<any>
const destroyMany = phoneNumbers =>
app.db.recycleRequest.destroy({
where: { phoneNumber: { [Op.in]: phoneNumbers } },
})
const classifyMatureRecycleRequests = async () => {
// channel admins have a 1 day grace period to redeem a channel slated for recycling
// by using it. calculate when that grace period started...
const gracePeriodStart = util.now().subtract(parseInt(recycleGracePeriod), 'ms')
// find all the recycle requests issued over a day ago
const matureRequests = await app.db.recycleRequest.find({
where: {
createdAt: { [Op.lte]: gracePeriodStart },
},
// find all the requests issued before the start of the grace period, indicating
// channels which should be considered for recycling (b/c their grace period has passed)
const matureRequests = await app.db.recycleRequest.findAll({
where: { createdAt: { [Op.lte]: gracePeriodStart } },
})
// find all the channel phone numbers that haven't been used in the last day
const unredeemedChannelPhoneNumbers = mapInvoke(
await app.db.messageCount.find({
where: {
channelPhoneNumber: { [Op.in]: mapInvoke(matureRequests, 'phoneNumber') },
updatedAt: { [Op.lte]: gracePeriodStart },
},
// make lists of redeemed and unredeemed channel phone numbers, where "redeemed" channels
// have been used since the start of the grace period, and thus should not be recycled
const [redeemed, toRecycle] = partition(
await app.db.messageCount.findAll({
where: { channelPhoneNumber: { [Op.in]: map(matureRequests, 'phoneNumber') } },
}),
'channelPhoneNumber',
messageCount => messageCount.updatedAt > gracePeriodStart,
)
// recycle all the phone numbers that haven't been used during the 1-day grace period
await Promise.all(unredeemedChannelPhoneNumbers.map(phoneNumberRegistrar.recycle))
// destroy all mature requests (whose numbers have now either been recycled or redeemed)
return app.db.recycleRequest.destroy({
where: { phoneNumber: { [Op.in]: mapInvoke(matureRequests, 'phoneNumber') } },
})
// pluck the channel phone numbers and return them for processing!
return {
redeemed: map(redeemed, 'channelPhoneNumber'),
toRecycle: map(toRecycle, 'channelPhoneNumber'),
}
}
const launchRecycleJob = () =>
repeatEvery(() => processRecycleRequests.catch(logger.error), recycleInterval)
module.exports = { requestToRecycle, processRecycleRequests, launchRecycleJob }
module.exports = { requestToRecycle, classifyMatureRecycleRequests, destroyMany }
......@@ -4,7 +4,6 @@ const inviteRepository = require('./db/repositories/invite')
const smsSenderRepository = require('./db/repositories/smsSender')
const hotlineMessageRepository = require('./db/repositories/hotlineMessage')
const diagnostics = require('./diagnostics')
const recycleRequestRepository = require('./db/repositories/recycleRequest')
const run = async () => {
logger.log('--- Running startup jobs...')
......@@ -29,10 +28,6 @@ const run = async () => {
inviteRepository.launchInviteDeletionJob()
logger.log('----- Launched data cleaning jobs.')
logger.log('----- Launching job to check for recycleable numbers...')
recycleRequestRepository.launchRecycleJob()
logger.log('----- Launched recycleable numbers job')
logger.log('---- Launching healthcheck job...')
diagnostics.launchHealthcheckJob()
logger.log('---- Launched healthcheck job...')
......
const { concat, take, drop, isEmpty, get } = require('lodash')
const uuidV4 = require('uuid/v4')
const stringHash = require('string-hash')
const moment = require('moment')
const crypto = require('crypto')
const {
crypto: { hashSalt },
......@@ -53,9 +54,11 @@ const sequence = async (asyncFuncs, delay = 0) => {
const batchesOfN = (arr, n) =>
isEmpty(arr) ? [] : concat([take(arr, n)], batchesOfN(drop(arr, n), n))
const nowInMillis = () => new Date().getTime()
const now = () => moment()
const nowTimestamp = () => new Date().toISOString()
const nowInMillis = () => moment().valueOf()
const nowTimestamp = () => moment().toISOString()
/**************** Logging ****************/
......@@ -134,6 +137,7 @@ module.exports = {
loggerOf,
logger,
noop,
now,
nowInMillis,
nowTimestamp,
prettyPrint,
......
import { describe, it, before, after, beforeEach, afterEach } from 'mocha'
import { expect } from 'chai'
import sinon from 'sinon'
import moment from 'moment'
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')
import util from '../../../../app/util'
import { values } from 'lodash'
const {
job: { recycleGracePeriod },
} = require('../../../../app/config')
describe('recycleablePhoneNumber repository', () => {
const phoneNumber = genPhoneNumber()
let db, recycleRequestCount, recycleStub
let db, recycleRequestCount
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: {} })
await app.db.messageCount.destroy({ where: {} })
sinon.restore()
})
after(async () => await app.stop())
......@@ -55,96 +56,86 @@ describe('recycleablePhoneNumber repository', () => {
})
})
// 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)
// })
// })
// })
describe('processing mature recycle requests', () => {
const now = moment().clone()
const gracePeriodStart = now.clone().subtract(recycleGracePeriod, 'ms')
const phoneNumbers = {
redeemed: genPhoneNumber(),
toRecycle: genPhoneNumber(),
pending: genPhoneNumber(),
}
const recycleRequests = {
redeemed: {
phoneNumber: phoneNumbers.redeemed,
// mature (created before start of grace period)
createdAt: gracePeriodStart.clone().subtract(1, 'ms'),
},
toRecycle: {
phoneNumber: phoneNumbers.toRecycle,
// mature (created before start of grace period)
createdAt: gracePeriodStart.clone().subtract(1, 'ms'),
},
pending: {
phoneNumber: phoneNumbers.pending,
// not mature (created after start of grace period)
createdAt: gracePeriodStart.clone().add(1, 'ms'),
},
}
const messageCounts = {
redeemed: {
channelPhoneNumber: phoneNumbers.redeemed,
// used during grace period
updatedAt: gracePeriodStart.clone().add(1, 'ms'),
},
toRecycle: {
channelPhoneNumber: phoneNumbers.toRecycle,
// not used during grace perdiod
updatedAt: gracePeriodStart.clone().subtract(1, 'ms'),
},
pending: {
channelPhoneNumber: phoneNumbers.pending,
// does not matter when last used (b/c not mature), but let's say during grace period
updatedAt: gracePeriodStart.clone().add(1, 'ms'),
},
}
beforeEach(async () => {
sinon.stub(util, 'now').returns(now.clone())
await Promise.all(
values(recycleRequests).map(recycleRequest =>
app.db.recycleRequest.create(recycleRequest).then(() =>
app.db.sequelize.query(`
update "recycleRequests"
set "createdAt" = '${recycleRequest.createdAt.toISOString()}'
where "phoneNumber" = '${recycleRequest.phoneNumber}';
`),
),
),
)
await Promise.all(
values(messageCounts).map(messageCount =>
app.db.messageCount.create(messageCount).then(() =>
app.db.sequelize.query(`
update "messageCounts"
set "updatedAt" = '${messageCount.updatedAt.toISOString()}'
where "channelPhoneNumber" = '${messageCount.channelPhoneNumber}';
`),
),
),
)
})
it('retrieves all mature recycle requests and classifies them as redeemed or toRecycle', async () => {
const res = await recycleRequestRepository.classifyMatureRecycleRequests(values(phoneNumbers))
expect(res).to.eql({
redeemed: [phoneNumbers.redeemed],
toRecycle: [phoneNumbers.toRecycle],
})
})
})
})
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