mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #2458 from gradido/semaphore
feat(backend): semaphore to lock transaction table
This commit is contained in:
commit
1aea1cc924
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -527,7 +527,7 @@ jobs:
|
|||||||
report_name: Coverage Backend
|
report_name: Coverage Backend
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./backend/coverage/lcov.info
|
result_path: ./backend/coverage/lcov.info
|
||||||
min_coverage: 74
|
min_coverage: 76
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hyperswarm/dht": "^6.2.0",
|
"@hyperswarm/dht": "^6.2.0",
|
||||||
"apollo-server-express": "^2.25.2",
|
"apollo-server-express": "^2.25.2",
|
||||||
|
"await-semaphore": "^0.1.3",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@ -1961,8 +1961,7 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// In the futrue this should not throw anymore
|
it('throws no error for the second confirmation', async () => {
|
||||||
it('throws an error for the second confirmation', async () => {
|
|
||||||
const r1 = mutate({
|
const r1 = mutate({
|
||||||
mutation: confirmContribution,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
@ -1982,8 +1981,7 @@ describe('ContributionResolver', () => {
|
|||||||
)
|
)
|
||||||
await expect(r2).resolves.toEqual(
|
await expect(r2).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
// data: { confirmContribution: true },
|
data: { confirmContribution: true },
|
||||||
errors: [new GraphQLError('Creation was not successful.')],
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import {
|
|||||||
sendContributionConfirmedEmail,
|
sendContributionConfirmedEmail,
|
||||||
sendContributionRejectedEmail,
|
sendContributionRejectedEmail,
|
||||||
} from '@/emails/sendEmailVariants'
|
} from '@/emails/sendEmailVariants'
|
||||||
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ContributionResolver {
|
export class ContributionResolver {
|
||||||
@ -579,8 +580,10 @@ export class ContributionResolver {
|
|||||||
clientTimezoneOffset,
|
clientTimezoneOffset,
|
||||||
)
|
)
|
||||||
|
|
||||||
const receivedCallDate = new Date()
|
// acquire lock
|
||||||
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
|
|
||||||
|
const receivedCallDate = new Date()
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||||
@ -590,7 +593,7 @@ export class ContributionResolver {
|
|||||||
.select('transaction')
|
.select('transaction')
|
||||||
.from(DbTransaction, 'transaction')
|
.from(DbTransaction, 'transaction')
|
||||||
.where('transaction.userId = :id', { id: contribution.userId })
|
.where('transaction.userId = :id', { id: contribution.userId })
|
||||||
.orderBy('transaction.balanceDate', 'DESC')
|
.orderBy('transaction.id', 'DESC')
|
||||||
.getOne()
|
.getOne()
|
||||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||||
|
|
||||||
@ -639,10 +642,11 @@ export class ContributionResolver {
|
|||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`Creation was not successful: ${e}`)
|
logger.error('Creation was not successful', e)
|
||||||
throw new Error(`Creation was not successful.`)
|
throw new Error('Creation was not successful.')
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
|
releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = new Event()
|
const event = new Event()
|
||||||
|
|||||||
@ -23,6 +23,11 @@ import { User } from '@entity/User'
|
|||||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { GraphQLError } from 'graphql'
|
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 mutate: any, query: any, con: any
|
||||||
let testEnv: any
|
let testEnv: any
|
||||||
@ -185,8 +190,7 @@ describe('TransactionLinkResolver', () => {
|
|||||||
describe('after one day', () => {
|
describe('after one day', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
||||||
setTimeout(() => {}, 1000 * 60 * 60 * 24)
|
|
||||||
jest.runAllTimers()
|
jest.runAllTimers()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { calculateDecay } from '@/util/decay'
|
|||||||
import { getUserCreation, validateContribution } from './util/creations'
|
import { getUserCreation, validateContribution } from './util/creations'
|
||||||
import { executeTransaction } from './TransactionResolver'
|
import { executeTransaction } from './TransactionResolver'
|
||||||
import QueryLinkResult from '@union/QueryLinkResult'
|
import QueryLinkResult from '@union/QueryLinkResult'
|
||||||
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
// TODO: do not export, test it inside the resolver
|
// TODO: do not export, test it inside the resolver
|
||||||
export const transactionLinkCode = (date: Date): string => {
|
export const transactionLinkCode = (date: Date): string => {
|
||||||
@ -165,10 +166,12 @@ export class TransactionLinkResolver {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
if (code.match(/^CL-/)) {
|
if (code.match(/^CL-/)) {
|
||||||
|
// acquire lock
|
||||||
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
logger.info('redeem contribution link...')
|
logger.info('redeem contribution link...')
|
||||||
|
const now = new Date()
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('REPEATABLE READ')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
@ -273,7 +276,7 @@ export class TransactionLinkResolver {
|
|||||||
.select('transaction')
|
.select('transaction')
|
||||||
.from(DbTransaction, 'transaction')
|
.from(DbTransaction, 'transaction')
|
||||||
.where('transaction.userId = :id', { id: user.id })
|
.where('transaction.userId = :id', { id: user.id })
|
||||||
.orderBy('transaction.balanceDate', 'DESC')
|
.orderBy('transaction.id', 'DESC')
|
||||||
.getOne()
|
.getOne()
|
||||||
let newBalance = new Decimal(0)
|
let newBalance = new Decimal(0)
|
||||||
|
|
||||||
@ -309,9 +312,11 @@ export class TransactionLinkResolver {
|
|||||||
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
|
releaseLock()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
const now = new Date()
|
||||||
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
|
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
|
||||||
const linkedUser = await DbUser.findOneOrFail(
|
const linkedUser = await DbUser.findOneOrFail(
|
||||||
{ id: transactionLink.userId },
|
{ id: transactionLink.userId },
|
||||||
@ -322,6 +327,9 @@ export class TransactionLinkResolver {
|
|||||||
throw new Error('Cannot redeem own transaction link.')
|
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()) {
|
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||||
throw new Error('Transaction Link is not valid anymore.')
|
throw new Error('Transaction Link is not valid anymore.')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -36,6 +36,8 @@ import { BalanceResolver } from './BalanceResolver'
|
|||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
import { findUserByEmail } from './UserResolver'
|
import { findUserByEmail } from './UserResolver'
|
||||||
|
|
||||||
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
memo: string,
|
memo: string,
|
||||||
@ -62,124 +64,133 @@ export const executeTransaction = async (
|
|||||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate amount
|
// acquire lock
|
||||||
const receivedCallDate = new Date()
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
|
||||||
await queryRunner.connect()
|
|
||||||
await queryRunner.startTransaction('REPEATABLE READ')
|
|
||||||
logger.debug(`open Transaction to write...`)
|
|
||||||
try {
|
try {
|
||||||
// transaction
|
// validate amount
|
||||||
const transactionSend = new dbTransaction()
|
const receivedCallDate = new Date()
|
||||||
transactionSend.typeId = TransactionTypeId.SEND
|
const sendBalance = await calculateBalance(
|
||||||
transactionSend.memo = memo
|
sender.id,
|
||||||
transactionSend.userId = sender.id
|
amount.mul(-1),
|
||||||
transactionSend.linkedUserId = recipient.id
|
receivedCallDate,
|
||||||
transactionSend.amount = amount.mul(-1)
|
transactionLink,
|
||||||
transactionSend.balance = sendBalance.balance
|
)
|
||||||
transactionSend.balanceDate = receivedCallDate
|
logger.debug(`calculated Balance=${sendBalance}`)
|
||||||
transactionSend.decay = sendBalance.decay.decay
|
if (!sendBalance) {
|
||||||
transactionSend.decayStart = sendBalance.decay.start
|
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
|
||||||
transactionSend.previous = sendBalance.lastTransactionId
|
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
logger.info(`commit Transaction successful...`)
|
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()
|
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
|
||||||
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()
|
const transactionReceive = new dbTransaction()
|
||||||
eventTransactionReceive.userId = transactionReceive.userId
|
transactionReceive.typeId = TransactionTypeId.RECEIVE
|
||||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
transactionReceive.memo = memo
|
||||||
eventTransactionReceive.transactionId = transactionReceive.id
|
transactionReceive.userId = recipient.id
|
||||||
eventTransactionReceive.amount = transactionReceive.amount
|
transactionReceive.linkedUserId = sender.id
|
||||||
await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive))
|
transactionReceive.amount = amount
|
||||||
} catch (e) {
|
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||||
await queryRunner.rollbackTransaction()
|
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
|
||||||
logger.error(`Transaction was not successful: ${e}`)
|
transactionReceive.balanceDate = receivedCallDate
|
||||||
throw new Error(`Transaction was not successful: ${e}`)
|
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
|
||||||
} finally {
|
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
|
||||||
await queryRunner.release()
|
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
|
||||||
}
|
transactionReceive.linkedTransactionId = transactionSend.id
|
||||||
logger.debug(`prepare Email for transaction received...`)
|
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
|
||||||
await sendTransactionReceivedEmail({
|
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||||
firstName: recipient.firstName,
|
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
|
||||||
lastName: recipient.lastName,
|
|
||||||
email: recipient.emailContact.email,
|
// Save linked transaction id for send
|
||||||
language: recipient.language,
|
transactionSend.linkedTransactionId = transactionReceive.id
|
||||||
senderFirstName: sender.firstName,
|
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||||
senderLastName: sender.lastName,
|
logger.debug(`send Transaction updated: ${transactionSend}`)
|
||||||
senderEmail: sender.emailContact.email,
|
|
||||||
transactionAmount: amount,
|
if (transactionLink) {
|
||||||
})
|
logger.info(`transactionLink: ${transactionLink}`)
|
||||||
if (transactionLink) {
|
transactionLink.redeemedAt = receivedCallDate
|
||||||
await sendTransactionLinkRedeemedEmail({
|
transactionLink.redeemedBy = recipient.id
|
||||||
firstName: sender.firstName,
|
await queryRunner.manager.update(
|
||||||
lastName: sender.lastName,
|
dbTransactionLink,
|
||||||
email: sender.emailContact.email,
|
{ id: transactionLink.id },
|
||||||
language: sender.language,
|
transactionLink,
|
||||||
senderFirstName: recipient.firstName,
|
)
|
||||||
senderLastName: recipient.lastName,
|
}
|
||||||
senderEmail: recipient.emailContact.email,
|
|
||||||
|
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,
|
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()
|
@Resolver()
|
||||||
|
|||||||
190
backend/src/graphql/resolver/semaphore.test.ts
Normal file
190
backend/src/graphql/resolver/semaphore.test.ts
Normal 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 })
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -75,10 +75,7 @@ const run = async () => {
|
|||||||
|
|
||||||
// create GDD
|
// create GDD
|
||||||
for (let i = 0; i < creations.length; i++) {
|
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])
|
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...')
|
logger.info('##seed## seeding all creations successful...')
|
||||||
|
|
||||||
|
|||||||
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Semaphore } from 'await-semaphore'
|
||||||
|
|
||||||
|
const CONCURRENT_TRANSACTIONS = 1
|
||||||
|
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)
|
||||||
@ -20,7 +20,7 @@ async function calculateBalance(
|
|||||||
time: Date,
|
time: Date,
|
||||||
transactionLink?: dbTransactionLink | null,
|
transactionLink?: dbTransactionLink | null,
|
||||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | 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
|
if (!lastTransaction) return null
|
||||||
|
|
||||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||||
|
|||||||
@ -1643,6 +1643,11 @@ asynckit@^0.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
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:
|
axios@^0.21.1:
|
||||||
version "0.21.4"
|
version "0.21.4"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user