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

Prevent execution of tasks on an already shutdown EL #1395

Merged
merged 5 commits into from
Mar 4, 2020

Conversation

realdoug
Copy link
Contributor

@realdoug realdoug commented Feb 13, 2020

Resolve #1383 ; crash if a task is attempted on a closed Event Loop

One note: I created some test cases here, but wasn't able to figure out how to integrate them into IntegrationTests but perhaps you can point me in the right direction and I'll add them.

Modifications:

Make SelectableEventLoop._scheduledTasks an optional, set it to null when shutting down and throw a precondition failure if task enqueuing is attempted while it is null (i.e. after shutdown).

Result:

see test cases

@swift-server-bot
Copy link

Can one of the admins verify this patch?

9 similar comments
@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@realdoug
Copy link
Contributor Author

@Lukasa @weissi Alrighty, made some updates to this one, lmk what you think.

public var safeToExecute: Bool {
if self.inEventLoop {
return true
}
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 this escape hatch is right. We can be in the event loop but at a stage where further enqueues of work will fail. In fact, the obvious situation where that happens is when the event loop is shut down. At that point we'll dequeue all pending tasks that have not yet executed and fail them. Failing those tasks dispatches callbacks, and those callbacks may try to call back around to add work to the task queue. That's a bad pattern, and we should catch it, but we won't if this hook is here.

I think we only want the external state check.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, I think we can do better.

The MultithreadedEventLoopGroup currently uses a _scheduledTasks PriorityQueue to store pending work items. This queue is guarded by _tasksLock. I propose that we wrap the PriorityQueue in a data structure that allows us to "scribble" it: to mark it permanently as unusable once we're sure we aren't going to touch it again.

We can do this just by making the PriorityQueue optional, I think, and force unwrapping it whenever we try to modify it. That will allow us to catch the case where tasks are enqueued too early. It doesn't give a good error message, but that might be fine.

If we really want a good error message, a custom enum that is basically an optional with the method we want would also work.

This avoids the need for a new computed property, and also avoids synchronisation concerns: we don't have to worry about communication between two locks. It also allows us to use the point in the actual code where we know no further tasks can execute (the defer block at the top of func run()) to guarantee that outcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, makes sense! Looking into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, pushed up a new approach with _scheduledTasks as an optional

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@weissi @Lukasa I think you could break this task into 3 parts:

  1. identify the minimum set of places where a check needs to be made before executing
  2. identify what to query as part of the check
  3. based on step 2, possibly write some data on close that can correctly fulfill the query

Since tasks can be enqueued from inside and outside the thread, I don't think there is any solution to step 1 that would allow us to use just internalState or just externalState safely.

My original approach branched on inEventLoop (though as @Lukasa pointed out I missed a test case), alternatively, current approach adds a bit of info that can be queried from tasksLock and thus all cases from step 1.

Copy link
Member

Choose a reason for hiding this comment

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

@realdoug hmm, I'm somewhat lost here. I'm pretty sure we can find out what state we're in wherever we are because:

  • if we're on the EventLoop, we can learn that we won't be processing new tasks if the internalState is noLongerRunning
  • if we're off the EventLoop, then we should be in .closing/.closed when we're shutting down

If we need another internal (or external) state, for example .aboutToExitThread or something, then I'm happy for you to add such a state. So for example at the very point where you currently set self._scheduledTasks = nil you could

assert(self.state == .noLongerRunning, "illegal state: \(self.state)")
self.state = . aboutToExitThread

Then in schedule0 you could

if self.inEventLoop {
    switch self.internalState {
        ...
    }
} else {
    switch self.externalState {
        ...
    }
}

and in those switches you can then do the appropriate checks depending on whether we're inside or outside the eventloop.

Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I think I got excited and wrote too much :) My only point was that there are two solutions: an if statement or an optional. Using just one of the two states won't work.

I pushed up a new version w/ an if statement inside _schedule0 based on your description. Let me know what you think.

@realdoug realdoug requested review from weissi and Lukasa February 21, 2020 15:43
@Lukasa Lukasa added the 🔨 semver/patch No public API change. label Feb 24, 2020
@Lukasa Lukasa added this to the 2.15.0 milestone Feb 24, 2020
Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Ok cool, I think I like this. I'd like to see what @weissi thinks.

@realdoug
Copy link
Contributor Author

Ok cool, I think I like this. I'd like to see what @weissi thinks.

Awesome! Happy to make updates if needed.

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

Looks like a great start, just a question regarding the optional

