0

I have a very simple service built on a Vapor app. The service consumes data from another family of services. Obviously this is just the sort of application the map methods were made for.

All the callbacks in the chain execute, but the last EventLoopFuture in the chain never completes, causing my service to hang indefinitely.

This code makes one async call to get a session Id, then uses that data to check if a person is a member of a particular group. The calls to the other service return reasonable values - currently the second call always returns an error.

When I call this method, all the callbacks execute and behave as expected.

Note that in this code snippet I have "unchained" the stages of the sequence to try to shine light on where the problem is arising, but the overall behavior is not affected.

I have tried many different permutations of the chain, using flatMap, map, and flatMapError where appropriate, without altering the final result.

let authService = remoteApi.authServices();

// EventLoopFuture<String> with sessionId
let sessionIdFuture = authService.getSessionId(username: userName, password: password)

// EventLoopFuture<Bool> with whether the user is in a particular group
let gsFuture = sessionIdFuture.flatMap { sessionId -> EventLoopFuture<Bool> in
    let groupMemberService = remoteApi.groupMemberServices()
    return groupMemberService.personIsInGroup(sessionId: sessionId, groupId: groupId, userId: userId)
}

// EventLoopFuture<Bool> if the above has an error, just return false
let errorFuture = gsFuture.flatMapErrorThrowing { error in
    // This executes successfully!
    return false
}

// EventLoopFuture<String> with the return data 
let returnFuture = errorFuture.flatMapThrowing { isMember -> String in
    // This executes successfully!
    let response = PersonIsMemberResponse(isMember: isMember)
    if let json = self.encodeResponse(response) {
        print(json) // {"isMember": false}
        return json
    } else {
        throw Abort(.internalServerError, reason: "could not encode our own dang data type");
    }
}.always { result in
    // This executes!
    do {
        try remoteApi.shutdown()
    } catch {
        print(error)
    }
}

gsFuture.whenComplete { result in
    // This executes!
    print("gsFuture complete!")
}
errorFuture.whenComplete { result in
     // This executes!
   print("errorFuture complete!")
}
returnFuture.whenComplete { result in
    // This DOES NOT execute!
    print("returnFuture complete!")
}

I don't see how the last flatMapThrowing can be executed and return a value, then the future not complete. What am I missing?

AlBlue
  • 23,254
  • 14
  • 71
  • 91
Jerry
  • 3,391
  • 1
  • 19
  • 28
  • I just tried this code (with all the service calls stubbed out) and it works fine for me. But let's get to the bottom of this: Are you sure that `try remoteApi.shutdown()` isn't blocking? You could for example add a `print("still here")` after that line. If it's still running then, would you mind describing what `remoteApi.shutdown()` is doing. Is it maybe shutting down the EventLoopGroup or the Vapor Application or so? – Johannes Weiss Jan 18 '21 at 20:45
  • Oh, and is this Linux or macOS? If it's macOS, the output of `sample YourBinaryName` whilst in the stuck state would also help. – Johannes Weiss Jan 18 '21 at 20:46
  • I'm commenting here because SO tells me that it's not a good answer if you "answer" with questions :). – Johannes Weiss Jan 18 '21 at 20:47
  • Indeed it is blocking. It looks like it calls syncShutdown() on an `AsyncHTTPClient`. I know that it gets very angry if the client is deinit without shutdown getting called. I guess I need to work out the right lifecycle for that service. (It is part of a separate library.) Anyway, thanks! If you copy your question as an answer I can give you the coveted checkmark. – Jerry Jan 18 '21 at 21:20
  • Cool! The idea is that you start with just _one_ EventLoopGroup and you reuse it for everything: Vapor, AsyncHTTPClient, your NIO code, etc ... – Johannes Weiss Jan 18 '21 at 21:59
  • This EventLoopGroup you can start & shutdown in `main.swift` . Similarly, you could create AsyncHTTPClient in main.swift and hand it into everything. But yes, as you point out, if you figure out the lifecycle, I'm sure this problem will go away fast :). – Johannes Weiss Jan 18 '21 at 22:00
  • The challenge was finding a good way to create something that was persistent across requests in vapor, since the general pattern is to create extensions that define services and middleware as computed properties, while the service defined in my library should have the same lifespan as the app itself. But with your help, I got it. – Jerry Jan 19 '21 at 08:30

1 Answers1

1

As we figured out together in the comments, it looks like try remoteApi.shutdown() is blocking which prevents anything further from happening.

Johannes Weiss
  • 52,533
  • 16
  • 102
  • 136