The issue with app providers
Vapor has a factor known as services, you possibly can add new performance to the system by following the sample described within the documentation. Learn-only providers are nice there is no such thing as a concern with them, they at all times return a brand new occasion of a given object that you simply wish to entry.
The issue is once you wish to entry a shared object or in different phrases, you wish to outline a writable service. In my case I wished to create a shared cache dictionary that I may use to retailer some preloaded variables from the database.
My preliminary try was to create a writable service that I can use to retailer these key-value pairs. I additionally wished to make use of a middleware and cargo all the things there upfront, earlier than the route handlers. š”
import Vapor
non-public extension Utility
struct VariablesStorageKey: StorageKey
typealias Worth = [String: String]
var variables: [String: String]
get
self.storage[VariablesStorageKey.self] ?? [:]
set
self.storage[VariablesStorageKey.self] = newValue
public extension Request
func variable(_ key: String) -> String?
software.variables[key]
struct CommonVariablesMiddleware: AsyncMiddleware
func reply(to req: Request, chainingTo subsequent: AsyncResponder) async throws -> Response
let variables = strive await CommonVariableModel.question(on: req.db).all()
var tmp: [String: String] = [:]
for variable in variables
if let worth = variable.worth
tmp[variable.key] = worth
req.software.variables = tmp
return strive await subsequent.reply(to: req)
Now you would possibly suppose that hey this appears to be like good and it will work and you might be proper, it really works, however there’s a HUGE drawback with this resolution. It isn’t thread-safe in any respect. ā ļø
Whenever you open the browser and sort http://localhost:8080/ the web page will load, however once you begin bombarding the server with a number of requests utilizing a number of threads (wrk -t12 -c400 -d30s http://127.0.0.1:8080/
) the appliance will merely crash.
There’s a related concern on GitHub, which describes the very same drawback. Sadly I used to be unable to unravel this with locks, I do not know why however it tousled much more issues with unusual errors and since I am additionally not in a position to run devices on my M1 Mac Mini, as a result of Swift packages usually are not code signed by default. I’ve spent so many hours on this and I’ve obtained very pissed off.
Constructing a customized world storage
After a break this concern was nonetheless bugging my thoughts, so I’ve determined to do some extra analysis. Vapor’s discord server is often an important place to get the best solutions.
I’ve additionally appeared up different internet frameworks, and I used to be fairly stunned that Hummingbird provides an EventLoopStorage by default. Anyway, I am not going to modify, however nonetheless it is a good to have characteristic.
As I used to be trying on the solutions I spotted that I want one thing just like the req.auth
property, so I’ve began to analyze the implementation particulars extra intently.
First, I eliminated the protocols, as a result of I solely wanted a plain [String: Any]
dictionary and a generic strategy to return the values based mostly on the keys. If you happen to take a better look it is fairly a easy design sample. There’s a helper struct that shops the reference of the request and this struct has an non-public Cache class that can maintain our tips to the situations. The cache is out there via a property and it’s saved contained in the req.storage
.
import Vapor
public extension Request
var globals: Globals
return .init(self)
struct Globals
let req: Request
init(_ req: Request)
self.req = req
public extension Request.Globals
func get<T>(_ key: String) -> T?
cache[key]
func has(_ key: String) -> Bool
get(key) != nil
func set<T>(_ key: String, worth: T)
cache[key] = worth
func unset(_ key: String)
cache.unset(key)
non-public extension Request.Globals
ultimate class Cache
non-public var storage: [String: Any]
init()
self.storage = [:]
subscript<T>(_ kind: String) -> T?
get storage[type] as? T
set storage[type] = newValue
func unset(_ key: String)
storage.removeValue(forKey: key)
struct CacheKey: StorageKey
typealias Worth = Cache
var cache: Cache
get
if let current = req.storage[CacheKey.self]
return current
let new = Cache()
req.storage[CacheKey.self] = new
return new
set
req.storage[CacheKey.self] = newValue
After altering the unique code I’ve provide you with this resolution. Perhaps it is nonetheless not one of the simplest ways to deal with this concern, however it works. I used to be in a position to retailer my variables inside a worldwide storage with out crashes or leaks. The req.globals
storage property goes to be shared and it makes potential to retailer knowledge that must be loaded asynchronously. š
import Vapor
public extension Request
func variable(_ key: String) -> String?
globals.get(key)
struct CommonVariablesMiddleware: AsyncMiddleware
func reply(to req: Request, chainingTo subsequent: AsyncResponder) async throws -> Response
let variables = strive await CommonVariableModel.question(on: req.db).all()
for variable in variables
if let worth = variable.worth
req.globals.set(variable.key, worth: worth)
else
req.globals.unset(variable.key)
return strive await subsequent.reply(to: req)
After I’ve run a number of extra exams utilizing wrk I used to be in a position to verify that the answer works. I had no points with threads and the app had no reminiscence leaks. It was a reduction, however nonetheless I am unsure if that is one of the simplest ways to deal with my drawback or not. Anyway I wished to share this with you as a result of I consider that there’s not sufficient details about thread security.
The introduction of async / await in Vapor will clear up many concurrency issues, however we will have some new ones as effectively. I actually hope that Vapor 5 can be an enormous enchancment over v4, persons are already throwing in concepts and they’re having discussions about the way forward for Vapor on discord. That is only the start of the async / await period each for Swift and Vapor, however it’s nice to see that lastly we’re going to have the ability to do away with EventLoopFutures. š„³