Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vapor 3: Eventloop bug detected when using wait()

I'm struggling with understanding how to perform a batch save of fetched objects and store them to the database. After I have stored the objects to the database I want to return the result of a query. I can't understand how to do this with EventLoopFuture since when I call .wait() I get the error message:

Precondition failed: BUG DETECTED: wait() must not be called when on an EventLoop.

As an example of my problem:

  • I need to fetch an entity from an external endpoint (let's say flights for an airport)
  • The result of that call needs to be saved to a database. If the flight exists in the database, it needs to be updated otherwise created.
  • When completed, a list of all the flights in from the database needs to be returned.

This is what I got so far, but which gives me the bug:

func flights(on conn: DatabaseConnectable, customerName: String, flightType: FlightType) throws -> Future<[Flight]> {

    return Airport.query(on: conn).filter(\.customerName == customerName).first().flatMap(to: [Flight].self) { airport in
      guard let airport = airport else {
        throw Abort(.notFound)
      }

      guard let airportId = airport.id else {
        throw Abort(.internalServerError)
      }

      // Update items for customer
      let fetcher: AirportManaging?

      switch customerName.lowercased() {
      case "coolCustomer":
        fetcher = StoreOneFetcher()
      default:
        fetcher = nil
        debugPrint("Unhandled customer to fetch from!")
        // Do nothing
      }

      let completion = Flight.query(on: conn).filter(\.airportId == airportId).filter(\.flightType == flightType).all

      guard let flightFetcher = fetcher else { // No customer fetcher to get from, but still return whats in the DB
        return completion()
      }

      return try flightFetcher.fetchDataForAirport(customerName, on: conn).then({ (flights) -> EventLoopFuture<[Flight]> in
        flights.forEach { flight in
          _ = try? self.storeOrUpdateFlightRecord(flight, airport: airport, on: conn).wait()
        }
        return completion()
      })
    }
  }

  func storeOrUpdateFlightRecord(_ flight: FetcherFlight, airport: Airport, on conn: DatabaseConnectable) throws -> EventLoopFuture<Flight> {
    guard let airportId = airport.id else {
      throw Abort(.internalServerError)
    }

    return Flight.query(on: conn).filter(\.itemName == flight.itemName).filter(\.airportId == airportId).filter(\.flightType == flight.type).all().flatMap(to: Flight.self) { flights in
      if let firstFlight = flights.first {
        debugPrint("Found flight in database, updating...")
        return flight.toFlight(forAirport: airport).save(on: conn)
      }

      debugPrint("Did not find flight, saving new...")
      return flight.toFlight(forAirport: airport).save(on: conn)
    }
  }

So the problem in on line _ = try? self.storeOrUpdateFlightRecord(flight, airport: airport, on: conn).wait(). I can't call wait() since it'll block the eventLoop but if I call map or flatMap I need in turn return an EventLoopFuture<U> (U being Flight), which I am totally uninterested in.

I want self.storeOrUpdateFlightRecord to be called and the result ignored. How can I do that?

like image 965
Paul Peelen Avatar asked Jan 27 '23 16:01

Paul Peelen


1 Answers

Yeah, you can't use .wait() on eventLoop.

In your case you could use flatten for batch operations

/// Flatten works on array of Future<Void>
return flights.map {
    try self.storeOrUpdateFlightRecord($0, airport: airport, on: conn)
        /// so transform a result of a future to Void
        .transform(to: ())
}
/// then run flatten, it will return Future<Void> as well
.flatten(on: conn).flatMap {
    /// then do what you want :)
    return completion()
}
like image 98
imike Avatar answered Feb 04 '23 14:02

imike