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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

vweb2 #19997

Merged
merged 12 commits into from
Dec 9, 2023
Merged

vweb2 #19997

merged 12 commits into from
Dec 9, 2023

Conversation

Casper64
Copy link
Member

@Casper64 Casper64 commented Nov 25, 2023

This pr adds a new version of vweb in x.vweb.

Documentation and the intergration of the other vweb modules are coming later.

Breaking changes

  • All existing vweb applications aren't compatible with x.vweb
  • Small change to picoevs raw_callback mode. Requires adding 1 extra parameter.

Biggest changes

  • The backend of vweb uses the picoev module. This makes vweb2 single threaded and non-blocking instead of multithreaded and blocking.
  • Split request context and App into different structs.
  • Different usage of middleware, only in the way the functions are registered
  • Removed restriction of not able to have nested Controllers for more flexibility
  • Lots of improvements for methods of vweb.Context.
  • Because the App and Context will be split static files are handled by embedding vweb.StaticHandler

App & Context

Splitting the App from the request context has the advantage that the attribute @[vweb_global] is no longer required! This is because not a new App struct is created for each request, but only the Context struct.
You can use the App struct for state that you want to share amongst routes and the Context struct for state that you want to keep request specific.

before:

pub struct App {
	vweb.Context
	value string @[vweb_global]
}

pub fn (mut app App) index() vweb.Result {
	return app.text('need for [vweb_global]: ${app.value}')
}

fn main() {
	vweb.run(&App{
		value: 'test'
	}, 8080)
}

after:

pub struct Context {
	vweb.Context
}

pub struct App {
	value string
}

pub fn (mut app App) index(mut ctx Context) vweb.Result {
	return ctx.text('no need for [vweb_global]: ${app.value}')
}

fn main() {
	mut app := &App{
		value: 'test'
	}
	vweb.run[App, Context](mut app, 8080)
}

You can also see that we need to pass the type of the App struct and our Context struct to vweb.run and we have to make our app mut. This also applies for Controllers.

vweb.run[App, Context](mut app, 8080)

Middleware

The middleware is changed to make it more powerful and similair to other big web frameworks:

  • Add middleware with a use function
  • Option to run middleware before the request is passed to your App, or after the request is processed by your App struct. The default behaviour is to run the function before the request is passed to the App struct.
  • The users Context struct is passed to middleware functions

Add the Middleware struct to you App struct and specify the type of the Context struct:

pub struct App {
	vweb.Middleware[Context]
}
mut app := &App{}
// middleware for all routes
app.use(handler: global_middleware)
// middleware for specific route(s)
app.route_use('/:foo', handler: dynamic_middleware)
// vweb middleware that adds gzip encoding to the HTTP response
app.use(vweb.encode_gzip[Context]())
// middleware with `after` set to true are functions that are executed when vweb is done
// and will be called right before the response is sent, you can use this to
// set headers on the response or other stuff
app.use(handler: after_middleware, after: true)

Static files

The methods for adding and handling static files are the same, but they are moved from vweb.Context to vweb.StaticHandler.

pub struct App {
	vweb.StaticHandler
}

fn main() {
	mut app := &App{}
	app.handle_static('testdata', true)!
}

Pros & cons of a single threaded design

pros:

  • Stability: single threaded event loops are very stable under heavy load
  • Speed: a lot of optimizations can and have been done to reduce the amount of "blocking" code as much as possible.
  • Can handle large requests and large payloads: reading and writing to sockets are done only when the socket is ready for read/write. For example it is possible to have lots of simultaneaous connections that download large files. In a threaded design the amount of simultaneaous connections that are handled is bound to the number of available threads.
  • Because the program is now single threaded you can have mut fields on the App struct without having to worry about race conditions instead of having to use shared, assuming you won't use that field in another thread.

cons:

  • While a method of the App struct or on the Context struct is executed the rest of the code will block until those functions are done. For example when a method needs to fetch data from an external source and that request takes 1 second all vweb will essentially wait for your work to be done. This could be mittigated by making the request in a seperate thread. (See Context.takeover_conn())
  • Complexity: writing "non-blocking" code adds a lot of complexity compared to "blocking" code, in terms of networking.
  • Perfomance: the performance of the web server will be very dependent on single-core performance.

Benchmarks

These benchmarks are only to provide an insight on the difference in performance, please don't extract too much meaning from the numbers.

