Skip to content

Commit

Permalink
fix: router problem removing Router. It also, make everything faster
Browse files Browse the repository at this point in the history
  • Loading branch information
enghitalo committed Dec 7, 2024
1 parent 71a17ec commit 2df7c74
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 135 deletions.
1 change: 1 addition & 0 deletions v/v/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ a
*.so
*.dylib
*.dll
*v

# Ignore binary output folders
bin/
Expand Down
30 changes: 14 additions & 16 deletions v/v/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,20 @@ fn main() {
}
```

### Router
## Test

The router setup and route handling are implemented in [src/router.v](v/vanilla/src/router.v). The `setup_router` function initializes the router and adds routes with their respective handler functions:
### CURL

```v
fn setup_router() Router {
mut router := Router{
root: RadixNode{
children: map[string]&RadixNode{}
}
}
// Adding routes with handler functions
router.add_route('GET', '/', home_controller)
router.add_route('GET', '/user', get_users_controller)
router.add_route('GET', '/user/:id', get_user_controller)
router.add_route('POST', '/user', create_user_controller)
return router
}
```sh
curl -X GET --verbose http://localhost:3000/ &&
curl -X GET --verbose http://localhost:3000/user &&
curl -X GET --verbose http://localhost:3000/user/1 &&
curl -X POST --verbose http://localhost:3000/

```

### WRK

```sh
wrk --connection 512 --threads 16 --duration 10s http://localhost:3000
```
38 changes: 20 additions & 18 deletions v/v/src/controllers.v
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
module main

import strings

const h_response_body = '{"message": "Hello, world!"}'
const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()
const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 1\r\nConnection: keep-alive\r\n\r\n1'.bytes()

const http_created_response = 'HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes()

