mirror of
https://github.com/waku-org/nwaku.git
synced 2025-01-12 07:44:57 +00:00
feat(common): added postgress async pool wrapper
This commit is contained in:
parent
1f79375679
commit
d81362b5e0
9
waku/common/postgres.nim
Normal file
9
waku/common/postgres.nim
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import
|
||||||
|
./postgres/common,
|
||||||
|
./postgres/connection,
|
||||||
|
./postgres/asyncpool
|
||||||
|
|
||||||
|
export
|
||||||
|
common,
|
||||||
|
connection,
|
||||||
|
asyncpool
|
176
waku/common/postgres/asyncpool.nim
Normal file
176
waku/common/postgres/asyncpool.nim
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# Simple async pool driver for postgress.
|
||||||
|
# Inspired by: https://github.com/treeform/pg/
|
||||||
|
when (NimMajor, NimMinor) < (1, 4):
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
else:
|
||||||
|
{.push raises: [].}
|
||||||
|
|
||||||
|
import
|
||||||
|
std/sequtils,
|
||||||
|
stew/results,
|
||||||
|
chronicles,
|
||||||
|
chronos
|
||||||
|
import
|
||||||
|
./common,
|
||||||
|
./connection
|
||||||
|
|
||||||
|
logScope:
|
||||||
|
topics = "postgres asyncpool"
|
||||||
|
|
||||||
|
|
||||||
|
## Database connection pool options
|
||||||
|
|
||||||
|
type PgAsyncPoolOptions* = object
|
||||||
|
minConnections: int
|
||||||
|
maxConnections: int
|
||||||
|
|
||||||
|
func init*(T: type PgAsyncPoolOptions, minConnections: Positive = 1, maxConnections: Positive = 5): T =
|
||||||
|
if minConnections > maxConnections:
|
||||||
|
raise newException(Defect, "maxConnections must be greater or equal to minConnections")
|
||||||
|
|
||||||
|
PgAsyncPoolOptions(
|
||||||
|
minConnections: minConnections,
|
||||||
|
maxConnections: maxConnections
|
||||||
|
)
|
||||||
|
|
||||||
|
func minConnections*(options: PgAsyncPoolOptions): int =
|
||||||
|
options.minConnections
|
||||||
|
|
||||||
|
func maxConnections*(options: PgAsyncPoolOptions): int =
|
||||||
|
options.maxConnections
|
||||||
|
|
||||||
|
|
||||||
|
## Database connection pool
|
||||||
|
|
||||||
|
type PgAsyncPoolState {.pure.} = enum
|
||||||
|
Live,
|
||||||
|
Closing,
|
||||||
|
Closed
|
||||||
|
|
||||||
|
type
|
||||||
|
## Database connection pool
|
||||||
|
PgAsyncPool* = ref object
|
||||||
|
connOptions: PgConnOptions
|
||||||
|
poolOptions: PgAsyncPoolOptions
|
||||||
|
|
||||||
|
state: PgAsyncPoolState
|
||||||
|
conns: seq[DbConn]
|
||||||
|
busy: seq[bool]
|
||||||
|
|
||||||
|
func isClosing*(pool: PgAsyncPool): bool =
|
||||||
|
pool.state == PgAsyncPoolState.Closing
|
||||||
|
|
||||||
|
func isLive*(pool: PgAsyncPool): bool =
|
||||||
|
pool.state == PgAsyncPoolState.Live
|
||||||
|
|
||||||
|
func isBusy*(pool: PgAsyncPool): bool =
|
||||||
|
pool.busy.allIt(it)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
proc close*(pool: var PgAsyncPool): Future[PgResult[void]] {.async.} =
|
||||||
|
## Gracefully wait and close all openned connections
|
||||||
|
if pool.state == PgAsyncPoolState.Closing:
|
||||||
|
while true:
|
||||||
|
await sleepAsync(0.milliseconds) # Do not block the async runtime
|
||||||
|
return ok()
|
||||||
|
|
||||||
|
pool.state = PgAsyncPoolState.Closing
|
||||||
|
|
||||||
|
# wait for the connections to be released and close them, without
|
||||||
|
# blocking the async runtime
|
||||||
|
while pool.busy.anyIt(it):
|
||||||
|
await sleepAsync(0.milliseconds)
|
||||||
|
|
||||||
|
for i in 0..<pool.conns.len:
|
||||||
|
if pool.busy[i]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pool.busy[i] = false
|
||||||
|
pool.conns[i].close()
|
||||||
|
|
||||||
|
|
||||||
|
proc forceClose(pool: var PgAsyncPool) =
|
||||||
|
## Close all the connections in the pool.
|
||||||
|
for i in 0..<pool.conns.len:
|
||||||
|
pool.busy[i] = false
|
||||||
|
pool.conns[i].close()
|
||||||
|
|
||||||
|
pool.state = PgAsyncPoolState.Closed
|
||||||
|
|
||||||
|
proc newConnPool*(connOptions: PgConnOptions, poolOptions: PgAsyncPoolOptions): Result[PgAsyncPool, string] =
|
||||||
|
## Create a new connection pool.
|
||||||
|
var pool = PgAsyncPool(
|
||||||
|
connOptions: connOptions,
|
||||||
|
poolOptions: poolOptions,
|
||||||
|
state: PgAsyncPoolState.Live,
|
||||||
|
conns: newSeq[DbConn](poolOptions.minConnections),
|
||||||
|
busy: newSeq[bool](poolOptions.minConnections),
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in 0..<poolOptions.minConnections:
|
||||||
|
let connRes = open(connOptions)
|
||||||
|
|
||||||
|
# Teardown the opened connections if we failed to open all of them
|
||||||
|
if connRes.isErr():
|
||||||
|
pool.forceClose()
|
||||||
|
return err(connRes.error)
|
||||||
|
|
||||||
|
pool.conns[i] = connRes.get()
|
||||||
|
pool.busy[i] = false
|
||||||
|
|
||||||
|
ok(pool)
|
||||||
|
|
||||||
|
|
||||||
|
proc getConn*(pool: var PgAsyncPool): Future[PgResult[DbConn]] {.async.} =
|
||||||
|
## Wait for a free connection or create if max connections limits have not been reached.
|
||||||
|
if not pool.isLive():
|
||||||
|
return err("pool is not live")
|
||||||
|
|
||||||
|
# stablish new connections if we are under the limit
|
||||||
|
if pool.isBusy() and pool.conns.len < pool.poolOptions.maxConnections:
|
||||||
|
let connRes = open(pool.connOptions)
|
||||||
|
if connRes.isOk():
|
||||||
|
let conn = connRes.get()
|
||||||
|
pool.conns.add(conn)
|
||||||
|
pool.busy.add(true)
|
||||||
|
|
||||||
|
return ok(conn)
|
||||||
|
else:
|
||||||
|
warn "failed to stablish a new connection", msg = connRes.error
|
||||||
|
|
||||||
|
# wait for a free connection without blocking the async runtime
|
||||||
|
while pool.isBusy():
|
||||||
|
await sleepAsync(0.milliseconds)
|
||||||
|
|
||||||
|
for index in 0..<pool.conns.len:
|
||||||
|
if pool.busy[index]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pool.busy[index] = true
|
||||||
|
return ok(pool.conns[index])
|
||||||
|
|
||||||
|
proc releaseConn(pool: var PgAsyncPool, conn: DbConn) =
|
||||||
|
## Mark the connection as released.
|
||||||
|
for i in 0..<pool.conns.len:
|
||||||
|
if pool.conns[i] == conn:
|
||||||
|
pool.busy[i] = false
|
||||||
|
|
||||||
|
|
||||||
|
proc query*(pool: var PgAsyncPool, query: SqlQuery, args: seq[string]): Future[PgResult[seq[Row]]] {.async.} =
|
||||||
|
## Runs the SQL query getting results.
|
||||||
|
let conn = ? await pool.getConn()
|
||||||
|
defer: pool.releaseConn(conn)
|
||||||
|
|
||||||
|
return await rows(conn, query, args)
|
||||||
|
|
||||||
|
proc exec*(pool: var PgAsyncPool, query: SqlQuery, args: seq[string]): Future[PgResult[void]] {.async.} =
|
||||||
|
## Runs the SQL query without results.
|
||||||
|
let conn = ? await pool.getConn()
|
||||||
|
defer: pool.releaseConn(conn)
|
||||||
|
|
||||||
|
let res = await rows(conn, query, args)
|
||||||
|
if res.isErr():
|
||||||
|
return err(res.error)
|
||||||
|
|
||||||
|
return ok()
|
9
waku/common/postgres/common.nim
Normal file
9
waku/common/postgres/common.nim
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
when (NimMajor, NimMinor) < (1, 4):
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
else:
|
||||||
|
{.push raises: [].}
|
||||||
|
|
||||||
|
|
||||||
|
import stew/results
|
||||||
|
|
||||||
|
type PgResult*[T] = Result[T, string]
|
106
waku/common/postgres/connection.nim
Normal file
106
waku/common/postgres/connection.nim
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
when (NimMajor, NimMinor) < (1, 4):
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
else:
|
||||||
|
{.push raises: [].}
|
||||||
|
|
||||||
|
import
|
||||||
|
stew/results,
|
||||||
|
chronicles,
|
||||||
|
chronos
|
||||||
|
|
||||||
|
import ./common
|
||||||
|
|
||||||
|
include db_postgres
|
||||||
|
|
||||||
|
|
||||||
|
logScope:
|
||||||
|
topics = "postgres connection"
|
||||||
|
|
||||||
|
|
||||||
|
## Connection options
|
||||||
|
|
||||||
|
type PgConnOptions* = object
|
||||||
|
connection: string
|
||||||
|
user: string
|
||||||
|
password: string
|
||||||
|
database: string
|
||||||
|
|
||||||
|
func init*(T: type PgConnOptions, connection, user, password, database: string): T =
|
||||||
|
PgConnOptions(
|
||||||
|
connection: connection,
|
||||||
|
user: user,
|
||||||
|
password: password,
|
||||||
|
database: database
|
||||||
|
)
|
||||||
|
|
||||||
|
func connection*(options: PgConnOptions): string =
|
||||||
|
options.connection
|
||||||
|
|
||||||
|
func user*(options: PgConnOptions): string =
|
||||||
|
options.user
|
||||||
|
|
||||||
|
func password*(options: PgConnOptions): string =
|
||||||
|
options.password
|
||||||
|
|
||||||
|
func database*(options: PgConnOptions): string =
|
||||||
|
options.database
|
||||||
|
|
||||||
|
|
||||||
|
## Connection management
|
||||||
|
|
||||||
|
proc error(db: DbConn): string =
|
||||||
|
## Extract the error message from the database connection.
|
||||||
|
$pqErrorMessage(db)
|
||||||
|
|
||||||
|
|
||||||
|
proc open*(options: PgConnOptions): common.PgResult[DbConn] =
|
||||||
|
## Open a new connection.
|
||||||
|
let conn = open(
|
||||||
|
options.connection,
|
||||||
|
options.user,
|
||||||
|
options.password,
|
||||||
|
options.database
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn.status != CONNECTION_OK:
|
||||||
|
var reason = conn.error
|
||||||
|
if reason.len > 0:
|
||||||
|
reason = "unknown reason"
|
||||||
|
|
||||||
|
return err("failed to connect to database: " & reason)
|
||||||
|
|
||||||
|
ok(conn)
|
||||||
|
|
||||||
|
|
||||||
|
proc rows*(db: DbConn, query: SqlQuery, args: seq[string]): Future[common.PgResult[seq[Row]]] {.async.} =
|
||||||
|
## Runs the SQL getting results.
|
||||||
|
if db.status != CONNECTION_OK:
|
||||||
|
return err("connection is not ok: " & db.error)
|
||||||
|
|
||||||
|
let success = pqsendQuery(db, dbFormat(query, args))
|
||||||
|
if success != 1:
|
||||||
|
return err(db.error)
|
||||||
|
|
||||||
|
while true:
|
||||||
|
let success = pqconsumeInput(db)
|
||||||
|
if success != 1:
|
||||||
|
return err(db.error)
|
||||||
|
|
||||||
|
if pqisBusy(db) == 1:
|
||||||
|
await sleepAsync(0.milliseconds) # Do not block the async runtime
|
||||||
|
continue
|
||||||
|
|
||||||
|
var pqResult = pqgetResult(db)
|
||||||
|
if pqResult == nil and db.error.len > 0:
|
||||||
|
# Check if its a real error or just end of results
|
||||||
|
return err(db.error)
|
||||||
|
|
||||||
|
var rows: seq[Row]
|
||||||
|
|
||||||
|
var cols = pqnfields(pqResult)
|
||||||
|
var row = newRow(cols)
|
||||||
|
for i in 0'i32..pqNtuples(pqResult) - 1:
|
||||||
|
setRow(pqResult, row, i, cols)
|
||||||
|
rows.add(row)
|
||||||
|
|
||||||
|
pqclear(pqResult)
|
Loading…
x
Reference in New Issue
Block a user