diff --git a/vlib/vweb/README.md b/vlib/vweb/README.md index 6bf5250e8b..c3c83603fa 100644 --- a/vlib/vweb/README.md +++ b/vlib/vweb/README.md @@ -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 web`** in your terminal ## Features @@ -16,6 +16,7 @@ Just run **`v new 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 diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index 5a6547579c..9938ce65d4 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -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] + } +}