Merge pull request #2458 from gradido/semaphore

feat(backend): semaphore to lock transaction table
This commit is contained in:
Ulf Gebhardt 2022-12-22 22:07:16 +01:00 committed by GitHub
commit 1aea1cc924
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 417 additions and 126 deletions

View File

@ -527,7 +527,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 74
min_coverage: 76
token: ${{ github.token }}
##########################################################################

View File

@ -20,6 +20,7 @@
"dependencies": {
"@hyperswarm/dht": "^6.2.0",
"apollo-server-express": "^2.25.2",
"await-semaphore": "^0.1.3",
"axios": "^0.21.1",
"class-validator": "^0.13.1",
"cors": "^2.8.5",

View File

@ -1961,8 +1961,7 @@ describe('ContributionResolver', () => {
})
})
// In the futrue this should not throw anymore
it('throws an error for the second confirmation', async () => {
it('throws no error for the second confirmation', async () => {
const r1 = mutate({
mutation: confirmContribution,
variables: {
@ -1982,8 +1981,7 @@ describe('ContributionResolver', () => {
)
await expect(r2).resolves.toEqual(
expect.objectContaining({
// data: { confirmContribution: true },
errors: [new GraphQLError('Creation was not successful.')],
data: { confirmContribution: true },
}),
)
})

View File

@ -50,6 +50,7 @@ import {
sendContributionConfirmedEmail,
sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
@Resolver()
export class ContributionResolver {
@ -579,8 +580,10 @@ export class ContributionResolver {
clientTimezoneOffset,
)
const receivedCallDate = new Date()
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
const receivedCallDate = new Date()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
@ -590,7 +593,7 @@ export class ContributionResolver {
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.balanceDate', 'DESC')
.orderBy('transaction.id', 'DESC')
.getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
@ -639,10 +642,11 @@ export class ContributionResolver {
})
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`Creation was not successful: ${e}`)
throw new Error(`Creation was not successful.`)
logger.error('Creation was not successful', e)
throw new Error('Creation was not successful.')
} finally {
await queryRunner.release()
releaseLock()
}
const event = new Event()

View File

@ -23,6 +23,11 @@ import { User } from '@entity/User'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
// mock semaphore to allow use fake timers
jest.mock('@/util/TRANSACTIONS_LOCK')
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
let mutate: any, query: any, con: any
let testEnv: any
@ -185,8 +190,7 @@ describe('TransactionLinkResolver', () => {
describe('after one day', () => {
beforeAll(async () => {
jest.useFakeTimers()
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
setTimeout(() => {}, 1000 * 60 * 60 * 24)
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({
mutation: login,

View File

@ -31,6 +31,7 @@ import { calculateDecay } from '@/util/decay'
import { getUserCreation, validateContribution } from './util/creations'
import { executeTransaction } from './TransactionResolver'
import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
// TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => {
@ -165,10 +166,12 @@ export class TransactionLinkResolver {
): Promise<boolean> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const user = getUser(context)
const now = new Date()
if (code.match(/^CL-/)) {
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
logger.info('redeem contribution link...')
const now = new Date()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
@ -273,7 +276,7 @@ export class TransactionLinkResolver {
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: user.id })
.orderBy('transaction.balanceDate', 'DESC')
.orderBy('transaction.id', 'DESC')
.getOne()
let newBalance = new Decimal(0)
@ -309,9 +312,11 @@ export class TransactionLinkResolver {
throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally {
await queryRunner.release()
releaseLock()
}
return true
} else {
const now = new Date()
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
const linkedUser = await DbUser.findOneOrFail(
{ id: transactionLink.userId },
@ -322,6 +327,9 @@ export class TransactionLinkResolver {
throw new Error('Cannot redeem own transaction link.')
}
// TODO: The now check should be done within the semaphore lock,
// since the program might wait a while till it is ready to proceed
// writing the transaction.
if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new Error('Transaction Link is not valid anymore.')
}

View File

@ -368,5 +368,74 @@ describe('send coins', () => {
)
})
})
describe('more transactions to test semaphore', () => {
it('sends the coins four times in a row', async () => {
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 10,
memo: 'first transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 20,
memo: 'second transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 30,
memo: 'third transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 40,
memo: 'fourth transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
})
})
})
})

View File

@ -36,6 +36,8 @@ import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
export const executeTransaction = async (
amount: Decimal,
memo: string,
@ -62,124 +64,133 @@ export const executeTransaction = async (
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
}
// validate amount
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(
sender.id,
amount.mul(-1),
receivedCallDate,
transactionLink,
)
logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) {
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
throw new Error("user hasn't enough GDD or amount is < 0")
}
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
logger.debug(`open Transaction to write...`)
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = sender.id
transactionSend.linkedUserId = recipient.id
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
transactionReceive.linkedUserId = sender.id
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`)
if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`)
transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update(
dbTransactionLink,
{ id: transactionLink.id },
transactionLink,
)
// validate amount
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(
sender.id,
amount.mul(-1),
receivedCallDate,
transactionLink,
)
logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) {
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
throw new Error("user hasn't enough GDD or amount is < 0")
}
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
logger.debug(`open Transaction to write...`)
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = sender.id
transactionSend.linkedUserId = recipient.id
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend)
const eventTransactionSend = new EventTransactionSend()
eventTransactionSend.userId = transactionSend.userId
eventTransactionSend.xUserId = transactionSend.linkedUserId
eventTransactionSend.transactionId = transactionSend.id
eventTransactionSend.amount = transactionSend.amount.mul(-1)
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
const eventTransactionReceive = new EventTransactionReceive()
eventTransactionReceive.userId = transactionReceive.userId
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
eventTransactionReceive.transactionId = transactionReceive.id
eventTransactionReceive.amount = transactionReceive.amount
await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive))
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`Transaction was not successful: ${e}`)
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
logger.debug(`prepare Email for transaction received...`)
await sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
email: recipient.emailContact.email,
language: recipient.language,
senderFirstName: sender.firstName,
senderLastName: sender.lastName,
senderEmail: sender.emailContact.email,
transactionAmount: amount,
})
if (transactionLink) {
await sendTransactionLinkRedeemedEmail({
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.emailContact.email,
language: sender.language,
senderFirstName: recipient.firstName,
senderLastName: recipient.lastName,
senderEmail: recipient.emailContact.email,
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
transactionReceive.linkedUserId = sender.id
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`)
if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`)
transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update(
dbTransactionLink,
{ id: transactionLink.id },
transactionLink,
)
}
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
const eventTransactionSend = new EventTransactionSend()
eventTransactionSend.userId = transactionSend.userId
eventTransactionSend.xUserId = transactionSend.linkedUserId
eventTransactionSend.transactionId = transactionSend.id
eventTransactionSend.amount = transactionSend.amount.mul(-1)
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
const eventTransactionReceive = new EventTransactionReceive()
eventTransactionReceive.userId = transactionReceive.userId
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
eventTransactionReceive.transactionId = transactionReceive.id
eventTransactionReceive.amount = transactionReceive.amount
await eventProtocol.writeEvent(
new Event().setEventTransactionReceive(eventTransactionReceive),
)
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`Transaction was not successful: ${e}`)
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
logger.debug(`prepare Email for transaction received...`)
await sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
email: recipient.emailContact.email,
language: recipient.language,
senderFirstName: sender.firstName,
senderLastName: sender.lastName,
senderEmail: sender.emailContact.email,
transactionAmount: amount,
transactionMemo: memo,
})
if (transactionLink) {
await sendTransactionLinkRedeemedEmail({
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.emailContact.email,
language: sender.language,
senderFirstName: recipient.firstName,
senderLastName: recipient.lastName,
senderEmail: recipient.emailContact.email,
transactionAmount: amount,
transactionMemo: memo,
})
}
logger.info(`finished executeTransaction successfully`)
return true
} finally {
releaseLock()
}
logger.info(`finished executeTransaction successfully`)
return true
}
@Resolver()

