Skip to content

Commit 04b0e96

Browse files
committed
implement database connection pools
Signed-off-by: George Lemon <georgelemon@protonmail.com>
1 parent 6f040bc commit 04b0e96

File tree

2 files changed

+117
-33
lines changed

2 files changed

+117
-33
lines changed

src/ozark/database.nim

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Made by Humans from OpenPeeps
55
# https://github.com/openpeeps/ozark
66

7-
import std/[macros, net, strutils, tables]
7+
import std/[macros, net, strutils, tables, locks, os]
88

99
import pkg/threading/once
1010
import pkg/db_connector/db_postgres
@@ -14,28 +14,27 @@ export Port, strVal, `%`
1414

1515
type
1616
DBConnectionPool* = ref object
17-
connections*: seq[DBConn]
18-
busyConnections*: seq[DBConn]
17+
connections*: seq[DBConn] # available
18+
busyConnections*: seq[DBConn] # checked-out
19+
maxSize*: int
20+
lock: Lock
1921

2022
DBDriver* = enum
2123
PostgreSQLDriver
2224
MYSQLDriver
2325
SQLiteDriver
2426

25-
DBConnection = ref object
26-
driver: DBDriver
27+
DBConnection* = ref object
28+
driver*: DBDriver
2729
address*, name*, user*, password*: string
28-
# dbConn: DBConn
29-
# pool: DBConnectionPool
30-
port: Port
30+
port*: Port
3131

32-
DBConnections = OrderedTableRef[string, DBConnection]
32+
DBConnections* = OrderedTableRef[string, DBConnection]
3333

3434
Ozark = object
3535
dbs: DBConnections
36-
# holds credentials for multiple Database Connections
3736
maindb: DBConnection
38-
# credentials for the main database connection
37+
mainPool: DBConnectionPool
3938

4039
var
4140
DB: ptr Ozark
@@ -45,13 +44,12 @@ proc getInstance*(): ptr Ozark =
4544
## Get the singleton instance of the database manager
4645
once(o):
4746
DB = createShared(Ozark)
47+
DB[].dbs = newOrderedTable[string, DBConnection]() # init map
4848
result = DB
4949

5050
proc initOzarkDatabase*(address, name, user, password: string,
5151
port: Port = Port(5432),
5252
driver: DBDriver = DBDriver.PostgreSQLDriver) =
53-
## Initializes the singleton instance of the database manager
54-
## using provided credentials as main database
5553
let db = getInstance()
5654
db[].maindb = DBConnection(
5755
address: address,
@@ -102,5 +100,93 @@ macro withDatabase*(id: static string, body: untyped) =
102100
db[id].password, db[$id].name)
103101
defer:
104102
dbcon.close()
103+
block:
104+
`body`
105+
106+
107+
proc openConn(cfg: DBConnection): DBConn =
108+
case cfg.driver
109+
of PostgreSQLDriver:
110+
open(cfg.address, cfg.user, cfg.password, cfg.name)
111+
else:
112+
raise newException(ValueError, "Only PostgreSQL driver pool is currently implemented.")
113+
114+
proc initOzarkPool*(size: Positive = 10) =
115+
## Initialize main DB connection pool.
116+
let db = getInstance()
117+
assert db[].maindb != nil, "Main DB credentials not initialized. Call initOzarkDatabase first."
118+
119+
var pool = DBConnectionPool(
120+
maxSize: size.int,
121+
connections: @[],
122+
busyConnections: @[]
123+
)
124+
initLock(pool.lock)
125+
126+
for _ in 0..<size.int:
127+
pool.connections.add(openConn(db[].maindb))
128+
129+
db[].mainPool = pool
130+
131+
proc closeOzarkPool*() =
132+
## Close all pooled connections.
133+
let db = getInstance()
134+
if db[].mainPool.isNil: return
135+
136+
acquire(db[].mainPool.lock)
137+
defer: release(db[].mainPool.lock)
138+
139+
for c in db[].mainPool.connections:
140+
c.close()
141+
for c in db[].mainPool.busyConnections:
142+
c.close()
143+
144+
db[].mainPool.connections.setLen(0)
145+
db[].mainPool.busyConnections.setLen(0)
146+
147+
proc acquireConn*(pool: DBConnectionPool, timeoutMs: int = 5000): DBConn =
148+
## Borrow one connection from pool, waiting up to timeoutMs.
149+
let stepMs = 10
150+
var waited = 0
151+
while waited <= timeoutMs:
152+
acquire(pool.lock)
153+
if pool.connections.len > 0:
154+
result = pool.connections.pop()
155+
pool.busyConnections.add(result)
156+
release(pool.lock)
157+
return
158+
release(pool.lock)
159+
sleep(stepMs)
160+
inc(waited, stepMs)
161+
162+
raise newException(ValueError, "Timed out waiting for a DB connection from pool.")
163+
164+
proc releaseConn*(pool: DBConnectionPool, conn: DBConn) =
165+
## Return a connection to pool.
166+
acquire(pool.lock)
167+
defer: release(pool.lock)
168+
169+
var idx = -1
170+
for i, c in pool.busyConnections:
171+
if c == conn:
172+
idx = i
173+
break
174+
175+
if idx >= 0:
176+
pool.busyConnections.del(idx)
177+
pool.connections.add(conn)
178+
179+
macro withDBPool*(body: untyped) =
180+
## Run queries using a pooled connection.
181+
result = newStmtList()
182+
add result, quote do:
183+
let db = getInstance()
184+
assert db != nil, "Database manager not initialized. Call initOzarkDatabase first."
185+
assert db[].mainPool != nil, "DB pool not initialized. Call initOzarkPool first."
186+
187+
let dbcon {.inject.} = acquireConn(db[].mainPool)
188+
defer:
189+
releaseConn(db[].mainPool, dbcon)
190+
105191
block:
106192
`body`

