- Performance
- Multithreading (and by extension, thread-safety)
- Security
I've gotten it to a usable point right now, and I think I could use it in production without any issues (I've been doing performance and load testing consistently during development), but it's missing a few key features, which I'd like to discuss and get some feedback on.
The framework is built primarily for HTTP servers, but my intention is to provide a framework for safe cross-thread communication via an event bus that helps implement the actor model of communication. I take a lot of inspiration from Vert.x in the JVM world, which uses a single application instance shared between threads that provides an event bus for communication. In Vert.x, there is the concept of a Verticle, which is effectively an actor. This is what I aim to do with KommandKit. However, as Nim is not an object-oriented language, I'm not sure what construct I should use to represent an actor. If anyone has any suggestions, I'd appreciate them. My current idea is creating a concept for an actor and making constructing them easier with macros, but I'm not sure.
Currently, to scale the HTTP server over CPU cores, you create a thread for each CPU core/thread. Those threads are passed an instance of the global KommandKit object which they can interact with. Since I haven't implemented the event bus yet, the shared object doesn't do anything right now. I'm currently waiting to implement that until I cam come up with how the implementation should look. This is something I'd like to get some feedback on. Right now I'm thinking there could be an outgoing Channel set up for each actor that's registered so that it can send messages to the event bus, and then any channels that actors subscribe to will create a corresponding incoming Channel for the actor that the event bus writes to. The issue here is I'm not sure how the actor should read from the channel. Since async work is going on in the thread where the actor is living, the blocking channel read procs cannot be used. Should I just poll? That introduces latency, and CPU usage if I'm polling for it too often. Perhaps there's a better construct to use, but I'm not sure. I may also have to open another outgoing Channel since messages will support replies.
On the HTTP side, I'm pretty happy with what I have right now. A standard router and middleware system is used, and everything is done with streams. I'd like to use Status' faststreams library in the future for various parts of it, but right now, dealing with reads and writes on the client is usable enough with helper procs on top of it.
Since the HTTP server is dealing with HTTP-native errors such as request validation errors, I decided that following in Vert.x's footsteps and allowing developers to assign handlers for HTTP error response codes was the best way to do error handling. From a normal request handler, you can return with an HTTP status, and optionally an Exception to go along with it, and then it will run the handler registered for that status, or a default one, making the optional Exception available. For normal exceptions caught within handlers, the server will invoke the HTTP 500 error handler, or the default one if no 500-specific handler is set up. This makes handling errors incredibly easy and idiomatic for HTTP. However, if anyone has suggestions, I'd be happy to hear them.
Having direct access to client read and write streams means the server can do everything from accepting uploads to sending files without memory issues. Right now things are a bit verbose with the API (you end routes with lines such as return await ctx.response.endWith("Hello world") and similar), but I'm working on hiding the complexity more and making it more approachable. With that said, I'm trying to keep the API as magic-free as possible to avoid confusion.
Let me know what you all think, and if you have any suggestions for the things I mentioned above, I'd be happy to hear them. My goal is to built a production-ready web framework that is performant and safe, and I think I'm on the right track. You can check out what the API looks at from the HTTP server test and benchmark, although I'm going to be writing a lot more documentation once the remaining parts of the design are solidified. I will also open a website with more documentation and tutorials to make web development in Nim more approachable.
Thanks in advance for your feedback.
However, as Nim is not an object-oriented language, I'm not sure what construct I should use to represent an actor. If anyone has any suggestions, I'd appreciate them. My current idea is creating a concept for an actor and making constructing them easier with macros, but I'm not sure.
Since you cannot create a container of "concept" this is doomed to fail. Go for a base type + inheritance.
However, as Nim is not an object-oriented language, I'm not sure what construct I should use to represent an actor.
I had similar issue, I needed to store abstract App inside of a Session. I also use event based processing, I call it the App but it's pretty much the Actor. And it behaves in pretty much the same way as Erlang Actor, basically just a single threaded function with huge switch and internal state, to listen and react on incoming events and respond with outcoming event. Here's the definition of the App (Actor) I use:
type App* = proc(events: seq[InEvent], mono_id: string): seq[OutEvent]
And this would be it in more idiomatic Nim:
proc buildApp: App =
let todo = TodoApp()
result = proc(events: seq[InEvent]): seq[OutEvent] =
todo.process(events)
That might work. I think in my case it would have to be an object with proc pointers since it would handle startup and shutdown events. I'll see what happens.
@Araq Do you have any suggestions in terms of event bus implementation, particularly about receiving events from channels on the async side? Using the polling method seems the most obvious but not ideal
@Araq Do you have any suggestions in terms of event bus implementation, particularly about receiving events from channels on the async side? Using the polling method seems the most obvious but not ideal
Not really no, sorry. I usually use polling too as it's the only thing that actually composes well.
particularly about receiving events from channels on the async side? Using the polling method seems the most obvious but not ideal
Channels and async use different kernel concurrency mechanisms and aren't comptible. What you need when using async are network based event loop mechanism. In Nim this is newSelectEvent. It looks like asyncnet stuff wraps them with newSelectEvent.
Using SelectEvent/AsyncSelectEvent is pretty efficient and uses the kernel select/poll but the Nim api doesn't let you pass data. So you need to combine them with a channel or queue separately. The core idea is to put data into a queue and then send a notification via SelectEvent that data is available. The receiver will be woken up and can get the data.
A while back I made some inettypes and inetqueues that do this for non-async. You can probably find some ideas from them, or perhaps tweak them to be async.