fn home_controller(params map[string]string) ![]u8 {
fn home_controller(params []string) ![]u8 {
return http_ok_response
}

fn get_users_controller(params map[string]string) ![]u8 {
fn get_users_controller(params []string) ![]u8 {
return http_ok_response
}

@[manualfree]
fn get_user_controller(params map[string]string) ![]u8 {
id := params['id'] or { return error('User ID required') }
@[direct_array_access; manualfree]
fn get_user_controller(params []string) ![]u8 {
if params.len == 0 {
return tiny_bad_request_response
}
id := params[0]
response_body := id

response := 'HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: ${response_body.len}\r
Connection: keep-alive\r
\r
${response_body}'.bytes()
mut sb := strings.new_builder(200)
sb.write_string('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ')
sb.write_string(response_body.len.str())
sb.write_string('\r\nConnection: keep-alive\r\n\r\n')
sb.write_string(response_body)

defer {
unsafe {
id.free()
response_body.free()
response.free()
params.free()
}
}

return response
return sb
}

fn create_user_controller(params map[string]string) ![]u8 {
return http_ok_response
fn create_user_controller(params []string) ![]u8 {
return http_created_response
}
4 changes: 1 addition & 3 deletions v/v/src/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ const port = 3000
const max_thread_pool_size = 8

fn main() {
mut server := Server{
router: setup_router()
}
mut server := Server{}

server.server_socket = create_server_socket(port)
if server.server_socket < 0 {
Expand Down
108 changes: 14 additions & 94 deletions v/v/src/router.v
Original file line number Diff line number Diff line change
@@ -1,103 +1,23 @@
module main

// RadixNode represents a node in the Radix Trie used for routing.
// It contains a map of children nodes, a handler function, and information
// about whether the node represents a parameterized segment.
// [see](https://en.wikipedia.org/wiki/Radix_tree)
@[heap]
struct RadixNode {
mut:
children map[string]&RadixNode
handler fn (params map[string]string) ![]u8 = unsafe { nil } // avoid the use of optional
is_param bool
param_name string
}

// Router represents a Radix Trie router with support for parameterized routes.
struct Router {
mut:
root RadixNode // Root node of the Radix Trie
params map[string]string // Map to store route parameters
}

// add_route adds a route to the Radix Trie with support for parameters
fn (mut router Router) add_route(method string, path string, handler fn (params map[string]string) ![]u8) {
segments := path.split('/').filter(it.len > 0)
mut node := &router.root

for segment in segments {
is_param := segment.starts_with(':')
segment_key := if is_param { ':' } else { segment }
if segment_key !in node.children {
node.children[segment_key] = &RadixNode{
children: map[string]&RadixNode{}
is_param: is_param
param_name: if is_param { segment[1..] } else { '' }
}
}
node = node.children[segment_key] or { panic('Unexpected radix trie error') }
}
node.handler = handler
}

// handle_request finds and executes the handler for a given route.
// It takes an HttpRequest object as an argument and returns the response as a byte array.
fn (mut router Router) handle_request(req HttpRequest) ![]u8 {
path := req.buffer[req.path.start..req.path.start + req.path.len]
segments := path.bytestr().split('/').filter(it.len > 0)
mut node := &router.root
// router.params.clear()

// FIXME: race condition here in router.params
router.params = map[string]string{}

for segment in segments {
mut matched := false

for key, child in node.children {
if child.is_param {
// FIXME: race condition here in router.params
router.params[child.param_name] = segment

node = child
matched = true
break
} else if key == segment {
node = child
matched = true
break
}
fn handle_request(req HttpRequest) ![]u8 {
method := unsafe { tos(&req.buffer[req.method.start], req.method.len) }
path := unsafe { tos(&req.buffer[req.path.start], req.path.len) }

if method == 'GET' {
if path == '/' {
return home_controller([])
} else if path.starts_with('/user/') {
id := path[6..]
return get_user_controller([id])
}

if !matched {
dump(req.buffer.bytestr())
return error('Route not matched. segment: ${segment}, path ${path.bytestr()}')
} else if method == 'POST' {
if path == '/user' {
return create_user_controller([])
}
}

if node.handler == unsafe { nil } {
dump(req.buffer.bytestr())
return error('Route does not have handler for path ${path.bytestr()}')
}
handler := node.handler

return handler(router.params)!
}

// setup_router initializes the router and adds predefined routes with their handler functions.
// It returns the initialized Router object.
fn setup_router() Router {
mut router := Router{
root: RadixNode{
children: map[string]&RadixNode{}
}
}

// Adding routes with handler functions/controllers
router.add_route('GET', '/', home_controller)
router.add_route('GET', '/user', get_users_controller)
router.add_route('GET', '/user/:id', get_user_controller)
router.add_route('POST', '/user', create_user_controller)

return router
return tiny_bad_request_response
}
7 changes: 3 additions & 4 deletions v/v/src/server.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ mut:
lock_flag sync.Mutex
has_clients int
threads [max_thread_pool_size]thread
router Router
}

fn C.fcntl(fd int, cmd int, arg int) int
Expand Down Expand Up @@ -241,11 +240,11 @@ fn process_events(server &Server) {
events := [max_connection_size]C.epoll_event{}
num_events := C.epoll_wait(server.epoll_fd, &events[0], max_connection_size, -1)
for i := 0; i < num_events; i++ {
if events[i].events & (C.EPOLLHUP | C.EPOLLERR) != 0 {
if events[i].events & u32((C.EPOLLHUP | C.EPOLLERR)) != 0 {
handle_client_closure(server, unsafe { events[i].data.fd })
continue
}
if events[i].events & C.EPOLLIN != 0 {
if events[i].events & u32(C.EPOLLIN) != 0 {
request_buffer := [140]u8{}
bytes_read := C.recv(unsafe { events[i].data.fd }, &request_buffer[0], 140 - 1,
0)
Expand All @@ -267,7 +266,7 @@ fn process_events(server &Server) {
// This lock is a workaround for avoiding race condition in router.params
// This slows down the server, but it's a temporary solution
(*server).lock_flag.lock()
response_buffer := (*server).router.handle_request(decoded_http_request) or {
response_buffer := handle_request(decoded_http_request) or {
eprintln('Error handling request ${err}')
C.send(unsafe { events[i].data.fd }, tiny_bad_request_response.data,
tiny_bad_request_response.len, 0)
Expand Down

0 comments on commit 2df7c74

Please sign in to comment.