Concurrent access to Database, Logger and HTTP client

Let's say I need three different independent worker loops. Each of them should be able to communicate with database, send http requests and log some messages about own activity. And these workers should not conflit with routes handling (req.db, reg.logger, req.client).

How to organize it properly in Vapor?

  1. Make it thread safe
  2. Properly control database connections through ConnectionsPool (and setup max connections number).
  3. Properly control http clients instances to undestand how many http request wil be active concurretly

My first attempt was just create detached Task for each loop with app reference inside (app.db, app.client, app.logger). As a result - some crashes connected with internal Vapor's storage during call getter for database. Could you explain please how I should do that in rules of Vapor's architecture?

P.S. It's not a criticism but trying to find any instructions to able use it correctly for my needs.

It sounds like you are trying to create a service. This link will let you set up a skeleton for the service Vapor: Advanced → Services. As for DB service connections are you trying to use an existing Database already attached to your Vapor App, or are you opening a connection within the service?

Thread safety is something that Swift is working to make inherent in all Swift code. Unless you try to force something unusual you get thread safety by default.

Each time a request arrives at a Vapor app the request/response is just processed. There is really no state kept between requests, each request is a fresh clean request unless you use a cookie, or Token to track the state from a previous request.

Do you expect the detached task to be a task that is running in the background waiting for something to arrive, or like a background task that you call while processing the Vapor request/response?

I'm not sure that I need a service in terms of Vapor doc explanation by your link. As I can see there - it's just some extension for Application or Request. With ability to store something inside Vapor storage and presented through computed property from extension.

In my case I have:

  1. Database - implemented with Fluent and it's models
  2. REST API to access database (it's implemented with Vapor routes/controllers)
  3. Some additional logic which should be running in background (infinite). This logic will communicate with the same database (fetch some data, process it, put some results back), and for such processing I need HTTP client(s) and logger.

Just to make it more clear - this background logic iterative in terms of some loops interactions but each iteration has 2 parts: "sync" - we are calling some sync and async functions one by one, and "async" - when we starting some concurrent tasks and do not waiting for its completion just go to the next iteration.

So actually my question is how to organise this stuff properly in Vapor to be able use existing connection pools for Database, make it thread-safe when accessing to Vapor instances from different concurrent detached tasks.

Maybe Vapor is not a proper tool to achieve that?
If we are talking about handling user's HTTP requests - it's more or less clear. It's relatively short amount of work connected to the request and its resources/context (logger, client, database).
But if wee need to do some background work in parallel with request handling - I can't find any docs or examples how to do that in proper way to be able use the same context resources for background work.

struct AppHandler: LifecycleHandler {
    func willBoot(_ app: Application) throws { }
    func didBoot(_ app: Application) throws {
        let eventLoop = app.eventLoopGroup.next()
        let promise = eventLoop.makePromise(of: Void.self)
        promise.completeWithTask {
            while true {
                let logger = app.logger
                let client = app.client
                let database = app.db
                let manager = MyExecutionManager(logger: logger, client: client, database: database)
                do {
                    try await manager.executeNextTask()
                    /// executeNextTask() also could contain some calls like that where we create separate promiseWithTask without waiting for completion in place of call
                } catch {
                    pack.logger.critical("Tasks execution loop failure:\n\(String(reflecting: error))")
                    break
                }
            }
        }
    }
    func shutdown(_ app: Application) { }
}

Or maybe I should create totally own pool for database connections, and own http client?
But definitely I would like to use the same logic to manage connections and concurrent http request as for rest of Vapor app.

Any help really appreciated.

Vapor is fine. The way I've handled this sort of situation in my own project, a turn based game server, is to have an actor GameManager that does the turn processing. A player submits a turn via an HTTP request, and that in turn generates a Task that invokes turn processing on the singleton game manager. The game manager holds a reference to the application, through which it accesses the database, logger, and other services. Separately, I define a LifecycleHandler that tells the game manager to stop processing when the application is shutting down.

Background processing happens on the game manager and may (almost certainly will) last longer than any single request. Clients periodically check back for updates, and the ultimate source of truth is the database.

1 Like

Thanks for answering!
Ok. If I got you correctly you process turns one by one in GameManager. But in my case it should be like that: by user request we "register" turn into database (here we can use DB, logger and client from request) and after that Game Manger should be able to fetch turn from DB, mark it as processing (separate transaction), make some internal http request to the third-party service and save result of such http call as the turn resolution (saving - separate transaction). But until we are waiting for http request completion - we should be able to start processing next registered turn if available (next iteration in my loops) from db (here we have another problem - we should be able to detect current cpu load and memory usage to limit concurrent turn's processing). I need such logic to be able process turns from DB by different worker instances with synchronisation through DB transactions.
So if I will create singleton for GameManager as actor - I will be able to control serial access to the app reference inside it, but as soon as I will create separate Task to delegate http call handling with saving results - should I create separate actor for that and keep app ref there as well? How it aligned with SwiftNIO event loops? Will it be safe to concurrent touch Application ref from different actors ?

Will it be safe to concurrent touch Application ref from different actors ?

Yes, because the Application handles concurrent access internally. When your code is already in the context of a request, the resources are stuck on the request as a convenience. When your code is running not in the context of a request (e.g., a Task spun off to do some background work which will complete sometime) then you just need to give that code a reference to the application so that when the code needs a resource it has somewhere to go to get it.

You may not even need that singleton/actor "manager" object that I use. I created that because I need to hook into the application life cycle and execute some code at application shutdown.

Thanks so much! Works as expected. At least for now I didn't find any issues.

Late to the party here but in general you shouldn't have any issue. The latest Vapor releases add some protection around accessing the Storage as it's all now Sendable and things are behind locks etc