Skip to content

Commit 4d817ff

Browse files
authored
Test transaction lock (#4)
- Transaction lock test - Feather database 1.0.0-beta.2 update - Readme updates
1 parent 8b0dc4e commit 4d817ff

5 files changed

Lines changed: 172 additions & 22 deletions

File tree

Package.resolved

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ let package = Package(
3737
dependencies: [
3838
.package(url: "https://github.com/apple/swift-log", from: "1.6.0"),
3939
.package(url: "https://github.com/vapor/postgres-nio", from: "1.27.0"),
40-
.package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.1"),
40+
.package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.2"),
41+
// [docc-plugin-placeholder]
4142
],
4243
targets: [
4344
.target(

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
Postgres driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package.
44

5-
![Release: 1.0.0-beta.1](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E1-F05138)
5+
[
6+
![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138)
7+
](
8+
https://github.com/feather-framework/feather-postgres-database/releases/tag/1.0.0-beta.2
9+
)
610

711
## Features
812

@@ -33,7 +37,7 @@ Postgres driver implementation for the abstract [Feather Database](https://githu
3337
Add the dependency to your `Package.swift`:
3438

3539
```swift
36-
.package(url: "https://github.com/feather-framework/feather-postgres-database", exact: "1.0.0-beta.1"),
40+
.package(url: "https://github.com/feather-framework/feather-postgres-database", exact: "1.0.0-beta.2"),
3741
```
3842

3943
Then add `FeatherPostgresDatabase` to your target dependencies:
@@ -45,7 +49,11 @@ Then add `FeatherPostgresDatabase` to your target dependencies:
4549

4650
## Usage
4751

48-
![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)
52+
[
53+
![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)
54+
](
55+
https://feather-framework.github.io/feather-postgres-database/documentation/featherpostgresdatabase/
56+
)
4957

5058
API documentation is available at the following link.
5159

@@ -127,7 +135,7 @@ The following database driver implementations are available for use:
127135
- Build: `swift build`
128136
- Test:
129137
- local: `swift test`
130-
- using Docker: `swift docker-test`
138+
- using Docker: `make docker-test`
131139
- Format: `make format`
132140
- Check: `make check`
133141

Sources/FeatherPostgresDatabase/PostgresDatabaseClient.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,10 @@ public struct PostgresDatabaseClient: DatabaseClient {
4747
/// - Throws: A `DatabaseError` if connection handling fails.
4848
/// - Returns: The query result produced by the closure.
4949
@discardableResult
50-
public func connection(
50+
public func connection<T>(
5151
isolation: isolated (any Actor)? = #isolation,
52-
_ closure: (PostgresConnection) async throws ->
53-
sending PostgresQueryResult,
54-
) async throws(DatabaseError) -> sending PostgresQueryResult {
52+
_ closure: (PostgresConnection) async throws -> sending T,
53+
) async throws(DatabaseError) -> sending T {
5554
do {
5655
return try await client.withConnection(closure)
5756
}
@@ -72,12 +71,10 @@ public struct PostgresDatabaseClient: DatabaseClient {
7271
/// - Throws: A `DatabaseError` if the transaction fails.
7372
/// - Returns: The query result produced by the closure.
7473
@discardableResult
75-
public func transaction(
74+
public func transaction<T>(
7675
isolation: isolated (any Actor)? = #isolation,
77-
_ closure: (
78-
(PostgresConnection) async throws -> sending PostgresQueryResult
79-
),
80-
) async throws(DatabaseError) -> sending PostgresQueryResult {
76+
_ closure: ((PostgresConnection) async throws -> sending T),
77+
) async throws(DatabaseError) -> sending T {
8178
do {
8279
return try await client.withTransaction(
8380
logger: logger,

Tests/FeatherPostgresDatabaseTests/FeatherPostgresDatabaseTestSuite.swift

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,150 @@ struct FeatherPostgresDatabaseTestSuite {
745745
}
746746
}
747747

748+
@Test
749+
func concurrentTransactionUpdates() async throws {
750+
try await runUsingTestDatabaseClient { database in
751+
let suffix = randomTableSuffix()
752+
let table = "sessions_\(suffix)"
753+
let sessionID = "session_\(suffix)"
754+
755+
enum TestError: Error {
756+
case missingRow
757+
}
758+
759+
try await database.execute(
760+
query: #"""
761+
DROP TABLE IF EXISTS "\#(unescaped: table)" CASCADE;
762+
"""#
763+
)
764+
try await database.execute(
765+
query: #"""
766+
CREATE TABLE "\#(unescaped: table)" (
767+
"id" TEXT NOT NULL PRIMARY KEY,
768+
"access_token" TEXT NOT NULL,
769+
"access_expires_at" TIMESTAMPTZ NOT NULL,
770+
"refresh_token" TEXT NOT NULL,
771+
"refresh_count" INTEGER NOT NULL DEFAULT 0
772+
);
773+
"""#
774+
)
775+
776+
// set an expired token
777+
try await database.execute(
778+
query: #"""
779+
INSERT INTO "\#(unescaped: table)"
780+
("id", "access_token", "access_expires_at", "refresh_token", "refresh_count")
781+
VALUES
782+
(
783+
\#(sessionID),
784+
'stale',
785+
NOW() - INTERVAL '5 minutes',
786+
'refresh',
787+
0
788+
);
789+
"""#
790+
)
791+
792+
func getValidAccessToken(sessionID: String) async throws -> String {
793+
try await database.transaction { connection in
794+
let result = try await connection.execute(
795+
query: #"""
796+
SELECT
797+
"access_token",
798+
"refresh_count",
799+
"access_expires_at" > NOW() + INTERVAL '60 seconds' AS "is_valid"
800+
FROM "\#(unescaped: table)"
801+
WHERE "id" = \#(sessionID)
802+
FOR UPDATE;
803+
"""#
804+
)
805+
let rows = try await result.collect()
806+
807+
guard let row = rows.first else {
808+
throw TestError.missingRow
809+
}
810+
811+
let isValid = try row.decode(
812+
column: "is_valid",
813+
as: Bool.self
814+
)
815+
if isValid {
816+
// token was valid, must be called X times
817+
return try row.decode(
818+
column: "access_token",
819+
as: String.self
820+
)
821+
}
822+
823+
// refresh, this branch can only be called 1 time
824+
let refreshCount = try row.decode(
825+
column: "refresh_count",
826+
as: Int.self
827+
)
828+
let newRefreshCount = refreshCount + 1
829+
let newToken = "token_\(newRefreshCount)"
830+
831+
try await Task.sleep(for: .milliseconds(40))
832+
833+
_ = try await connection.execute(
834+
query: #"""
835+
UPDATE "\#(unescaped: table)"
836+
SET
837+
"access_token" = \#(newToken),
838+
"access_expires_at" = NOW() + INTERVAL '10 minutes',
839+
"refresh_count" = \#(newRefreshCount)
840+
WHERE "id" = \#(sessionID);
841+
"""#
842+
)
843+
844+
return newToken
845+
}
846+
}
847+
848+
let workerCount = 80
849+
var tokens: [String] = []
850+
try await withThrowingTaskGroup(of: String.self) { group in
851+
for _ in 0..<workerCount {
852+
group.addTask {
853+
try await getValidAccessToken(sessionID: sessionID)
854+
}
855+
}
856+
for try await token in group {
857+
tokens.append(token)
858+
}
859+
}
860+
861+
#expect(Set(tokens).count == 1)
862+
863+
let result =
864+
try await database.execute(
865+
query: #"""
866+
SELECT
867+
"access_token",
868+
"refresh_count",
869+
"access_expires_at" > NOW() AS "is_valid"
870+
FROM "\#(unescaped: table)"
871+
WHERE "id" = \#(sessionID);
872+
"""#
873+
)
874+
.collect()
875+
876+
#expect(result.count == 1)
877+
#expect(
878+
try result[0].decode(column: "refresh_count", as: Int.self)
879+
== 1
880+
)
881+
#expect(
882+
try result[0].decode(column: "access_token", as: String.self)
883+
== "token_1"
884+
)
885+
#expect(
886+
try result[0].decode(column: "is_valid", as: Bool.self)
887+
== true
888+
)
889+
}
890+
}
891+
748892
@Test
749893
func doubleRoundTrip() async throws {
750894
try await runUsingTestDatabaseClient { database in

0 commit comments

Comments
 (0)