'use strict'
const { AWS, log } = require('./util')
const { sleep, exists } = require('@nuskin/uncle-buck')
const dynamodb = new AWS.DynamoDB.DocumentClient()
const sqs = new AWS.SQS()
const TABLE_COUNTER = 'ConnectionLifeguardCounter'
const TABLE_UUID = 'ConnectionLifeguardUuid'
const QURL = `https://sqs.${process.env.AWS_REGION}.amazonaws.com/${process.env.ACCOUNT_ID}/ConnectionLifeguard`
const { customAlphabet } = require('nanoid')
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 22)
/**
* @typedef {object} approvalResponse
* @property {boolean} approved Is `true` when the request was approved. Else it is `false`.
* @property {number} count Current count of the pool.
* @property {string} uuid The unique identifier created when the request is approved.
*/
/**
* @typedef {object} doneResponse
* @property {number} count the current count after decrementing
*/
/**
* getExpireAt produces epochSeconds equaling now + 2 minutes
* @returns {number} epochSeconds the number of seconds elapsed since 1970-01-01T00:00:00Z
*/
const getExpireAt = () => {
let now = new Date()
return parseInt(now.setMinutes(now.getMinutes() + 2) / 1000)
}
/**
* insertUuid inserts a record into the uuid table.
* @param {string} uuid
* @param {string} pool
*/
const insertUuid = async (uuid, pool) => {
log.debug(`calling insertUuid(${uuid},${pool})`)
const params = {
TableName: TABLE_UUID,
Item: {
uuid,
pool,
expireAt: getExpireAt()
}
}
log.debug({params})
try {
const result = await dynamodb.put(params).promise()
await sender(uuid,pool)
log.debug({result})
} catch (err) {
log.info({err})
}
}
/**
* deleteUuid removes the uuid record from the uuid table.
* @param {string} uuid
*/
const deleteUuid = async (uuid) => {
log.debug(`calling deleteUuid(${uuid})`)
const params = {
TableName: TABLE_UUID,
Key: { uuid },
ReturnValues: 'ALL_OLD'
}
log.debug({params})
try {
const deleteUuidResult = await dynamodb.delete(params).promise()
log.debug({deleteUuidResult})
} catch (err) {
log.info({err})
}
}
/**
* decrement will decrement the counter by 1 for the specified `pool`
* @param {String} pool
* @returns {doneResponse} response contains decremented count.
*/
const decrement = async (pool) => {
log.debug(`calling decrement(${pool})`)
let decrementResult = { count: 0 }
try {
const params = {
TableName: TABLE_COUNTER,
Key: { pool },
UpdateExpression: 'SET #count = #count - :decr',
ConditionExpression: '#count > :MIN',
ExpressionAttributeNames: {'#count' : 'count'},
ExpressionAttributeValues: {
':decr': 1,
':MIN': 0
},
ReturnValues: 'UPDATED_NEW'
}
log.debug({params})
const record = await dynamodb.update(params).promise()
log.debug({record})
if(exists(record, 'Attributes')){
decrementResult.count = record.Attributes.count
}
} catch (error) {
log.info({error})
}
log.debug({decrementResult})
return decrementResult
}
/**
* done is used to notify ConnectionLifeguard that you no longer need a database connection.
* done attempts to remove the uuid from the uuid table, then decrements the pool counter.
* @param {String} pool The name of the pool your request was for.
* @param {String} uuid The uuid that was given to you when your request was approved.
* @returns {doneResponse} response object is returned with decremented count.
*/
const done = async (pool, uuid) => {
log.debug(`calling done(${pool},${uuid})`)
await deleteUuid(uuid)
return await decrement(pool)
}
/**
* create creates a new entry for the record passed in.
* @param {String} pool The record entry to create
* @returns {approvalResponse} response object containing your request data.
*/
const requestApproval = async (pool) => {
log.debug(`calling requestApproval(${pool})`)
let attempts = 0
let result = {
approved: false
}
do {
try {
attempts++
const params = {
TableName: TABLE_COUNTER,
Key: { pool },
UpdateExpression: 'SET #count = :incr + #count',
ConditionExpression: '#count < :MAX',
ExpressionAttributeNames: {'#count' : 'count'},
ExpressionAttributeValues: {
':incr': 1,
':MAX': 100
},
ReturnValues: 'UPDATED_NEW'
}
log.debug({params})
const record = await dynamodb.update(params).promise()
log.debug({record})
if(exists(record, 'Attributes')){
result.count = record.Attributes.count
result.approved = true
result.uuid = nanoid() // also put in ddb
await insertUuid(result.uuid, pool)
}
} catch (err) {
log.error({attempts, err})
await sleep(100)
}
} while (!result.approved && attempts < 10)
log.debug({result})
return result
}
/**
* This places a message in a queue that is delayed by a specified number of seconds.
* Once the message becomes visible it is consumed by a lambda that will remove the uuid
* record and decrement the pool counter.
* @param {string} uuid
* @param {string} pool
*/
const sender = async (uuid, pool) => {
log.info(`calling sender(${uuid},${pool})`)
// SQS message parameters
const params = {
MessageBody: JSON.stringify({uuid,pool}),
QueueUrl: QURL
}
const sqsResponse = await sqs.sendMessage(params).promise()
log.info({sqsResponse})
}
module.exports = {
done,
requestApproval
}