Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Precondition failed in Redis 4.1.0 when using repository pattern #180

Closed
leonidas-o opened this issue Jan 30, 2021 · 14 comments · Fixed by #184
Closed

Precondition failed in Redis 4.1.0 when using repository pattern #180

leonidas-o opened this issue Jan 30, 2021 · 14 comments · Fixed by #184
Labels
bug Something isn't working

Comments

@leonidas-o
Copy link

leonidas-o commented Jan 30, 2021

Describe the bug

After updating to 4.1.0 the following error is thrown as soon as I use my cacheRepo.

[ ERROR ] Abort.401: UserModel not authenticated. [request-id: 0661DE6F-B71E-41D6-9B06-2BB4A3F92F9B]
Precondition failed: file .../NIO/ChannelPipeline.swift, line 1447
2021-01-30 15:24:10.165750+0100 Run[13873:207019] Precondition failed: file .../NIO/ChannelPipeline.swift, line 1447

The execution gets stuck at:

    /// Checks the necessary condition of currently running on the called `EventLoop` for making forward progress.
    @inlinable
    public func preconditionInEventLoop(file: StaticString = #file, line: UInt = #line) {
        precondition(self.inEventLoop, file: file, line: line)
    }

Routes without cacheRepo usage, are not affected. Downgrading to redis 4.0.0 shows no errors and everything works as expected.

Inside configure.swift redis is setup using:

    let redisHostname = Environment.get("REDIS_HOSTNAME") ?? "localhost"
    let redisPort: Int
    let redisPassword = Environment.get("REDIS_PASSWORD")
    if (app.environment == .testing) {
        if let testPort = Environment.get("REDIS_PORT_TEST") {
            redisPort = Int(testPort) ?? 6380
        } else {
            redisPort = 6380
        }
    } else {
        redisPort = 6379
    }
    app.redis.configuration = try .init(hostname: redisHostname, port: redisPort, password: redisPassword)
    
    // specify what repository will be created by cacheRepoFactory
    app.cacheRepoFactory.use { req in
        RedisRepo(client: req.redis)
    }
    app.cacheRepoFactory.useForApp { app in
        RedisRepo(client: app.redis)
    }

RedisRepo contains just the requirements from the cacheRepo protocol:

struct RedisRepo: CacheRepo {
    let client: RedisClient
    
    // MARK: - JSON Storage
    
    func save<E>(key: String, to entity: E) -> EventLoopFuture<Void> where E: Encodable {
        let redisKey = RedisKey(key)
        do {
            return client.set(redisKey, to: try JSONEncoder().encode(entity), onCondition: .none, expiration: .keepExisting)
                .transform(to: ())
        } catch {
            return client.eventLoop.makeFailedFuture(error)
        }
    }
    ...

CacheRepoFactory contains:

protocol CacheRepo: ABACCacheRepo {
    func save<E>(key: String, to entity: E) -> EventLoopFuture<Void> where E: Encodable
    func save<E>(key: String, to entity: E, expirationInSeconds seconds: Int) -> EventLoopFuture<Void> where E: Encodable
    func get<D>(key: String, as type: D.Type) -> EventLoopFuture<D?> where D: Decodable
    func getExistingKeys<D>(using keys: [String], as type: [D].Type) -> EventLoopFuture<[D]> where D: Decodable
    func setExpiration(forKey key: String, afterSeconds seconds: Int) -> EventLoopFuture<Bool>
    func delete(key: String) -> EventLoopFuture<Int>
    func delete(keys: [String]) -> EventLoopFuture<Int>
    func timeToLive(key: String) -> EventLoopFuture<Int>
    func exists(_ keys: String...) -> EventLoopFuture<Int>
    func exists(_ keys: [String]) -> EventLoopFuture<Int>
    // hash storage
    func getHash<D>(key: String, field: String, as type: D.Type) -> EventLoopFuture<D?> where D: Decodable
    func getHashAll<D>(key: String, as type: D.Type) -> EventLoopFuture<[String:D]> where D: Decodable
    func setHash<E>(_ key: String, field: String, to entity: E) -> EventLoopFuture<Bool> where E: Encodable
    func setMHash<E>(_ key: String, items: Dictionary<String, E>) -> EventLoopFuture<Void> where E: Encodable
    func deleteHash(_ key: String, fields: String...) -> EventLoopFuture<Int>
    func deleteHash(_ key: String, fields: [String]) -> EventLoopFuture<Int>
}
 
 
 
struct CacheRepoFactory {
    // CacheRepo in Request
    var make: ((Request) -> CacheRepo)?
    mutating func use(_ make: @escaping ((Request) -> CacheRepo)) {
        self.make = make
    }
    
    // CacheRepo in Application
    var makeForApp: ((Application) -> CacheRepo)?
    mutating func useForApp(_ make: @escaping ((Application) -> CacheRepo)) {
        self.makeForApp = make
    }
}
 
 
 
extension Application {
    private struct CacheRepoKey: StorageKey {
        typealias Value = CacheRepoFactory
    }
 
    var cacheRepoFactory: CacheRepoFactory {
        get {
            self.storage[CacheRepoKey.self] ?? .init()
        }
        set {
            self.storage[CacheRepoKey.self] = newValue
        }
    }
}
 
 
 
extension Application {
    var cacheRepo: CacheRepo {
        self.cacheRepoFactory.makeForApp!(self)
    }
}
 
extension Request {
    var cacheRepo: CacheRepo {
        self.application.cacheRepoFactory.make!(self)
    }
}

To Reproduce

First I encountered the error after request.cacheRepo.get(...) was used. But it actually happens on all repo methods. As soon as you try to return request.eventLoop...

struct UserModelBearerAuthenticator: BearerAuthenticator {

    struct AccessDataKey: StorageKey {
        typealias Value = AccessData
    }
    
    func authenticate(bearer: BearerAuthorization, for request: Request) -> EventLoopFuture<Void> {
        return request.cacheRepo.get(key: bearer.token, as: AccessData.self).flatMap { accessData in
            guard let accessData = accessData else {
                return request.eventLoop.makeSucceededFuture(())
            }
            ...

Steps to reproduce the behavior:

  1. Add redis 4.1.0 package
  2. Create repository and factory (see code snippets above)
  3. Use whatever cacheRepo method you want in a route handler, afterwards every return of a future (req.eventLoop.) will throw an error
  4. See error

Expected behavior

No error, same behaviour like in redis 4.0.0.

Environment

  • Vapor Framework version: 4.39.2
  • Vapor Toolbox version: 18.3.0
  • OS version: MacOS Catalina 10.15.7
@leonidas-o leonidas-o added the bug Something isn't working label Jan 30, 2021
@leonidas-o leonidas-o changed the title Precondition failed in Redis 4.1.0 when using Repository Precondition failed in Redis 4.1.0 when using repository pattern Jan 31, 2021
@pankajsoni19
Copy link

Same here. I reverted back to 4.0.0

@Mordil
Copy link
Member

Mordil commented Feb 3, 2021

Yes, for now our recommendation is to pin Redis as .upToNextMinor(from: "4.0.0") until we can properly get a fix out.

@siemensikkema
Copy link
Member

@leonidas-o or @pankajsoni19 could I ask you to try this using:
.package(url: "https://gitlab.com/vapor/redis.git", .branch("feature/eventloop-fix"))

@pankajsoni19
Copy link

I have moved out of the organization where I wrote the swift based server code. So objective was to attain stability in last deployment under me. I would not be able to give time to test this out.

@leonidas-o
Copy link
Author

@siemensikkema I guess you mean github.com/vapor/redis.git and not gitlab...? Okay, that looks good. At least all my routes are working now without throwing an error.

@siemensikkema
Copy link
Member

@leonidas-o haha, yeah how did I make that mistake? I blindly copy/pasted and adapted from another line...
Great to hear that it's working! That should allow us to avoid reverting 4.1.0.

@leonidas-o
Copy link
Author

🙂 never mind and thanks for the fix. I guess this will be just the next tag after 4.1.0?

@0xTim
Copy link
Member

0xTim commented Feb 5, 2021

@leonidas-o yeah this has been released in 4.1.1

@HashedViking
Copy link

I've got a crash on boot when redis version > 4.0.0:

Fatal error: No redis found for id default, or the app may not have finished booting. Also, the eventLoop must be from Application's EventLoopGroup.: file Redis/RedisStorage.swift, line 51

But pinning to 4.0.0 helps:
.package(url: "https://github.com/vapor/redis.git", .exact("4.0.0"))

@0xTim
Copy link
Member

0xTim commented Mar 3, 2021

@HashedViking which version of Redis exactly is causing the crash?

@HashedViking
Copy link

HashedViking commented Mar 3, 2021

@0xTim 4.2.0

boot.swift

public func boot(_ app: Application) throws {   
    let cachedPKsFuture = app.redis.get(RedisKey(FRK.firebasePKs),
                                        asJSON: FirebasePublicKeys.self) // <<--- crash
// ...
}

On boot I try to fetch the data previously cached in Redis, with 4.0.0 this way was ok.

@0xTim
Copy link
Member

0xTim commented Mar 5, 2021

@HashedViking if you call app.boot() before that does it work?

@HashedViking
Copy link

yeah, calling try? app.boot() at the first line fixes the problem, is it intended behaviour?

@0xTim
Copy link
Member

0xTim commented Mar 5, 2021

Not intentionally but yes it's required because of the way Redis sets up its internal state

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants