Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix schedule(after:interval:leeway:) disposal #584

Merged
merged 10 commits into from
Jan 5, 2018

Conversation

jjoelson
Copy link
Contributor

Addresses #582. Retain timers in QueueScheduler so that they will not be cancelled if the user doesn’t retain the returned Disposable.

Note that the test leaks a timer, but I'm not sure how else to test that the timer continues running even when the Disposable is not retained.

Checklist

  • Updated CHANGELOG.md.

Retain timers in `QueueScheduler` so that they will not be cancelled if the user doesn’t retain the returned `Disposable`.
Copy link
Member

@andersio andersio left a comment

Choose a reason for hiding this comment

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

Thanks for the PR. 😸

The timers would be released upon deinitialisation of the scheduler, so there should have no leakage AFAICU.

expect(false).to(equal(true), description: "timer not cancelled on disposal")
}

scheduler.schedule(after: Date(), interval: .milliseconds(10), leeway: .seconds(0)) {
Copy link
Member

@andersio andersio Dec 28, 2017

Choose a reason for hiding this comment

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

You might scope the scheduling call in a function, so that the disposable is guaranteed to be deinitialised.

var count = 0
let timesToIncrement = 3

// Start two repeating timers, dispose the first, and ensure only the second runs.
Copy link
Member

Choose a reason for hiding this comment

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

It'd probably be better off as two separate test cases. One for cancellation, while another for not retaining the disposable.

@@ -193,8 +193,14 @@ public final class QueueScheduler: DateScheduler {

public let queue: DispatchQueue

private let lock: Lock

private var timers: [DispatchSourceTimer]
Copy link
Member

Choose a reason for hiding this comment

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

You may use Atomic. I'd prefer Set which fits the scenario here (insert/removal only).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DispatchSourceTimer isn't Hashable, so I think it would have to be an NSMutableSet. Is that an improvement over using a Swift array?

CHANGELOG.md Outdated
@@ -1,6 +1,8 @@
# master
*Please add new entries at the top.*

1. Fixed `schedule(after:interval:leeway:)` being cancelled when the returned `Disposable` is not retained.
Copy link
Member

Choose a reason for hiding this comment

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

Please add the PR no & your handle.

// Start two repeating timers, dispose the first, and ensure only the second runs.

disposable.inner = scheduler.schedule(after: Date(), interval: .milliseconds(10), leeway: .seconds(0)) {
expect(false).to(equal(true), description: "timer not cancelled on disposal")
Copy link
Member

Choose a reason for hiding this comment

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

You may replace this with fail("message").


expect(count) == 0

scheduler.queue.resume()
Copy link
Member

@andersio andersio Dec 28, 2017

Choose a reason for hiding this comment

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

For the cancellation test, I'd wait for 1.5-2x the interval after resuming the queue so that the test case could actually fail.

@andersio
Copy link
Member

andersio commented Dec 29, 2017

A private wrapper for DispatchSourceTimer would work. The hash value & the equality can be derived from the object identity with ObjectIdentifier and ===.

private let value: DispatchSourceTimer

fileprivate var hashValue: Int {
return value.hash
Copy link
Member

Choose a reason for hiding this comment

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

Please use ObjectIdentifier(value).hashValue. The Dispatch protocol hierarchies do not inherit from NSObjectProtocol on platforms w/o ObjC interop.

timers.modify { timers in
timers.append(timer)
timers.insert(wrappedTimer)
}

return AnyDisposable {
timer.cancel()

self.timers.modify { timers in
Copy link
Member

Choose a reason for hiding this comment

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

Could we weakify self here?

count += 1

if count == timesToRun {
disposable1.dispose()
Copy link
Member

Choose a reason for hiding this comment

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

This should be disposable2. The macOS CI seems failing because of this.

expect(count) == 0

scheduler.queue.resume()
expect{count}.toEventually(equal(timesToIncrement))
Copy link
Member

Choose a reason for hiding this comment

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

IIRC expect uses @autoclosure, so it is already implicitly captured.


// This expectation should take about 2.0 * interval to be fulfilled, and that's
// enough time to ensure that the first timer was actually cancelled.
expect{count}.toEventually(equal(timesToRun))
Copy link
Member

Choose a reason for hiding this comment

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

Ditto for @autoclosure.

@andersio
Copy link
Member

andersio commented Dec 29, 2017

Hmm

ReactiveSwiftTests.SchedulerSpec
  QueueScheduler__on_a_given_queue__should_repeatedly_run_actions_after_a_given_date_when_the_disposable_is_not_retained, expected to eventually equal <3>, got <4>

@jjoelson
Copy link
Contributor Author

I think I see the bug, but strange that it’s not failing on my machine. I guess there’s a race condition where the expectation sees that it got incremented to 3 and succeeds before it gets incremented that last time.

I will fix when I get back to my PC.

@andersio
Copy link
Member

CI has limited concurrency (1 logical processor IIRC).

Specify `pollInterval` on the asynchronous expectation to avoid false positive test success.
@jjoelson
Copy link
Contributor Author

jjoelson commented Jan 2, 2018

Is there a test still failing on Linux? I'm having a tough time understanding the log.

@jjoelson
Copy link
Contributor Author

jjoelson commented Jan 2, 2018

OK, I found the error on Linux:

/home/travis/build/ReactiveCocoa/ReactiveSwift/Sources/Scheduler.swift:185:10: error: cannot invoke initializer for type 'ObjectIdentifier' with an argument list of type '(DispatchSourceTimer)'
                return ObjectIdentifier(value).hashValue
                       ^
/home/travis/build/ReactiveCocoa/ReactiveSwift/Sources/Scheduler.swift:185:10: note: overloads for 'ObjectIdentifier' exist with these partially matching parameter lists: (AnyObject), (Any.Type)
                return ObjectIdentifier(value).hashValue
                       ^
/home/travis/build/ReactiveCocoa/ReactiveSwift/Sources/Scheduler.swift:193:20: error: binary operator '===' cannot be applied to two 'DispatchSourceTimer' operands
                return lhs.value === rhs.value
                       ~~~~~~~~~ ^   ~~~~~~~~~
/home/travis/build/ReactiveCocoa/ReactiveSwift/Sources/Scheduler.swift:193:20: note: overloads for '===' exist with these partially matching parameter lists: (AnyObject?, AnyObject?), (NSNull?, NSNull?), (AnyClass, AnyClass)
                return lhs.value === rhs.value

Will fix when I get home tonight.

`DispatchSourceTimer` doesn’t inherit from `NSObjectProtocol` on Linux. Basing hashing and equality on the wrapper is sufficient because the  same wrapper instance can be used insert and remove from the `Set` of retained timers.
@jjoelson
Copy link
Contributor Author

jjoelson commented Jan 3, 2018

Stumped by this one:

2018-01-02 23:49:31.857 xcodebuild[2551:9128] Error 2018-01-02 23:49:31.857 xcodebuild[2551:9128] Error Domain=IDETestOperationsObserverErrorDomain Code=6 "Early unexpected exit, operation never finished bootstrapping - no restart will be attempted" UserInfo={NSLocalizedDescription=Early unexpected exit, operation never finished bootstrapping - no restart will be attempted}

** TEST EXECUTE FAILED **

@mdiep
Copy link
Contributor

mdiep commented Jan 3, 2018

Usually that's downstream from whatever the actual error was.

@jjoelson
Copy link
Contributor Author

jjoelson commented Jan 3, 2018

Seems to be working now. Judging by this Stackoverflow thread, that's an error that can occur randomly.

@andersio andersio merged commit 8fa0921 into ReactiveCocoa:master Jan 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants