bugfix: retrieve and update ticketbook in the same query (#6101)

* bugfix: retrieve and update ticketbook in the same query

* bump up NS version

* Update Cargo.toml

* remove SKIP LOCKED part of the query

---------

Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com>
This commit is contained in:
Jędrzej Stuczyński
2025-10-15 13:53:07 +01:00
committed by GitHub
parent 6a9a767ab4
commit ca0c9898f0
7 changed files with 95 additions and 90 deletions
Generated
+1 -1
View File
@@ -6597,7 +6597,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.0.9"
version = "4.0.10"
dependencies = [
"ammonia",
"anyhow",
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE ecash_ticketbook SET used_tickets = used_tickets + 1 WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": []
},
"hash": "8c92a413a2853a2508c0e8a17ae8723c400930663c4c76e96dfdc7e8c98501ca"
}
@@ -0,0 +1,65 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE ecash_ticketbook\n SET used_tickets = used_tickets + 1\n WHERE id = (\n SELECT id\n FROM ecash_ticketbook\n WHERE used_tickets < total_tickets\n AND expiration_date >= $1\n AND ticketbook_type = $2\n ORDER BY expiration_date ASC\n LIMIT 1\n FOR UPDATE\n )\n RETURNING *\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "serialization_revision",
"type_info": "Int2"
},
{
"ordinal": 2,
"name": "ticketbook_type",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "ticketbook_data",
"type_info": "Bytea"
},
{
"ordinal": 4,
"name": "expiration_date",
"type_info": "Date"
},
{
"ordinal": 5,
"name": "epoch_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "total_tickets",
"type_info": "Int4"
},
{
"ordinal": 7,
"name": "used_tickets",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Date",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "af5c78ef980e38d81f58f72f21c9cd410f83b8750196e0cf5fa5af23883e76df"
}
@@ -3,7 +3,7 @@
[package]
name = "nym-node-status-api"
version = "4.0.9"
version = "4.0.10"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -1,5 +1,4 @@
use anyhow::{Result, anyhow};
use std::ops::{Deref, DerefMut};
use std::{str::FromStr, time::Duration};
pub(crate) mod models;
@@ -8,9 +7,7 @@ pub(crate) mod queries;
#[cfg(test)]
mod tests;
use sqlx::{
ConnectOptions, PgPool, Postgres, Transaction, migrate::Migrator, postgres::PgConnectOptions,
};
use sqlx::{ConnectOptions, PgPool, Postgres, migrate::Migrator, postgres::PgConnectOptions};
static MIGRATOR: Migrator = sqlx::migrate!("./migrations_pg");
@@ -18,35 +15,6 @@ pub(crate) type DbPool = PgPool;
pub(crate) type DbConnection = sqlx::pool::PoolConnection<Postgres>;
pub(crate) struct StorageTransaction<'a> {
inner: Transaction<'a, Postgres>,
}
impl<'a> StorageTransaction<'a> {
pub(crate) async fn commit(self) -> Result<(), sqlx::Error> {
self.inner.commit().await
}
}
impl<'a> From<Transaction<'a, Postgres>> for StorageTransaction<'a> {
fn from(inner: Transaction<'a, Postgres>) -> Self {
Self { inner }
}
}
impl<'a> Deref for StorageTransaction<'a> {
type Target = Transaction<'a, Postgres>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<'a> DerefMut for StorageTransaction<'a> {
fn deref_mut(&mut self) -> &mut Transaction<'a, Postgres> {
&mut self.inner
}
}
#[derive(Clone)]
pub(crate) struct Storage {
pool: DbPool,
@@ -1,7 +1,7 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::db::{Storage, StorageTransaction};
use crate::db::Storage;
use crate::ticketbook_manager::storage::auxiliary_models::StoredIssuedTicketbook;
use nym_credential_proxy_lib::storage::models::{
RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey,
@@ -10,10 +10,6 @@ use time::Date;
use tracing::error;
impl Storage {
pub(crate) async fn begin_storage_tx(&self) -> Result<StorageTransaction<'_>, sqlx::Error> {
self.pool.begin().await.map(Into::into)
}
pub(crate) async fn available_tickets_of_type(&self, typ: &str) -> Result<i64, sqlx::Error> {
let count = sqlx::query!(
r#"
@@ -217,46 +213,38 @@ impl Storage {
.await?;
Ok(())
}
}
impl<'a> StorageTransaction<'a> {
pub(crate) async fn get_next_unspent_ticketbook(
&mut self,
&self,
ticket_type: String,
deadline: Date,
) -> Result<Option<StoredIssuedTicketbook>, sqlx::Error> {
sqlx::query_as(
sqlx::query_as!(
StoredIssuedTicketbook,
r#"
SELECT *
FROM ecash_ticketbook
WHERE used_tickets + 1 <= total_tickets
AND expiration_date >= $1
AND ticketbook_type = $2
ORDER BY expiration_date ASC
LIMIT 1
UPDATE ecash_ticketbook
SET used_tickets = used_tickets + 1
WHERE id = (
SELECT id
FROM ecash_ticketbook
WHERE used_tickets < total_tickets
AND expiration_date >= $1
AND ticketbook_type = $2
ORDER BY expiration_date ASC
LIMIT 1
FOR UPDATE
)
RETURNING *
"#,
deadline,
ticket_type
)
.bind(deadline)
.bind(ticket_type)
.fetch_optional(&mut ***self)
.fetch_optional(&self.pool)
.await
}
pub(crate) async fn increase_used_ticketbook_tickets(
&mut self,
ticketbook_id: i32,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE ecash_ticketbook SET used_tickets = used_tickets + 1 WHERE id = $1",
ticketbook_id
)
.execute(&mut ***self)
.await?;
Ok(())
}
pub(crate) async fn set_distributed_ticketbook(
&mut self,
&self,
testrun_id: i32,
ticketbook_id: i32,
assigned_index: i32,
@@ -271,7 +259,7 @@ impl<'a> StorageTransaction<'a> {
ticketbook_id,
assigned_index
)
.execute(&mut ***self)
.execute(&self.pool)
.await?;
Ok(())
}
@@ -91,15 +91,14 @@ impl TicketbookManagerStorage {
testrun_id: i32,
) -> anyhow::Result<Option<RetrievedTicketbook>> {
let deadline = ecash_today().ecash_date();
let mut tx = self.storage.begin_storage_tx().await?;
// we don't want ticketbooks with expiration in the past
let Some(raw) = tx
// note: this query updates the spent tickets atomically
let Some(raw) = self
.storage
.get_next_unspent_ticketbook(ticket_type.to_string(), deadline)
.await?
else {
// make sure to finish our tx
tx.commit().await?;
return Ok(None);
};
@@ -110,10 +109,9 @@ impl TicketbookManagerStorage {
)
.map_err(|err| anyhow!("failed to deserialise stored ticketbook: {err}"))?;
tx.set_distributed_ticketbook(testrun_id, raw.id, raw.used_tickets)
self.storage
.set_distributed_ticketbook(testrun_id, raw.id, raw.used_tickets)
.await?;
tx.increase_used_ticketbook_tickets(raw.id).await?;
tx.commit().await?;
deserialised.update_spent_tickets(raw.used_tickets as u64);
Ok(Some(RetrievedTicketbook {