tests/test2.nim

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,15 @@ test "init embedded postgres and create tables":
2929

3030
initOzarkDatabase("localhost", "postgres", "postgres", "postgres", Port(5432))
3131
withDB do:
32-
Models.rawSQL("""
33-
CREATE TABLE IF NOT EXISTS users (
34-
id SERIAL PRIMARY KEY,
35-
username VARCHAR(50),
36-
name VARCHAR(100),
37-
email VARCHAR(100)
38-
)""").exec()
32+
Models.table(Users).prepareTable().exec()
33+
Models.table(Users).dropTable(cascade = true).exec()
34+
Models.table(Users).prepareTable().exec()
35+
36+
initOzarkPool(15)
3937

4038
suite "INSERT and SELECT queries":
4139
test "insert and select data":
42-
withDB do:
40+
withDBPool do:
4341
var name = "John"
4442
let id = Models.table(Users).insert({
4543
name: name,
@@ -58,7 +56,7 @@ suite "INSERT and SELECT queries":
5856
check res.entries[0].email == "test@example.com"
5957

6058
test "select specific columns":
61-
withDB do:
59+
withDBPool do:
6260
let res = Models.table(Users)
6361
.select(["name", "email"])
6462
.where("name", "John").get()
@@ -68,22 +66,22 @@ suite "INSERT and SELECT queries":
6866

6967
suite "WHERE queries":
7068
test "where query":
71-
withDB do:
69+
withDBPool do:
7270
let res = Models.table(Users)
7371
.select("name")
7472
.where("name", "John").get()
7573
check res.isEmpty == false
7674
# check res.get(0).name == "John"
7775

7876
test "whereNot query":
79-
withDB do:
77+
withDBPool do:
8078
let res = Models.table(Users)
8179
.select("name")
8280
.whereNot("name", "John").get()
8381
check res.isEmpty
8482

8583
test "orWhere query":
86-
withDB do:
84+
withDBPool do:
8785
let res = Models.table(Users)
8886
.select("name")
8987
.where("name", "Ghost")
@@ -92,7 +90,7 @@ suite "WHERE queries":
9290
check res.get(0).name == "John"
9391

9492
test "orWhereNot query":
95-
withDB do:
93+
withDBPool do:
9694
let res = Models.table(Users)
9795
.select("name")
9896
.whereNot("name", "John")
@@ -103,51 +101,51 @@ suite "WHERE queries":
103101

104102
suite "LIKE queries":
105103
test "like query":
106-
withDB do:
104+
withDBPool do:
107105
let res = Models.table(Users).select("name")
108106
.whereLike("name", "Jo").get()
109107
check res.isEmpty == false
110108
check res.get(0).name == "John"
111109

112110
test "whereStartsLike query":
113-
withDB do:
111+
withDBPool do:
114112
let res = Models.table(Users).select("name")
115113
.whereStartsLike("name", "Jo").get()
116114
check res.isEmpty == false
117115
check res.get(0).name == "John"
118116

119117

120118
test "whereEndsLike query":
121-
withDB do:
119+
withDBPool do:
122120
let res = Models.table(Users).select("name")
123121
.whereEndsLike("name", "hn").get()
124122
check res.isEmpty == false
125123
check res.get(0).name == "John"
126124

127125
test "whereNotLike query":
128-
withDB do:
126+
withDBPool do:
129127
let res = Models.table(Users).select("name")
130128
.whereNot("name", "Ghost").get()
131129
check res.isEmpty == false
132130
check res.get(0).name == "John"
133131

134132
test "wereNotStartsLike query":
135-
withDB do:
133+
withDBPool do:
136134
let res = Models.table(Users).select("name")
137135
.whereNotStartsLike("name", "Gh").get()
138136
check res.isEmpty == false
139137
check res.get(0).name == "John"
140138

141139
test "whereNotEndsLike query":
142-
withDB do:
140+
withDBPool do:
143141
let res = Models.table(Users).select("name")
144142
.whereNotEndsLike("name", "st").get()
145143
check res.isEmpty == false
146144
check res.get(0).name == "John"
147145

148146
suite "IN queries":
149147
test "whereNotIn query":
150-
withDB do:
148+
withDBPool do:
151149
let res = Models.table(Users).select("name")
152150
.whereNotIn("name", "John").get()
153151
check res.isEmpty == true

0 commit comments

Comments
 (0)