Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connection pool #177

Merged
merged 26 commits into from
Oct 16, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
263d9a3
Add pool implementation for SQLite.
moigagoo Oct 15, 2022
087c324
Add tests for connection pool.
moigagoo Oct 15, 2022
9c71710
Add new test to gitignore.
moigagoo Oct 15, 2022
20a6ef2
Enable --mm:orc for tests.
moigagoo Oct 15, 2022
2eb62a3
Minor formatting cleanup.
moigagoo Oct 15, 2022
6cdf12f
Add doc comments.
moigagoo Oct 15, 2022
6847824
Define --mm:orc for the necessary test only.
moigagoo Oct 15, 2022
ff252e4
Fix doc comments, add pool to API docs.
moigagoo Oct 15, 2022
41bdd85
Minor formatting fixes.
moigagoo Oct 15, 2022
ba02980
Minor formatting fixes.
moigagoo Oct 15, 2022
e94fb5b
Remove redundant check.
moigagoo Oct 15, 2022
20dee37
Update pool implenetation to support postgres.
moigagoo Oct 15, 2022
1e152cb
SQLite: Update pool tests to support the new implementation.
moigagoo Oct 15, 2022
f106d68
Postgres: Add tests for pool.
moigagoo Oct 15, 2022
45d15ae
Add blank line.
moigagoo Oct 15, 2022
d144707
Add blank lines.
moigagoo Oct 15, 2022
b3a5217
Book: Add chapter on connection pooling.
moigagoo Oct 15, 2022
bf3e13b
Book: Improve chapter on pooling.
moigagoo Oct 15, 2022
7cf3271
Update changelog.
moigagoo Oct 15, 2022
60a2b22
Rename Conn to DbConn for consistency.
moigagoo Oct 16, 2022
c4798be
rowutils: Fix docstrings.
moigagoo Oct 16, 2022
d372fa9
Book: Pool: Add code samples and prose on closing the pool.
moigagoo Oct 16, 2022
bec892f
Changelog: Set next version to 2.6.0
moigagoo Oct 16, 2022
912e948
Fix book task.
moigagoo Oct 16, 2022
70f93f9
Allow arbitrary getDb function to be passed to newPool.
moigagoo Oct 16, 2022
75c9610
Add tests for pooling with a custom connection provider.
moigagoo Oct 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ tests/sqlite/tfkpragma_selfref
tests/sqlite/tfkpragmanotint
tests/sqlite/tfkpragmanotmodel
tests/sqlite/tnull
tests/sqlite/tpool
tests/sqlite/trelated
tests/sqlite/trepeatfks
tests/sqlite/trawselect
Expand All @@ -56,6 +57,7 @@ tests/postgres/tfkpragma_selfref
tests/postgres/tfkpragmanotint
tests/postgres/tfkpragmanotmodel
tests/postgres/tnull
tests/postgres/tpool
tests/postgres/trelated
tests/postgres/trepeatfks
tests/postgres/trawselect
Expand Down
3 changes: 2 additions & 1 deletion src/norm.nim
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{.warning[UnusedImport]: off.}

import norm/[model, pragmas, sqlite, postgres]
import norm/[model, pragmas, sqlite, postgres, pool]

106 changes: 106 additions & 0 deletions src/norm/pool.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import std/locks

import sqlite, postgres


type
Conn = sqlite.DbConn | postgres.DbConn
Pool*[T: Conn] = ref object
defaultSize: Natural
conns: seq[T]
poolExhaustedPolicy: PoolExhaustedPolicy
lock: Lock
PoolExhaustedError* = object of CatchableError
PoolExhaustedPolicy* = enum
pepRaise
pepExtend


func newPool*[T: Conn](defaultSize: Positive, poolExhaustedPolicy = pepRaise): Pool[T] =
##[ Create a connection pool of the given size.

``poolExhaustedPolicy`` defines how the pool reacts when a connection is requested but the pool has no connection available:

- ``pepRaise`` (default) means throw ``PoolExhaustedError``
- ``pepExtend`` means throw “add another connection to the pool.”
]##

result = Pool[T](defaultSize: defaultSize, conns: newSeq[T](defaultSize), poolExhaustedPolicy: poolExhaustedPolicy)

initLock(result.lock)

for conn in result.conns.mitems:
conn =
when T is sqlite.DbConn:
moigagoo marked this conversation as resolved.
Show resolved Hide resolved
sqlite.getDb()
elif T is postgres.DbConn:
postgres.getDb()

