1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

vweb: implement database pool (#18010)

This commit is contained in:
Casper Kuethe 2023-04-23 02:37:15 +02:00 committed by GitHub
parent 6f85384f7f
commit 5f870f41b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 183 additions and 10 deletions

View File

@ -6,7 +6,7 @@ The [gitly](https://gitly.org/) site is based on vweb.
**_Some features may not be complete, and have some bugs._**
## Getting start
## Quick Start
Just run **`v new <name> web`** in your terminal
## Features
@ -16,6 +16,7 @@ Just run **`v new <name> web`** in your terminal
- **Easy to deploy** just one binary file that also includes all templates. No need to install any
dependencies.
- **Templates are precompiled** all errors are visible at compilation time, not at runtime.
- **Multithreaded** by default
### Examples
@ -391,6 +392,96 @@ pub fn (mut app App) not_found() vweb.Result {
}
```
### Databases
The `db` field in a vweb app is reserved for database connections. The connection is
copied to each new request.
**Example:**
```v
module main
import vweb
import db.sqlite
struct App {
vweb.Context
mut:
db sqlite.DB
}
fn main() {
// create the database connection
mut db := sqlite.connect('db')!
vweb.run(&App{
db: db
}, 8080)
}
```
### Multithreading
By default, a vweb app is multithreaded, that means that multiple requests can
be handled in parallel by using multiple CPU's: a worker pool. You can
change the number of workers (maximum allowed threads) by altering the `nr_workers`
option. The default behaviour is to use the maximum number of jobs (cores in most cases).
**Example:**
```v ignore
fn main() {
// assign a maximum of 4 workers
vweb.run_at(&App{}, nr_workers: 4)
}
```
#### Database Pool
A single connection database works fine if you run your app with 1 worker, of if
you access a file-based database like a sqlite file.
This approach will fail when using a non-file based database connection like a mysql
connection to another server somewhere on the internet. Multiple threads would need to access
the same connection at the same time.
To resolve this issue, you can use the vweb's built-in database pool. The database pool
will keep a number of connections open when the app is started and each worker is
assigned its own connection.
Let's look how we can improve our previous example with database pooling and using a
postgresql server instead.
**Example:**
```v
module main
import vweb
import db.pg
struct App {
vweb.Context
db_handle vweb.DatabasePool[pg.DB]
mut:
db pg.DB
}
fn get_database_connection() pg.DB {
// insert your own credentials
return pg.connect(user: 'user', password: 'password', dbname: 'database') or { panic(err) }
}
fn main() {
// create the database pool and pass our `get_database_connection` function as handler
pool := vweb.database_pool(handler: get_database_connection)
// no need to set the `db` field
vweb.run(&App{
db_handle: pool
}, 8080)
}
```
If you don't use the default number of workers (`nr_workers`) you have to change
it to the same number in `vweb.run_at` as in `vweb.database_pool`
### Controllers
Controllers can be used to split up app logic so you are able to have one struct
per `"/"`. E.g. a struct `Admin` for urls starting with `"/admin"` and a struct `Foo`
@ -458,7 +549,8 @@ There will be an error, because the controller `Admin` handles all routes starti
#### Databases and `[vweb_global]` in controllers
Fields with `[vweb_global]` like a database have to passed to each controller individually.
Fields with `[vweb_global]` have to passed to each controller individually.
The `db` field is unique and will be treated as a `vweb_global` field at all times.
**Example:**
```v
@ -470,14 +562,14 @@ import db.sqlite
struct App {
vweb.Context
vweb.Controller
pub mut:
db sqlite.DB [vweb_global]
mut:
db sqlite.DB
}
struct Admin {
vweb.Context
pub mut:
db sqlite.DB [vweb_global]
mut:
db sqlite.DB
}
fn main() {
@ -494,6 +586,50 @@ fn main() {
}
```
#### Using a database pool
**Example:**
```v
module main
import vweb
import db.pg
struct App {
vweb.Context
vweb.Controller
db_handle vweb.DatabasePool[pg.DB]
mut:
db pg.DB
}
struct Admin {
vweb.Context
db_handle vweb.DatabasePool[pg.DB]
mut:
db pg.DB
}
fn get_database_connection() pg.DB {
// insert your own credentials
return pg.connect(user: 'user', password: 'password', dbname: 'database') or { panic(err) }
}
fn main() {
// create the database pool and pass our `get_database_connection` function as handler
pool := vweb.database_pool(handler: get_database_connection)
mut app := &App{
db_handle: pool
controllers: [
vweb.controller('/admin', &Admin{
db_handle: pool
}),
]
}
}
```
### Responses
#### - set_status

View File

@ -371,7 +371,16 @@ pub fn (ctx &Context) get_header(key string) string {
return ctx.req.header.get_custom(key) or { '' }
}
pub type DatabasePool[T] = fn (tid int) T
interface DbPoolInterface {
db_handle voidptr
mut:
db voidptr
}
interface DbInterface {
mut:
db voidptr
}
@ -425,7 +434,7 @@ pub fn controller[T](path string, global_app &T) &ControllerPath {
path: path
handler: fn [global_app, path, routes] [T](ctx Context, mut url urllib.URL, tid int) {
// request_app is freed in `handle_route`
mut request_app := new_request_app[T](global_app, ctx)
mut request_app := new_request_app[T](global_app, ctx, tid)
// transform the url
url.path = url.path.all_after_first(path)
handle_route[T](mut request_app, url, &routes, tid)
@ -514,7 +523,7 @@ pub fn run_at[T](global_app &T, params RunParams) ! {
}
}
fn new_request_app[T](global_app &T, ctx Context) &T {
fn new_request_app[T](global_app &T, ctx Context, tid int) &T {
// Create a new app object for each connection, copy global data like db connections
mut request_app := &T{}
$if T is MiddlewareInterface {
@ -522,9 +531,15 @@ fn new_request_app[T](global_app &T, ctx Context) &T {
middlewares: global_app.middlewares.clone()
}
}
$if T is DbInterface {
$if T is DbPoolInterface {
// get database connection from the connection pool
request_app.db = global_app.db_handle(tid)
} $else $if T is DbInterface {
// copy a database to a app without pooling
request_app.db = global_app.db
}
$for field in T.fields {
if field.is_shared {
unsafe {
@ -622,7 +637,7 @@ fn handle_conn[T](mut conn net.TcpConn, global_app &T, routes &map[string]Route,
}
}
mut request_app := new_request_app(global_app, ctx)
mut request_app := new_request_app(global_app, ctx, tid)
handle_route(mut request_app, url, routes, tid)
}
@ -995,3 +1010,25 @@ fn (mut w Worker[T]) process_incomming_requests() {
eprintln('[vweb] closing worker ${w.id}.')
}
}
[params]
pub struct PoolParams[T] {
handler fn () T [required]
nr_workers int = runtime.nr_jobs()
}
// database_pool creates a pool of database connections
pub fn database_pool[T](params PoolParams[T]) DatabasePool[T] {
mut connections := []T{}
// create a database connection for each worker
for _ in 0 .. params.nr_workers {
connections << params.handler()
}
return fn [connections] [T](tid int) T {
$if vweb_trace_worker_scan ? {
eprintln('[vweb] worker ${tid} received database connection')
}
return connections[tid]
}
}