How to handle scarce resource disputes in a procurement system - node.js

In the code below, when you have a purchase order for a product with 1 unit and two users try to buy at the same time, both are able to buy. I'm using transactions in this process and I believed that the transaction idea would handle this case.
I'm using DB postgres and I've already tested using ISOLATION LEVEL for this transaction, but without success.
try {
//I tried this, but doesn't work
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
let order_total = 0;
const products = [];
const sales = [];
for (const pBought of data.products) {
let prod = await trx('tb_product')
.select('tb_product.id', 'tb_product.title', 'tb_product.price', 'tb_product.price_total', 'tb_product.quantity', 'tb_product.id_salesman', 'tb_product.width', 'tb_product.height', 'tb_product_discount.value', 'tb_product_discount.is_deleted')
.leftJoin('tb_product_discount', 'tb_product_discount.id', 'tb_product.id_discount')
.where({ 'tb_product.id': pBought.id_product, 'tb_product.is_active': true, 'tb_product.is_deleted': false });
prod = prod[0];
if (!prod) throw new Error('Produto inexistente!');
let salesman = await trx('tb_salesman')
.select('*')
.where({ 'id': prod.id_salesman, 'is_deleted': false });
salesman = salesman[0];
if (!salesman) throw new Error('Vendedor inexistente!');
if (salesman.id !== prod.id_salesman)
throw new Error('Vendedor inconsistente!');
if (!prod || !salesman)
throw new Error('Não foi possível recuperar as informações!');
if (pBought.quantity <= 0)
throw new Error(`Quantidade informada de ${prod.title} menor que 1!`);
console.log(prod.quantity, pBought.quantity);
if (prod.quantity < pBought.quantity)
throw new Error(`Quantidade de ${prod.title} maior que a disponível!`);
if (prod.price !== pBought.price)
throw new Error(`Preço de produtos inválido, atualize a página!`);
let aux;
if (prod.value && !prod.is_deleted) {
aux = (prod.price - prod.value) * prod.width * prod.height;
order_total += roundToTwo(aux * pBought.quantity);
} else {
aux = prod.price_total;
order_total += roundToTwo(aux * pBought.quantity);
}
sales.push({
id_salesman: salesman.id,
id_product: prod.id,
product_title: prod.title,
quantity: prod.quantity,
unit_price: parseInt(aux.toFixed(0)),
price: pBought.quantity * aux
});
products.push({ id_product: prod.id, price: aux, discount: prod.value, quantity: pBought.quantity, available_quantity: prod.quantity, tax: process.env.PERCENTAGE_TAX });
}
let order = await trx('tb_order').insert({ order_total, id_client: client.client.id_client, id_address: address.address.id, portions: data.portions, method: data.method }).returning('*');
order = order[0];
for (const prod of products) {
if (prod.available_quantity - prod.quantity)
await trx('tb_product').update('quantity', prod.available_quantity - prod.quantity)
.where({ 'id': prod.id_product });
else
await trx('tb_product').update({ 'quantity': 0, 'is_active': false })
.where({ 'id': prod.id_product });
delete prod['available_quantity'];
prod.id_order = order.id;
}
await trx('tb_order_product').insert(products);
const wasPaid = await Payment.execute(sales, { portions: data.portions, method: data.method, card_hash: data.card_hash, document: data.document, name: data.name, total: parseInt((order_total * 100)).toFixed(0) }, client, address, order.id);
if (!wasPaid.success) throw new Error(wasPaid.message);
trx.commit();
return { success: true, order };
} catch (error) {
Message.warning(error);
return { success: false, message: error.message }
}
I'm using Node.js and Knex.
I want know how can I handle this scenario for allow only one user take a product at once.
Using serializable for isolation level I get error, because even non-scarce products cannot be acquired simultaneously, because it locks the table and, if another user tries to buy, any product that is at the same instant, will receive an error.

Related

NodeJs parallel request in a loop too slow