func defaultSize*(pool: Pool): Natural =
pool.defaultSize

func size*(pool: Pool): Natural =
len(pool.conns)

func pop*[T: Conn](pool: var Pool[T]): T =
##[ Take a connection from the pool.

If you're calling this manually, don't forget to `add <#add,Pool,DbConn>`_ it back!
]##

withLock(pool.lock):
if pool.size > 0:
result = pool.conns.pop()
else:
case pool.poolExhaustedPolicy
of pepRaise:
raise newException(PoolExhaustedError, "Pool exhausted")
of pepExtend:
result =
when T is sqlite.DbConn:
sqlite.getDb()
elif T is postgres.DbConn:
postgres.getDb()

func add*[T: Conn](pool: var Pool, dbConn: T) =
##[ Add a connection to the pool.

Use to return a borrowed connection to the pool.
]##

withLock(pool.lock):
pool.conns.add(dbConn)

func reset*(pool: var Pool) =
## Reset pool size to ``defaultSize`` by closing and removing extra connections.

withLock(pool.lock):
while pool.size > pool.defaultSize:
var conn = pool.conns.pop()
close conn

func close*(pool: var Pool) =
## Close the pool by closing and removing all its connetions.

withLock(pool.lock):
for conn in pool.conns.mitems:
close conn

pool.conns.setLen(0)

template withDb*(pool: var Pool, body: untyped): untyped =
moigagoo marked this conversation as resolved.
Show resolved Hide resolved
##[ Wrapper for DB operations.

Takes a ``DbConn`` from a pool as ``db`` variable,
runs your code in a ``try`` block, and returns connection to the pool afterward.
]##

block:
let db {.inject.} = pool.pop()

try:
body

finally:
pool.add(db)

3 changes: 1 addition & 2 deletions src/norm/postgres.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import std/[os, logging, strutils, sequtils, options, sugar, strformat, tables]

when (NimMajor, NimMinor) <= (1, 6):
Expand Down Expand Up @@ -38,7 +37,7 @@ const

# Sugar to get DB config from environment variables

proc getDb*(): DbConn =
proc getDb*: DbConn =
## Create a ``DbConn`` from ``DB_HOST``, ``DB_USER``, ``DB_PASS``, and ``DB_NAME`` environment variables.

open(getEnv(dbHostEnv), getEnv(dbUserEnv), getEnv(dbPassEnv), getEnv(dbNameEnv))
Expand Down
18 changes: 9 additions & 9 deletions src/norm/private/postgres/rowutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ func isEmptyColumn*(row: Row, index: int): bool =

## This does the actual heavy lifting for parsing
proc fromRowPos[T: ref object](obj: var T, row: Row, pos: var Natural, skip: static bool = false) =
##[ Convert ``ndb.sqlite.Row`` instance into `ref object`_ instance, from a given position.
moigagoo marked this conversation as resolved.
Show resolved Hide resolved
##[ Convert ``ndb.sqlite.Row`` instance into `Model`_ instance, from a given position.

This is a helper proc to convert to `ref object`_ instances that have fields of the same type.
This is a helper proc to convert to `Model`_ instances that have fields of the same type.
]##

for fld, dummyVal in T()[].fieldPairs:
when isRefObject(typeof(dummyVal)): ## If we're dealing with a ``ref object`` field
if dot(obj, fld).toOptional().isSome: ## and it's either a ``some ref object`` or ``ref object``
if dot(obj, fld).toOptional().isSome: ## and it's either a ``some Model`` or ``Model``
var subMod = dot(obj, fld).toOptional().get() ## then we try to populate it with the next ``row`` values.

if row.isEmptyColumn(pos): ## If we have a ``NULL`` at this point, we return an empty ``ref object``:
when typeof(dummyVal) is Option: ## ``val`` is guaranteed to be either ``ref object`` or an ``Option[ref object]`` at this point,
if row.isEmptyColumn(pos): ## If we have a ``NULL`` at this point, we return an empty ``Model``:
when typeof(dummyVal) is Option: ## ``val`` is guaranteed to be either ``Model`` or an ``Option[Model]`` at this point,
when isRefObject(dummyVal):
dot(obj, fld) = none typeof(subMod) ## and the fact that we got a ``NULL`` tells us it's an ``Option[ref object]``,
dot(obj, fld) = none typeof(subMod) ## and the fact that we got a ``NULL`` tells us it's an ``Option[Model]``,

