Skip to content

Commit

Permalink
Merge pull request #94 from ISibboI/93-add-tests-for-behaviour-of-fai…
Browse files Browse the repository at this point in the history
…ledaborted-transactions

Add tests for behaviour of failed/aborted transactions
  • Loading branch information
ISibboI authored Dec 10, 2023
2 parents 6e20664 + 7e52301 commit 13dda55
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .github/workflows/web-api-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
- name: Run database migrations
run: debugBinary/bin/rvoc-backend apply-migrations

- name: Run internal integration tests
run: debugBinary/bin/rvoc-backend run-internal-integration-tests

- name: Run integration tests
uses: BerniWittmann/background-server-action@v1
with:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE test_can_be_safely_dropped_in_production;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE test_can_be_safely_dropped_in_production (
id SERIAL PRIMARY KEY,
name TEXT
);
5 changes: 5 additions & 0 deletions backend/rvoc-backend/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
},
error::RVocError,
error::RVocResult,
integration_tests::run_internal_integration_tests,
job_queue::{jobs::update_witkionary::run_update_wiktionary, spawn_job_queue_runner},
model::user::password_hash::PasswordHash,
web::run_web_api,
Expand Down Expand Up @@ -52,6 +53,9 @@ enum Cli {
#[arg(short, long)]
password: Option<SecureBytes>,
},

/// Run integration tests that require a database, but use APIs that are not exposed through the web interface.
RunInternalIntegrationTests,
}

#[instrument(skip(configuration))]
Expand All @@ -73,6 +77,7 @@ pub async fn run_cli_command(configuration: &Configuration) -> RVocResult<()> {
Cli::SetPassword { username, password } => {
set_password(username, password, configuration).await?
}
Cli::RunInternalIntegrationTests => run_internal_integration_tests(configuration).await?,
}

Ok(())
Expand Down
21 changes: 21 additions & 0 deletions backend/rvoc-backend/src/database/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ diesel::table! {
}
}

diesel::table! {
/// Representation of the `test_can_be_safely_dropped_in_production` table.
///
/// (Automatically generated by Diesel.)
test_can_be_safely_dropped_in_production (id) {
/// The `id` column of the `test_can_be_safely_dropped_in_production` table.
///
/// Its SQL type is `Int4`.
///
/// (Automatically generated by Diesel.)
id -> Int4,
/// The `name` column of the `test_can_be_safely_dropped_in_production` table.
///
/// Its SQL type is `Nullable<Text>`.
///
/// (Automatically generated by Diesel.)
name -> Nullable<Text>,
}
}

diesel::table! {
/// Representation of the `users` table.
///
Expand Down Expand Up @@ -147,6 +167,7 @@ diesel::allow_tables_to_appear_in_same_query!(
job_queue,
languages,
sessions,
test_can_be_safely_dropped_in_production,
users,
word_types,
words,
Expand Down
149 changes: 149 additions & 0 deletions backend/rvoc-backend/src/integration_tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::time::Duration;

use tokio::time::sleep;
use tracing::{info, instrument};

use crate::configuration::Configuration;
use crate::database::create_async_database_connection_pool;
use crate::error::{RVocError, RVocResult};

#[instrument(err, skip(configuration))]
pub async fn run_internal_integration_tests(configuration: &Configuration) -> RVocResult<()> {
test_aborted_transaction(configuration).await
}

#[instrument(err, skip(configuration))]
async fn test_aborted_transaction(configuration: &Configuration) -> RVocResult<()> {
let database_connection_pool = create_async_database_connection_pool(configuration).await?;

// Set up test table
database_connection_pool
.execute_transaction::<_, RVocError>(
|database_connection| {
Box::pin(async move {
use crate::database::schema::test_can_be_safely_dropped_in_production::dsl::*;
use diesel::ExpressionMethods;
use diesel_async::RunQueryDsl;

diesel::delete(test_can_be_safely_dropped_in_production)
.filter(id.eq_any([1, 2]))
.execute(database_connection)
.await?;
diesel::insert_into(test_can_be_safely_dropped_in_production)
.values([(id.eq(1), name.eq("Tim")), (id.eq(2), name.eq("Tom"))])
.execute(database_connection)
.await?;

Ok(())
})
},
0,
)
.await?;

info!("Test table set up successfully");

// Trigger a serialisation failure
let (first, second) = tokio::join!(
database_connection_pool.execute_transaction::<_, RVocError>(
|database_connection| Box::pin(async move {
use crate::database::schema::test_can_be_safely_dropped_in_production::dsl::*;
use diesel::ExpressionMethods;
use diesel::OptionalExtension;
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;

let tim: Option<String> = test_can_be_safely_dropped_in_production
.select(name)
.filter(name.eq("Tim"))
.first(database_connection)
.await
.optional()?
.unwrap();

sleep(Duration::from_secs(5)).await;

diesel::update(test_can_be_safely_dropped_in_production)
.filter(name.eq("Tom"))
.set(name.eq(tim.unwrap()))
.execute(database_connection)
.await?;

Ok(())
}),
0
),
database_connection_pool.execute_transaction::<_, RVocError>(
|database_connection| Box::pin(async move {
use crate::database::schema::test_can_be_safely_dropped_in_production::dsl::*;
use diesel::ExpressionMethods;
use diesel::OptionalExtension;
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;

let tom: Option<String> = test_can_be_safely_dropped_in_production
.select(name)
.filter(name.eq("Tom"))
.first(database_connection)
.await
.optional()?
.unwrap();

sleep(Duration::from_secs(5)).await;

diesel::update(test_can_be_safely_dropped_in_production)
.filter(name.eq("Tim"))
.set(name.eq(tom.unwrap()))
.execute(database_connection)
.await?;

Ok(())
}),
0
),
);

info!("Serialisation failure should have triggered");
info!("First result: {first:?}");
info!("Second result: {second:?}");
assert!(
matches!(
first,
Err(RVocError::DatabaseTransactionRetryLimitReached { .. })
) || matches!(
second,
Err(RVocError::DatabaseTransactionRetryLimitReached { .. })
)
);

// Ensure that we can still do transactions on the same data
database_connection_pool
.execute_transaction::<_, RVocError>(
|database_connection| {
Box::pin(async move {
use crate::database::schema::test_can_be_safely_dropped_in_production::dsl::*;
use diesel::ExpressionMethods;
use diesel_async::RunQueryDsl;

diesel::update(test_can_be_safely_dropped_in_production)
.filter(id.eq(1))
.set(name.eq("Tim"))
.execute(database_connection)
.await?;
diesel::update(test_can_be_safely_dropped_in_production)
.filter(id.eq(2))
.set(name.eq("Tom"))
.execute(database_connection)
.await?;

Ok(())
})
},
0,
)
.await?;

info!("Success! Transactions still work after serialisation failure");

Ok(())
}
1 change: 1 addition & 0 deletions backend/rvoc-backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod cli;
mod configuration;
mod database;
mod error;
mod integration_tests;
mod job_queue;
mod model;
mod web;
Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
fi
if [ ! -d $PGDATA ]; then
echo 'Initializing postgresql database...'
initdb $PGDATA --auth=trust >/dev/null
initdb $PGDATA --no-locale --encoding=UTF8 --auth=trust >/dev/null
fi
echo "Starting postgres"
pg_ctl start -l $LOG_PATH -o "-c listen_addresses= -c unix_socket_directories=$PGHOST"
Expand Down

0 comments on commit 13dda55

Please sign in to comment.