so I have this feature that I am working on that takes an object (Over 10k items) picks an item from this object, sends it to an api, which process it then give a response, then proceeds to the next item.
Currently using the async library, the mapLimit method and it works as aspected.
But my problem is that it’s takes too long to loop through the entire dataset cause of the length.
This feature is suppose to be a continual process, once the entire object is iterated, wait for few seconds then do the same thing again.
I tried forking a child_process for this, broke the objects into chunks and created a process for each chunks till it was completed. This worked well as intended but the memory consumption was massive other processes on the server failed as a result of lack of available memory, even after exiting the process after it was completed.
Please how do I achieve this at a faster rate?
I use this to get the list of wallets.
getListofWallet = async () => {
try {
const USDT = await usdt.query(sql`
SELECT * FROM USDT ORDER BY id DESC;
`);
let counter = 0;
let completion = 0;
async.mapLimit(USDT, 6, async (user) => {
let userDetail = {
email: user.email,
id: user.user_id,
address: user.address
}
try {
await this.getWalletTransaction(userDetail);
completion++;
} catch (TronGridException) {
completion++;
console.log(":: A TronGrid Exception Occured");
console.log(TronGridException);
}
if (USDT.length == completion || USDT.length == (completion-5)) {
setTimeout(() => {
this.getListofWallet();
}, 60000);
console.log('~~~~~~~ Finished Wallet Batch ~~~~~~~~~');
}
}
);
} catch (error) {
console.log(error);
console.log('~~~~~~~Restarting TronWallet File after Crash ~~~~~~~~~');
this.getListofWallet();
}
}
Then I use this to process to data sent and perform the neccessary action.
getWalletTransaction = async (walletDetail) => {
const config = {
headers: {
'TRON-PRO-API-KEY': process.env.api_key,
'Content-Type': 'application/json'
}
};
const getTransactionFromAddress = await axios.get(`https://api.trongrid.io/v1/accounts/${walletDetail.address}/transactions/trc20`, config);
const response = getTransactionFromAddress.data;
const currentTimeInMillisecond = 1642668127000; //1632409548000
response.data.forEach(async (value) => {
if (value.block_timestamp >= currentTimeInMillisecond && value.token_info.address == "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t") {
let doesHashExist = await transactionCollection.query(sql`SELECT * FROM transaction_collection WHERE txhash=${value.transaction_id};`);
if (doesHashExist.length == 0) {
if (walletDetail.address == value.to) {
const checkExistence = await CryptoTransactions2.query(sql`
SELECT * FROM CryptoTransaction2 WHERE txHash=${value.transaction_id};
`);
if (checkExistence.length == 0) {
const xCollection = {
collection: "CryptoTransaction2",
queryObject: {
currency: "USDT",
sender: ObjectID("60358d21ec2b4b33e2fcd62e"),
receiver: ObjectID(walletDetail.id),
amount: parseFloat(tronWeb.fromSun(value.value)),
txHash: value.transaction_id,
description: "New wallet Deposit " + "60358d21ec2b4b33e2fcd62e" + " into " + value.to,
category: "deposit",
createdAt: new Date(),
updatedAt: new Date(),
},
};
await new MongoDbService().createTransaction(xCollection);
//create record inside cryptotransactions db.
await CryptoTransactions2.query(sql`INSERT INTO CryptoTransaction2 (txHash) VALUES (${value.transaction_id})`)
});
}

Get number of requisitions made by the user in a day

I have a POST ENDPOINT in my API where i want to register all work journals a worker made.
export const registerPoint = async (req: Request, res: Response) => {
const user = res.locals.decoded;
const {id} = user;
const ponto = new Ponto;
ponto.datePoint = new Date();
ponto.user_id = id;
//this numPoint should restart every day for that user...
ponto.numPoint = 1;
res.json({ msg: "Register point Route", user, id });
return;
}
how can i control how many times this requisition was made by a worker?
I want to control the variable numPoint, when the user makes this requisition it should increase by 01 and then in the final of the day returns it to 0.
Anyone knows about a solution or about a npm package that can handle this?
EDIT: Im storing all the data with SEQUELIZE + MYSQL.
As a starting point you could use a simple database for storing the data such as https://www.npmjs.com/package/sqlite3 or MySQL.
Running jobs daily you could consider https://www.npmjs.com/package/node-cron , for example having a daily job (outside of an API call function);
var cron = require('node-cron');
cron.schedule('0 0 1 * *', () => {
console.log('running every minute to 1 from 5');
});
From what I understand, you need a logging/audit trail mechanism. Since you are using MySQL you can create a new table with columns like (datetime, user_id, action). Every time the user does any action, it will be logged here. Then you can easily find out the count by aggregation. You won't need to reset any count for any user, if data doesn't exist for a given date then it's count will be 0.
I've made a solution that it worked for what i want.
Here is the solution below:
export const registerPoint = async (req: Request, res: Response) => {
const decodedUser = res.locals.decoded;
const user = await User.findByPk(decodedUser.id);
const TODAY_START = new Date().setHours(0, 0, 0, 0);
const NOW = new Date();
const markedPointsOfDay = await Ponto.findAll({
where: {
datePoint: {
[Op.gt]: TODAY_START,
[Op.lt]: NOW
},
}
});
const markedPointsOfDayByUser = await Ponto.findAll({
where: {
datePoint: {
[Op.gt]: TODAY_START,
[Op.lt]: NOW
},
UserId: (user !== null) ? user.id : decodedUser.id
}
})
if (!markedPointsOfDay || markedPointsOfDay.length === 0) {
const ponto = new Ponto;
ponto.datePoint = new Date();
if (user) {
console.log(user)
ponto.UserId = user.id as number;
console.log(ponto.UserId);
}
if (markedPointsOfDayByUser) {
ponto.numPoint = markedPointsOfDayByUser.length + 1;
}
const newPoint = await ponto.save();
res.json({ msg: "Ponto registrado com sucesso", msg2: "Caiu no IF de quando nao encontrou ponto do DIA", newPoint })
return;
}
if (markedPointsOfDay) {
const ponto = new Ponto;
ponto.datePoint = new Date();
if (user) {
ponto.UserId = user.id as number;
}
if (markedPointsOfDayByUser) {
ponto.numPoint = markedPointsOfDayByUser.length + 1;
}
const newPoint = await ponto.save();
res.json({ msg: "ponto registrado", markedPoint: newPoint, markedPointsOfDayByUser });
return;
}
return;
}

Is the Node.js global object compatible with Discord interactions?

Project Overview: Discord bot that uses discord.js where users can play rock-paper-scissors against each other through a queue system. It also has different ranks and an Elo system. Queues are stored in the global object as queue objects (see createQueue below).
Issue: Every 15 queues or so, someone will attempt to enter the queue and instead of joining the queue with another player already in it, it will put the player in a queue with the same lobby ID, but the other player won't be in it. It is strange and inconsistent behavior I have attributed to the unpredictability of globals, but I am assuming that I don't have the full story so I came here to ask. If you need more context, I am willing to provide it. Also, potential solutions would be appreciated if possible :)
Example Issue - both players have entered a queue with the same lobby ID, but are not in the same queue
manageQueues.js:
const createQueue = async (rank) => {
return {
players: [],
playerIdsIndexed: {},
timeouts: {},
game: {
number: 0,
p1: {
user: {},
choice: '',
score: 0
},
p2: {
user: {},
choice: '',
score: 0
}
},
lobby: {
id: global.lobbyId++,
rank: rank
}
};
};
const addPlayerToQueue = async (player, rank, timeout) => {
// There is no existing rank queue
if (!global[`${rank}Queue`]) {
console.log('No existing rank queue, making one...');
global[`${rank}Queue`] = await createQueue(rank);
}
// Failsafe
if (Object.keys(global[`${rank}Queue`].playerIdsIndexed).length >= 2) { console.log('Failsafe'); return undefined; }
// Player already in queue
if (global[`${rank}Queue`].playerIdsIndexed[player.id]) { console.log('Player already in queue'); return 'in'; }
// Player not in queue & it has room
const { players, timeouts, playerIdsIndexed, lobby: { id } } = global[`${rank}Queue`];
players.push(player);
playerIdsIndexed[player.id] = player;
if (timeout) {
timeouts[player.id] = setTimeout(async () => {
await removePlayerFromQueue(player, rank);
}, timeout);
}
return global[`${rank}Queue`];
};
queue.js (the command interaction):
async execute(interaction) {
const { user, channel, guild } = interaction;
const queueLength = interaction.options.getInteger('queue_length');
const queue = await addPlayerToQueue(user, channel.name, (queueLength ? queueLength : defaultTimeout) * 60 * 1000);
if (!queue) return interaction.reply({ content: 'The bot is currently making the queue.', ephemeral: true });
if (queue === 'in') return interaction.reply({ content: 'You are already in a queue.', ephemeral: true });
const { players, lobby: { rank, id } } = queue;
console.log(`Players in queue now: ${players.length}`);
await interaction.reply({ embeds: [queueEmbed(queue, interaction)] });
console.log(`${user.username} joined Lobby ${id} in ${capitalize(rank)}`);
const rankRole = guild.roles.cache.find(r => r.name === `${capitalize(rank)} Ping`);
if (players.length === 1 && Object.keys(ranks).includes(rank) && rankRole) {
await interaction.channel.send({ content: `<#&${rankRole.id}>` });
}
if (players.length === 2) {
global[`${rank}Queue`] = null;
await game(queue, interaction);
}
return;
}

Transactions With Mongoose and NodeJs, calling an external function

I have some code that have implemented Transaction and it works perfectly, but now I need to call a function inside this code(it has a comment in my code example), both parts have to have success or then nothing have to be done. It is not working properly. How can I integrate the "sendCashback, function" to work perfectly with my Transaction?
This is my code:
var transaction: any
let cashback_sent: any
try{
const transactiontimectrl = Number(process.env.TRANSACTION_TIME_STOP)
let dt = new Date();
dt.setSeconds(dt.getSeconds() - transactiontimectrl);
const timectrl = dt.toISOString();
console.log('teste transaction1')
console.log(timectrl)
const session = await mongoose.startSession();
await session.withTransaction(async () => {
const transactionRes = await Transaction.findOne(
{
from: from,
//to: to,
//value: valuetodebit,
createdAt: {$gt: timectrl}
}).session(session)
if(!transactionRes){
transaction = await Transaction.create([
{
from,
to,
value: value,
division_factor: destinationUserGroup.division_factor,
title: title || null,
description: description || null,
type: type || null,
hasCashback: hasCashback,
realmoney: realmoney,
valuetodebit: valuetodebit
}],{ session })
}else{
throw new Error('Erro, uma transação semelhante foi realizada recentemente')
}
if (!transaction) {
throw new Error('Erro, tente novamente mais tarde')
}
console.log('#sadihjaisvq3')
let fromBalance
//Saldo real
if (realmoney) {
fromBalance = await Balance.findOne({
type: IBalanceType.GENERAL,
user: from
}).session(session)
}
//Saldo cashback
else {
fromBalance = await Balance.findOne({
type: IBalanceType.CASHBACK,
user: from
}).session(session)
}
console.log('****criar balance2')
let toBalance = await Balance.findOne({
type: IBalanceType.GENERAL,
user: to
}).session(session)
console.log('****criar balance1', toBalance)
if (!toBalance) {
console.log('****criar balance')
toBalance = await Balance.create(
{
user: to
})
}
let toLivetPay = await Balance.findOne({
type: IBalanceType.GENERAL,
user: toLivet
}).session(session)
if (!fromBalance || !toBalance || !toLivetPay) throw new Error()
if (!credit) {
fromBalance.value = fromBalance.value + parseFloat(valuetodebit) * -1
}
// fromBalance.value = fromBalance.value + parseFloat(valuetodebit) * -1
toBalance.value = toBalance.value + parseFloat(value)
let valorCash = Number(value) / 0.75
let valorLivet = Number(valorCash) * 0.25
toLivetPay.value = toLivetPay.value + parseFloat(String(valorLivet))
await fromBalance.save()
await toBalance.save()
await toLivetPay.save()
if (hasCashback) {
if (!saque) {
//EXTERNAL FUNCTION---------------------------------------------------------
cashback_sent = await sendCashback(
transaction[0],
body.originUser!,
body.destinationUser!
)
//------------------------------------------------------------------------
}
if (!cashback_sent) {
throw new Error('Erro ao distribuir cashback')
}
console.log('depois do cashback')
}
})
The transaction advanced usage is what you need. To work with async await, checkout this link.
As described in the linked mongoose documentation, you can call session.startTransaction() method to start a transaction, and whenever you wish to abort, call session.abortTransaction() to abort the transaction, after all the operations in the transaction are success, call session.commitTransaction() to commit the transaction.

node.js blocking, non-blocking issue in for statement

How can I save all of the json into my mongoldb?
Strangely, only the first value is stored every time.
It might be blocking/non-blocking issue.
json = {
["name":"Karl","id":"azo0"],
["name":"Robert","id":"bdd10"],
["name":"Joan","id":"difj90"],
["name":"Hallyn","id":"fmak88"],
["name":"Michael","id":"vma91"]
};
for(var i = 0; i < json.length; i++){
id = json[i].id;
name = json[i].name;
var ctx = {"id":id,"name":name};
db.json_db.count(ctx).exec(function(err, count) {
if(count < 1){
var model = new User({
"name":json[i].name,
"id":json[i].id
});
model.save(function(){
console.log("ok"+i);
});
}
});
};
After inserting, all of datas are filled with ["name":"Karl","id":"azo0"]
To check out console.log("ok"+i), it prints always "ok0" not "ok1", "ok2", "ok3"... etc..
How can I prevent this issue?
Incase you're using Async package, this is an best way to solve your problem...
async.eachSeries(json, (item, done) => {
let user = new User(
{
"name":json[i].name,
"id":json[i].id
},
(err, user) => {
if(err){
// handle err
}
return done();
}
);
});
.exec() tells me you're using Mongoose. So your loop can rewritten as:
const json = [
{name: "Karl", id: "azo0"},
{name: "Robert", id: "bdd10"},
{name: "Joan", id: "difj90"},
{name: "Hallyn", id: "fmak88"},
{name: "Michael", id: "vma91"}
];
for (const item of json) {
const count = await db.json_db.count(item).exec()
if (!count) {
await new User(item).save()
}
}
Error handling omitted.
See http://exploringjs.com/es2016-es2017/ch_async-functions.html

Resources