@@ -56,7 +56,7 @@ internal final class SelectableEventLoop: EventLoop {
/* private but tests */ internal let _selector: NIO.Selector<NIORegistration>
private let thread: NIOThread
@usableFromInline
internal var _scheduledTasks = PriorityQueue<ScheduledTask>(ascending: true)
internal var _scheduledTasks: PriorityQueue<ScheduledTask>? = PriorityQueue<ScheduledTask>(ascending: true)
Copy link
Member

Choose a reason for hiding this comment

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

@realdoug one question here: Why do we make this optional instead of say adding a state to the InternalState?

Copy link
Contributor

Choose a reason for hiding this comment

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

That was my suggestion: #1395 (comment)

// reserve the correct capacity so we don't need to realloc later on.
scheduledTasksCopy.reserveCapacity(_scheduledTasks.count)
while let sched = _scheduledTasks.pop() {
scheduledTasksCopy.reserveCapacity(_scheduledTasks!.count)
Copy link
Member

Choose a reason for hiding this comment

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

nit: self. missing in front of _scheduledTasks twice (this is some old code so it was missing previously too)

public var safeToExecute: Bool {
if self.inEventLoop {
return true
}
Copy link
Member

Choose a reason for hiding this comment

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

@realdoug hmm, I'm somewhat lost here. I'm pretty sure we can find out what state we're in wherever we are because:

  • if we're on the EventLoop, we can learn that we won't be processing new tasks if the internalState is noLongerRunning
  • if we're off the EventLoop, then we should be in .closing/.closed when we're shutting down

If we need another internal (or external) state, for example .aboutToExitThread or something, then I'm happy for you to add such a state. So for example at the very point where you currently set self._scheduledTasks = nil you could

assert(self.state == .noLongerRunning, "illegal state: \(self.state)")
self.state = . aboutToExitThread

Then in schedule0 you could

if self.inEventLoop {
    switch self.internalState {
        ...
    }
} else {
    switch self.externalState {
        ...
    }
}

and in those switches you can then do the appropriate checks depending on whether we're inside or outside the eventloop.

Does that make sense?

@realdoug realdoug requested review from weissi and Lukasa March 4, 2020 03:51
Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

Awesome, thank you so much. This is pretty much there, just a minor change required.

switch self.internalState {
case .exitingThread:
return false
default:
Copy link
Member

Choose a reason for hiding this comment

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

could you change this from default: to enumerating the cases explicitly like case .running..., .running..., .noLongerRunning? It's important that we get the compiler errors if we add a case.

@usableFromInline
internal var _validExternalStateToScheduleTasks: Bool {
switch self.externalState {
case .open, .closing:
Copy link
Member

Choose a reason for hiding this comment

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

could you change this from default: to enumerating the cases explicitly like case .running..., .running..., .noLongerRunning? It's important that we get the compiler errors if we add a case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@weissi ok done & done

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

Awesome, thank you!

@weissi
Copy link
Member

weissi commented Mar 4, 2020

@swift-nio-bot test this please

@weissi
Copy link
Member

weissi commented Mar 4, 2020

CC @Lukasa

@weissi
Copy link
Member

weissi commented Mar 4, 2020

@swift-nio-bot test this please

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@Lukasa Lukasa merged commit 51238f4 into apple:master Mar 4, 2020
weissi added a commit to weissi/swift-nio that referenced this pull request Mar 18, 2020
Motivation:

Since apple#1395, we crash when something is scheduled to an already shutdown
EventLoop. That's a good intent (apple#1383) but we feel it's a little too
risky right now.

Modifications:

Print an ERROR instead of just crashing.

Result:

Fewer crashes.
weissi added a commit to weissi/swift-nio that referenced this pull request Mar 18, 2020
Motivation:

Since apple#1395, we crash when something is scheduled to an already shutdown
EventLoop. That's a good intent (apple#1383) but we feel it's a little too
risky right now.

Modifications:

Print an ERROR instead of just crashing.

Result:

Fewer crashes.
Lukasa pushed a commit that referenced this pull request Mar 18, 2020
Motivation:

Since #1395, we crash when something is scheduled to an already shutdown
EventLoop. That's a good intent (#1383) but we feel it's a little too
risky right now.

Modifications:

Print an ERROR instead of just crashing.

Result:

Fewer crashes.
pull bot pushed a commit to scope-demo/swift-nio that referenced this pull request Mar 18, 2020
Motivation:

Since apple#1395, we crash when something is scheduled to an already shutdown
EventLoop. That's a good intent (apple#1383) but we feel it's a little too
risky right now.

Modifications:

Print an ERROR instead of just crashing.

Result:

Fewer crashes.
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NIO should assert/precondition is work is execute/submit/flatSubmitted so a closed EventLoop
4 participants