The issue with app companies
Vapor has a factor known as companies, you may add new performance to the system by following the sample described within the documentation. Learn-only companies are nice there is no such thing as a difficulty with them, they all the time return a brand new occasion of a given object that you simply wish to entry.
The issue is while you wish to entry a shared object or in different phrases, you wish to outline a writable service. In my case I needed 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 needed to make use of a middleware and cargo every part there upfront, earlier than the route handlers. 💡
import Vapor
non-public extension Software {
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 may assume that hey this appears good and it will work and you might be proper, it really works, however there’s a HUGE drawback with this resolution. It is not thread-safe in any respect. ⚠️
Whenever you open the browser and kind http://localhost:8080/ the web page will load, however while 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 applying will merely crash.
There’s a related difficulty on GitHub, which describes the very same drawback. Sadly I used to be unable to resolve this with locks, I do not know why nevertheless 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 bought very pissed off.
Constructing a customized world storage
After a break this difficulty was nonetheless bugging my thoughts, so I’ve determined to do some extra analysis. Vapor’s discord server is often an excellent place to get the proper solutions.
I’ve additionally appeared up different net frameworks, and I used to be fairly stunned that Hummingbird presents an EventLoopStorage by default. Anyway, I am not going to modify, however nonetheless it is a good to have function.
As I used to be wanting on the ideas I noticed 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 method to return the values based mostly on the keys. In case you take a more in-depth 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 that could the situations. The cache is accessible by 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 {
closing 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 give you this resolution. Possibly it is nonetheless not one of the simplest ways to deal with this difficulty, nevertheless 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 doable to retailer information 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 checks utilizing wrk I used to be in a position to affirm that the answer works. I had no points with threads and the app had no reminiscence leaks. It was a aid, however nonetheless I am undecided if that is one of the simplest ways to deal with my drawback or not. Anyway I needed 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 remedy many concurrency issues, however we will have some new ones as effectively. I actually hope that Vapor 5 will likely 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 just the start of the async / await period each for Swift and Vapor, nevertheless it’s nice to see that lastly we’re going to have the ability to eliminate EventLoopFutures. 🥳