inc pos
subMod.fromRowPos(row, pos, skip = true) ## Then we skip all the ``row`` values that should've gone into this submodel.
Expand All @@ -43,7 +43,7 @@ proc fromRowPos[T: ref object](obj: var T, row: Row, pos: var Natural, skip: sta
inc pos ##
subMod.fromRowPos(row, pos) ## we actually populate the submodel.

else: ## If the field is a ``none ref object``,
else: ## If the field is a ``none Model``,
inc pos ## don't bother trying to populate it at all.

else:
Expand All @@ -56,9 +56,9 @@ proc fromRowPos[T: ref object](obj: var T, row: Row, pos: var Natural, skip: sta


proc fromRow*[T: ref object](obj: var T, row: Row) =
##[ Populate `ref object`_ instance from ``ndb.postgres.Row`` instance.
##[ Populate `Model`_ instance from ``ndb.postgres.Row`` instance.

Nested `ref object`_ fields are populated from the same ``ndb.postgres.Row`` instance.
Nested `Model`_ fields are populated from the same ``ndb.postgres.Row`` instance.
]##

var pos: Natural = 0
Expand Down
16 changes: 8 additions & 8 deletions src/norm/private/sqlite/rowutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ func isEmptyColumn*(row: Row, index: int): bool =

## This does the actual heavy lifting for parsing
proc fromRowPos[T: ref object](obj: var T, row: Row, pos: var Natural, skip: static bool = false) =
##[ Convert ``ndb.sqlite.Row`` instance into `ref object`_ instance, from a given position.
##[ Convert ``ndb.sqlite.Row`` instance into `Model`_ instance, from a given position.

This is a helper proc to convert to `ref object`_ instances that have fields of the same type.
This is a helper proc to convert to `Model`_ instances that have fields of the same type.
]##

for fld, dummyVal in T()[].fieldPairs:
when isRefObject(typeof(dummyVal)): ## If we're dealing with a ``ref object`` field
if dot(obj, fld).toOptional().isSome: ## and it's either a ``some ref object`` or ``ref object``
var subMod = dot(obj, fld).toOptional().get() ## then we try to populate it with the next ``row`` values.

if row.isEmptyColumn(pos): ## If we have a ``NULL`` at this point, we return an empty ``ref object``:
when typeof(dummyVal) is Option: ## ``val`` is guaranteed to be either ``ref object`` or an ``Option[ref object]`` at this point,
if row.isEmptyColumn(pos): ## If we have a ``NULL`` at this point, we return an empty ``Model``:
when typeof(dummyVal) is Option: ## ``val`` is guaranteed to be either ``Model`` or an ``Option[Model]`` at this point,
when isRefObject(dummyVal):
dot(obj, fld) = none typeof(subMod) ## and the fact that we got a ``NULL`` tells us it's an ``Option[ref object]``,
dot(obj, fld) = none typeof(subMod) ## and the fact that we got a ``NULL`` tells us it's an ``Option[Model]``,

inc pos
subMod.fromRowPos(row, pos, skip = true) ## Then we skip all the ``row`` values that should've gone into this submodel.
Expand All @@ -43,7 +43,7 @@ proc fromRowPos[T: ref object](obj: var T, row: Row, pos: var Natural, skip: sta
inc pos ##
subMod.fromRowPos(row, pos) ## we actually populate the submodel.

else: ## If the field is a ``none ref object``,
else: ## If the field is a ``none Model``,
inc pos ## don't bother trying to populate it at all.

else:
Expand All @@ -55,9 +55,9 @@ proc fromRowPos[T: ref object](obj: var T, row: Row, pos: var Natural, skip: sta
inc pos

proc fromRow*[T: ref object](obj: var T, row: Row) =
##[ Populate `ref object`_ instance from ``ndb.sqlite.Row`` instance.
##[ Populate `Model`_ instance from ``ndb.sqlite.Row`` instance.

Nested `ref object`_ fields are populated from the same ``ndb.sqlite.Row`` instance.
Nested `Model`_ fields are populated from the same ``ndb.sqlite.Row`` instance.
]##

var pos: Natural = 0
Expand Down
2 changes: 1 addition & 1 deletion src/norm/sqlite.nim
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const dbHostEnv* = "DB_HOST"

# Sugar to get DB config from environment variables

proc getDb*(): DbConn =
proc getDb*: DbConn =
## Create a ``DbConn`` from ``DB_HOST`` environment variable.

open(getEnv(dbHostEnv), "", "", "")
Expand Down
Loading