All apps are tested in -prod mode on the apps showed in the example from " App & Context".
old: 24 threads:

wrk -t 12 -c 400 http://localhost:8081 -d 15s
Running 15s test @ http://localhost:8081
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    47.08ms  131.90ms   1.93s    96.85%
    Req/Sec   453.57    391.62     3.55k    91.11%
  70688 requests in 15.07s, 6.74MB read
  Socket errors: connect 0, read 0, write 0, timeout 10
Requests/sec:   4690.24
Transfer/sec:    458.03KB

new:

wrk -t 12 -c 400 http://localhost:8080/ -d 15s
Running 15s test @ http://localhost:8080/
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.89ms  739.45us  60.38ms   93.83%
    Req/Sec     5.63k   332.55     9.74k    83.18%
  1012075 requests in 15.05s, 124.51MB read
Requests/sec:  67260.53
Transfer/sec:      8.27MB

Keep in mind that for a multithreaded application this is the worst-case scenario in terms of load and for the single threaded version the best-case scenario.

Sending bigger responses

Every request returns a 4328KB file. It is clear that this new structure can handle bigger responses in an efficient manner.

wrk -t 12 -c 400 http://localhost:8080/ -d 15s
Running 15s test @ http://localhost:8080/
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    29.92ms    1.53ms  53.92ms   96.09%
    Req/Sec     1.11k   135.88     2.00k    77.41%
  199085 requests in 15.06s, 853.81MB read
Requests/sec:  13220.03
Transfer/sec:     56.70MB

TODO

  • Write new documentation
  • Integrate existing vweb submodules with vweb2
  • Database pool
  • Simple reverse proxy provided by vweb

馃[deprecated] Generated by Copilot at 28eb046

This pull request adds several new features and improvements to the vweb module, such as nested and host-specific controllers, middleware functions and options, static file handling, large payload handling, and more parsing and routing functions. It also refactors the vweb module by separating some structs and functions into different files and adds more test functions and test data to increase the test coverage of the module.

馃[deprecated] Generated by Copilot at 28eb046

  • Refactor the vweb module by separating the Context, Controller, Middleware, StaticHandler, and parsing functions into their own files (vlib/x/vweb/context.v, vlib/x/vweb/controller.v, vlib/x/vweb/middleware.v, vlib/x/vweb/static_handler.v, and vlib/x/vweb/parse.v) (link, link, link, link, link)
  • Add a new feature to the vweb module by allowing nested controllers and host-specific controllers, and provide methods for registering and generating controllers (link)
  • Add a new feature to the vweb module by allowing middleware functions and options, and provide methods for adding global or route-specific middleware (link)
  • Add a new field total_read to the BufferedReader struct to keep track of the total number of bytes read from the underlying reader, and increment it in the read and read_byte methods (link, link, link)
  • Add a new parameter events to the raw_cb fields of the Picoev and PicoevConfig structs, and pass it to the raw callback function in the handle_read and handle_write methods (link, link, link, link)
  • Make the Picoev.del method public and accessible from other modules (link)
  • Add a blank line between the constants in the vlib/picoev/picoev.v file for readability (link)
  • Add a blank line to the ./examples/pico/raw_callback.v file for readability (link)
  • Add a new parameter events to the handle_conn function in ./examples/pico/raw_callback.v (link)
  • Improve the test coverage of the vweb module by adding test functions for the route_matches, controller, middleware, static handler, large payload, and vweb app features, and provide test data and a test server (vlib/x/vweb/route_test.v, vlib/x/vweb/tests/controller_test.v, vlib/x/vweb/tests/middleware_test.v, vlib/x/vweb/tests/static_handler_test.v, vlib/x/vweb/tests/large_payload_test.v, vlib/x/vweb/tests/vweb_app_test.v, vlib/x/vweb/tests/testdata/, and vlib/x/vweb/tests/vweb_test_server.v) (link, link, link, link, link, link, link, link, link, link)
  • Improve the test coverage of the BufferedReader struct by adding test functions for the total_read field after calling the read and read_line methods (vlib/io/reader_test.v) (link)

@enghitalo
Copy link
Contributor

Great!!!

One question. How will a Middleware work with redirect now?

@Casper64
Copy link
Member Author

Great!!!

One question. How will a Middleware work with redirect now?

The same as before, the only difference is when the redirect occurs: before the request is processed or after.

Example:

