Skip to content
New issue

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

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

Already on GitHub? # to your account

Add an always method on EventLoopFuture #981

Merged
merged 9 commits into from
May 15, 2019
15 changes: 15 additions & 0 deletions Sources/NIO/EventLoopFuture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1267,3 +1267,18 @@ func executeAndComplete<Value>(_ promise: EventLoopPromise<Value>?, _ body: () t
promise?.fail(e)
}
}


extension EventLoopFuture {
/// Adds an observer callback to this `EventLoopFuture` that is called when the
/// `EventLoopFuture` has any result.
///
/// - parameters:
/// - callback: the callback that is called when the `EventLoopFuture` is fulfilled.
/// - returns: the current `EventLoopFuture`
public func always(_ callback: @escaping () -> Void) -> EventLoopFuture<Value> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa do you think we should give users the result here? For whenComplete we hand them the result so why not here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I just came across a case where having the above with the result (or alwaysSuccess/alwaysError) would have been helpful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I mind particularly. Arguably there are a family of functions here (giving them the name always for now though the compiler wouldn't like it much) created by twiddling both the return value of the function and the return value of the closure it accepts:

  1. Future<T>.always(_: () -> Void) -> Future<T>
  2. Future<T>.always(_: () -> Void) -> Void
  3. Future<T>.always(_: (Result<T, Error>) -> Void) -> Future<T>
  4. Future<T>.always(_: (Result<T, Error>) -> Void) -> Void
  5. Future<T>.always<U>(_: (Result<T, Error>) -> U) -> Future<U>
  6. Future<T>.always<U>(_: (Result<T, Error>) -> Future<U>) -> Future<U>
  7. Future<T>.always(_: (Result<T, Error>) -> Future<Void>) -> Future<T>

This proposal covers (1). (4) is currently spelled whenComplete. (3) is what you're proposing instead, and can do everything (1) can do at the cost of being a bit noisier in the code when you want (1). (2) is what whenComplete used to be in NIO 1, before we had a Result type to use.

(5) is arguably a function that would be called mapBoth, and (6) would be flatMapBoth by analogy. (7) is akin to (4) but allows returning a Future while persisting the result of the computation.

I think there could be a case made for providing all of these functions: the biggest challenge would be providing descriptive names, as we cannot rely on the compiler to provide overload resolution. (7) is the least obviously useful.

I think we should stick with always being the name for this function. We should also provide (3) and call it something like alwaysWithResult. Otherwise we'll force a lot of people to write always { _ in } and that kinda sucks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa Mostly agree, just that for whenComplete we decided to only offer the 'with result' case (but we didn't name it whenCompleteWithResult). Therefore, I'm not sure if for always we should do always and alwaysWithResult... But I agree that the _ in kinda sucks and obviously that the compiler won't like overloading at all. I'd probably still go for consistency with whenComplete.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion here, I'm happy to follow your preference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi @Lukasa If there's a version providing the result then I'd think alwaysWithSuccess and alwaysWithError would probably be requested too? Providing a value like this is pretty close to Java's peek.

If that's the case then maybe these are distinct from the always as it stands at the moment?

Future<T>.always(_: () -> Void) -> Future<T>
Future<T>.peek(_: (T) -> Void) -> Future<T>
Future<T>.peekError(_: (Error) -> Void) -> Future<T>
Future<T>.peekResult(_: (Result<T, Error>) -> Void) -> Future<T>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I kind of like the peeks. My only concern with leaving always as is would be always not giving you a value but whenComplete giving you the result. Just looks a bit inconsistent, that's all :). Especially that in NIO 1 we had whenComplete giving you nothing and we switched to whenComplete giving you the result for NIO2 :).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, it's probably best to be consistent with whenComplete even though I prefer the NIO 1 version of it! I think I ignore the result more often than not...

I would vote to change it back in some future version of NIO and introduce whenResult to be closer in name to the other methods which provide a Result to the callback. The above would then be analogues of the when{Complete,Success,Failure,Result} which return the future.

Copy link
Member

@weissi weissi Apr 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. when{Complete,Success,Failure,Result} make sense to me. Filed #986

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Palleas just talked to @Lukasa about this. Let's add the Result<...> to the closure for always for consistency with whenComplete. The cleanup and providing the other missing methods can be done as #986 .

self.whenComplete { _ in callback() }
return self
}

}
2 changes: 2 additions & 0 deletions Tests/NIOTests/EventLoopFutureTest+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ extension EventLoopFutureTest {
("testWhenAllCompleteResultsWithFailuresStillSucceed", testWhenAllCompleteResultsWithFailuresStillSucceed),
("testWhenAllCompleteResults", testWhenAllCompleteResults),
("testWhenAllCompleteResolvesAfterFutures", testWhenAllCompleteResolvesAfterFutures),
("testAlways", testAlways),
("testAlwaysWithFailingPromise", testAlwaysWithFailingPromise),
]
}
}
Expand Down
40 changes: 40 additions & 0 deletions Tests/NIOTests/EventLoopFutureTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1015,4 +1015,44 @@ class EventLoopFutureTest : XCTestCase {
let results = try assertNoThrowWithValue(mainFuture.wait().map { try $0.get() })
XCTAssertEqual(results, [0, 1, 2, 3, 4])
}

struct DatabaseError: Error {}
struct Database {
let query: () -> EventLoopFuture<[String]>

var closed = false

init(query: @escaping () -> EventLoopFuture<[String]>) {
self.query = query
}

func runQuery() -> EventLoopFuture<[String]> {
return query()
}

mutating func close() {
self.closed = true
}
}

func testAlways() throws {
let group = EmbeddedEventLoop()
let loop = group.next()
var db = Database { loop.makeSucceededFuture(["Item 1", "Item 2", "Item 3"]) }

XCTAssertFalse(db.closed)
let _ = try assertNoThrowWithValue(db.runQuery().always { db.close() }.map { $0.map { $0.uppercased() }}.wait())
XCTAssertTrue(db.closed)
}

func testAlwaysWithFailingPromise() throws {
let group = EmbeddedEventLoop()
let loop = group.next()
var db = Database { loop.makeFailedFuture(DatabaseError()) }

XCTAssertFalse(db.closed)
let _ = try XCTAssertThrowsError(db.runQuery().always { db.close() }.map { $0.map { $0.uppercased() }}.wait()) { XCTAssertTrue($0 is DatabaseError) }
XCTAssertTrue(db.closed)

}
}