Reputation: 651
I have a middleware which fetches a token if there is no one to be found in Redis.
struct TokenMiddleware: Middleware, TokenAccessor {
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-ID")) else {
throw Abort(.badRequest, reason: "missing 'Client-ID' in header")
}
guard request.clientID.isEmpty == false else {
throw Abort(.badRequest, reason: "'Client-ID' in header is empty")
}
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-Secret")) else {
throw Abort(.badRequest, reason: "missing 'Client-Secret' in header")
}
/// getToken fetches a new Token and stores it in Redis for the controller to use
return try self.getToken(request: request).flatMap(to: Response.self) { token in
return try next.respond(to: request)
}
}
}
extension TokenMiddleware: Service {}
But this causes multiple processes fetching new tokens on their own and therefore a race condition.
How can I handle this in vapor?
Upvotes: 2
Views: 739
Reputation: 651
I solved the problem now, thanks to Soroush from http://khanlou.com/2017/09/dispatch-on-the-server/ who hinted me into the right direction. More infos on the DispatchQueues can be found in an excellent article from https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
So:
In both iOS and Vapor on the Server we can create a DispatchQueue
. In my case I am using a concurrent one that, in the critical part where token reading, fetching (if needed) and token writing happens, I use a barrier.
The barrier lets only one in and thus in this part everything is executed like a serial queue.
Hope this helps anybody that might come across the same issue
import Vapor
protocol TokenAccessor: RedisAccessor {
}
extension TokenAccessor {
/// Main convenience function that handles expiry, refetching etc
///
/// - Check if token was saved before
/// - We store the token in redis
/// - We use redis TTL feature to handle token expiry
///
func getToken(request: Request) throws -> Future<Token> {
print(":getToken(request:)")
let promise = request.eventLoop.newPromise(Token.self)
return request.withNewConnection(to: .redis) { redis in
let concurrentQueue = DispatchQueue(label: "com.queuename.gettoken",
attributes: .concurrent)
/// Making the concurrent queue serial because only one is allowed to fetch a new token at a time
concurrentQueue.async(flags: .barrier) {
let _ = redis.get(request.clientIdLastDigits, as: String.self).map(to: Void.self) { tokenOpt in
guard let accessToken = tokenOpt else {
try self.fetchNewToken(forRequest: request).do { newToken in
print("fetched a new token")
promise.succeed(result: newToken)
}.catch { error in
print("failed fetching a new token")
promise.fail(error: error)
}
return
}
print("got a valid token from redis")
let token = Token(client: request.clientIdLastDigits, token: accessToken, expiresIn: Date())
// return request.future(token)
promise.succeed(result: token)
}
}
return promise.futureResult
}
}
...
This is triggered in front of my methods via a middleware (so I don't need to think about it)
import Vapor
struct TokenMiddleware: Middleware, TokenAccessor {
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-ID")) else {
throw Abort(.badRequest, reason: "missing 'Client-ID' in header")
}
guard request.clientID.isEmpty == false else {
throw Abort(.badRequest, reason: "'Client-ID' in header is empty")
}
guard let _ = request.http.headers.firstValue(name: HTTPHeaderName("Client-Secret")) else {
throw Abort(.badRequest, reason: "missing 'Client-Secret' in header")
}
return try self.getToken(request: request).flatMap(to: Response.self) { token in
return try next.respond(to: request)
}
}
}
extension TokenMiddleware: Service {}
Upvotes: 1