View File

@ -0,0 +1,190 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Decimal from 'decimal.js-light'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { logger } from '@test/testSetup'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { creationFactory, nMonthsBefore } from '@/seeds/factory/creation'
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
import {
confirmContribution,
createContribution,
createTransactionLink,
redeemTransactionLink,
login,
createContributionLink,
sendCoins,
} from '@/seeds/graphql/mutations'
let mutate: any, con: any
let testEnv: any
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('semaphore', () => {
let contributionLinkCode = ''
let bobsTransactionLinkCode = ''
let bibisTransactionLinkCode = ''
let bibisOpenContributionId = -1
let bobsOpenContributionId = -1
beforeAll(async () => {
const now = new Date()
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bobBaumeister)
await creationFactory(testEnv, {
email: 'bibi@bloxberg.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: nMonthsBefore(new Date()),
confirmed: true,
})
await creationFactory(testEnv, {
email: 'bob@baumeister.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: nMonthsBefore(new Date()),
confirmed: true,
})
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(200),
name: 'Test Contribution Link',
memo: 'Danke für deine Teilnahme an dem Test der Contribution Links',
cycle: 'ONCE',
validFrom: new Date(2022, 5, 18).toISOString(),
validTo: new Date(now.getFullYear() + 1, 7, 14).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
contributionLinkCode = 'CL-' + contributionLink.code
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bobsLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
email: 'bob@baumeister.de',
amount: 20,
memo: 'Bobs Link',
},
})
const {
data: { createContribution: bobsContribution },
} = await mutate({
mutation: createContribution,
variables: {
creationDate: contributionDateFormatter(new Date()),
amount: 200,
memo: 'Bobs Contribution',
},
})
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bibisLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 20,
memo: 'Bibis Link',
},
})
const {
data: { createContribution: bibisContribution },
} = await mutate({
mutation: createContribution,
variables: {
creationDate: contributionDateFormatter(new Date()),
amount: 200,
memo: 'Bibis Contribution',
},
})
bobsTransactionLinkCode = bobsLink.code
bibisTransactionLinkCode = bibisLink.code
bibisOpenContributionId = bibisContribution.id
bobsOpenContributionId = bobsContribution.id
})
it('creates a lot of transactions without errors', async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const bibiRedeemContributionLink = mutate({
mutation: redeemTransactionLink,
variables: { code: contributionLinkCode },
})
const redeemBobsLink = mutate({
mutation: redeemTransactionLink,
variables: { code: bobsTransactionLinkCode },
})
const bibisTransaction = mutate({
mutation: sendCoins,
variables: { email: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
})
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
const bobRedeemContributionLink = mutate({
mutation: redeemTransactionLink,
variables: { code: contributionLinkCode },
})
const redeemBibisLink = mutate({
mutation: redeemTransactionLink,
variables: { code: bibisTransactionLinkCode },
})
const bobsTransaction = mutate({
mutation: sendCoins,
variables: { email: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
})
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const confirmBibisContribution = mutate({
mutation: confirmContribution,
variables: { id: bibisOpenContributionId },
})
const confirmBobsContribution = mutate({
mutation: confirmContribution,
variables: { id: bobsOpenContributionId },
})
await expect(bibiRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
await expect(redeemBobsLink).resolves.toMatchObject({ errors: undefined })
await expect(bibisTransaction).resolves.toMatchObject({ errors: undefined })
await expect(bobRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
await expect(redeemBibisLink).resolves.toMatchObject({ errors: undefined })
await expect(bobsTransaction).resolves.toMatchObject({ errors: undefined })
await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined })
await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined })
})
})

View File

@ -75,10 +75,7 @@ const run = async () => {
// create GDD
for (let i = 0; i < creations.length; i++) {
const now = new Date().getTime() // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
await creationFactory(seedClient, creations[i])
// eslint-disable-next-line no-empty
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
}
logger.info('##seed## seeding all creations successful...')

View File

@ -0,0 +1,4 @@
import { Semaphore } from 'await-semaphore'
const CONCURRENT_TRANSACTIONS = 1
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)

View File

@ -20,7 +20,7 @@ async function calculateBalance(
time: Date,
transactionLink?: dbTransactionLink | null,
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } })
if (!lastTransaction) return null
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)

View File

@ -1643,6 +1643,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
await-semaphore@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
axios@^0.21.1:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"