module main

import x.vweb

pub struct Context {
	vweb.Context
}

pub struct App {
	vweb.Middleware[Context]
}

@['/to-redirect']
pub fn (app &App) to_redirect(mut ctx Context) vweb.Result {
	println('from redirect')
	return ctx.text('from redirect')
}

pub fn (app &App) other(mut ctx Context) vweb.Result {
	return ctx.text('from other')
}

fn redirect(mut ctx Context) bool {
	ctx.redirect('/other')
	// return false to indicate we sent another response
	return false
}

fn main() {
	mut app := &App{}
	app.Middleware.route_use('/to-redirect', handler: redirect, after: true)

	vweb.run[App, Context](mut app, 8080)
}

This code will print "from redirect", but in the browser you will see the text "from other". If after was set to false we would not see the text "from redirect" in the terminal, which is the default behaviour.

The response is not sent directly when you call ctx.text(), but only after all middleware has been executed.

@Casper64 Casper64 marked this pull request as ready for review November 30, 2023 15:57
@Casper64
Copy link
Member Author

Will add the other TODO鈥檚 in separate pull requests. This one is already quite big

vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
vlib/x/vweb/README.md Outdated Show resolved Hide resolved
@enghitalo
Copy link
Contributor

Hi, @Casper64. Will something like this works in vweb2? Note that sse function cannot have a return.

module main

import vweb
import vweb.sse
import rand

struct App {
	vweb.Context
mut:
	sse_connections shared []sse.SSEConnection
}

pub fn (mut app App) before_request() {
	app.add_header('Access-Control-Allow-Origin', '*')
}

@['/sse'; get]
fn (mut app App) sse() {
	println('sse')
	mut conn := sse.new_connection(app.conn)

	conn.headers['Access-Control-Allow-Origin'] = '*'

	conn.start() or {
		println('err: ${err}')
		// return app.server_error(501) // REVIEW
	}

	lock app.sse_connections {
		app.sse_connections << conn
	}
	
	// for {
	// }
}

// This not work (there are not connection)
@['/notification'; post]
fn (mut app App) notification() vweb.Result {
	lock app.sse_connections {
		for mut conn in app.sse_connections {
			conn.send_message(sse.SSEMessage{
				id: rand.uuid_v4()
				event: 'statusUpdate'
				data: app.req.data
				retry: 3000
			}) or { eprintln(err) }
			conn.send_message(sse.SSEMessage{
				id: rand.uuid_v4()
				event: 'ping'
				data: app.req.data
				retry: 3000
			}) or { eprintln(err) }
		}
	}

	return app.text('Notification received')
}

fn main() {
	shared sse_connections := []sse.SSEConnection{}

	vweb.run(&App{
		sse_connections: sse_connections
	}, 3001)
}

@Casper64
Copy link
Member Author

Casper64 commented Dec 7, 2023

@enghitalo yes!

vweb.Context has a new method: takover_conn. See the comment I provided

// takeover_conn prevents vweb from automatically sending a response and closing
// the connection. You are responsible for closing the connection.
// In takeover mode if you call a Context method the response will be directly
// send over the connetion and you can send multiple responses.
// This function is usefull when you want to keep the connection alive and/or
// send multiple responses. Like with the SSE.

And the advanced usage section of the readme for an example.

Or in your case you can change the sse method to return ctx.takover_conn()

@['/sse'; get]
fn (mut app App) sse(mut ctx Context) vweb.Result {
	println('sse')
	mut conn := sse.new_connection(app.conn)

	conn.headers['Access-Control-Allow-Origin'] = '*'

	conn.start() or {
		println('err: ${err}')
		// return app.server_error(501) // REVIEW
	}

	lock app.sse_connections {
		app.sse_connections << conn
	}
        return ctx.takeover_conn()
}

Comment on lines +262 to +263
'vlib/vweb/x/tests/vweb_test.v',
'vlib/vweb/x/tests/vweb_app_test.v',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'vlib/vweb/x/tests/vweb_test.v',
'vlib/vweb/x/tests/vweb_app_test.v',
'vlib/x/vweb/tests/vweb_test.v',
'vlib/x/vweb/tests/vweb_app_test.v',

@medvednikov medvednikov merged commit 08189d6 into vlang:master Dec 9, 2023
48 checks passed
@Casper64 Casper64 deleted the vweb2 branch December 10, 2023 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants