Precondition failed in Redis 4.1.0 when using repository pattern
leonidas-o opened this issue · comments
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:
- Add redis 4.1.0 package
- Create repository and factory (see code snippets above)
- Use whatever cacheRepo method you want in a route handler, afterwards every return of a future (
req.eventLoop.
) will throw an error - 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
Same here. I reverted back to 4.0.0
Yes, for now our recommendation is to pin Redis as .upToNextMinor(from: "4.0.0")
until we can properly get a fix out.
@leonidas-o or @pankajsoni19 could I ask you to try this using:
.package(url: "https://gitlab.com/vapor/redis.git", .branch("feature/eventloop-fix"))
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.
@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.
@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.
🙂 never mind and thanks for the fix. I guess this will be just the next tag after 4.1.0?
@leonidas-o yeah this has been released in 4.1.1
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"))
@HashedViking which version of Redis exactly is causing the crash?
@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.
@HashedViking if you call app.boot()
before that does it work?
yeah, calling try? app.boot()
at the first line fixes the problem, is it intended behaviour?
Not intentionally but yes it's required because of the way Redis sets up its internal state