Friday, August 12, 2022
HomeiOS DevelopmentConstructing a world storage for Vapor

Constructing a world storage for Vapor


The issue with app providers


Vapor has a factor known as providers, you may add new performance to the system by following the sample described within the documentation. Learn-only providers are nice there isn’t any difficulty with them, they at all times return a brand new occasion of a given object that you just need to entry.

The issue is while you need to entry a shared object or in different phrases, you need 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 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 = attempt 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 attempt await subsequent.reply(to: req)
    }
}


Now you may assume that hey this seems good and it will work and you’re proper, it really works, however there’s a HUGE downside with this answer. It isn’t thread-safe in any respect. ⚠️


If you open the browser and sort 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 appliance will merely crash.

There’s a related difficulty on GitHub, which describes the very same downside. Sadly I used to be unable to unravel this with locks, I do not know why but it surely 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 will not be code signed by default. I’ve spent so many hours on this and I’ve acquired 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 precise solutions.


I’ve additionally regarded up different internet frameworks, and I used to be fairly stunned that Hummingbird affords 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 trying 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 carefully.


First, I eliminated the protocols, as a result of I solely wanted a plain [String: Any] dictionary and a generic solution 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 may maintain our tips that could the cases. The cache is on the market by way of 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 {

    remaining 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 present = req.storage[CacheKey.self] {
                return present
            }
            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 answer. Perhaps it is nonetheless not the easiest way to deal with this difficulty, but it surely works. I used to be in a position to retailer my variables inside a world storage with out crashes or leaks. The req.globals storage property goes to be shared and it makes attainable 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 = attempt 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 attempt 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 undecided if that is the easiest way to deal with my downside or not. Anyway I needed to share this with you as a result of I imagine that there’s not sufficient details about thread security.

The introduction of async / await in Vapor will clear up many concurrency issues, however we’ll have some new ones as nicely. 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, but it surely’s nice to see that lastly we’re going to have the ability to do away with EventLoopFutures. 🥳


RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular