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

Event-driven reaper #21

Closed
wants to merge 1 commit into from
Closed

Conversation

DaveCTurner
Copy link

Hi,

I'd like to use the resource-pool package, but the polling model for the reaper doesn't work well with what I'm trying to do. Running the reaper every second is too frequent, but simply decreasing the polling frequency means that stale resources will hang around for too long.

I've made an event-driven reaper in the past as part of another project and it worked well before, and I've now extracted this basic functionality into the alarmclock package.

Would you consider a change to resource-pool to make it use this event-driven model rather than the current polling one? I will prepare a pull request if so.

I'm willing to believe that alarmclock is not suitable in its current form, or has some subtle bug or performance issue, so if you have comments for how it could be improved then I'd be glad to hear them.

Cheers,

@basvandijk
Copy link
Collaborator

An event-driven reaper sounds like a good idea. I haven't looked closely at the implementation of alarmclock yet but I noticed a potential dead-lock issue:

setAlarmVar :: MVar UTCTime -> UTCTime -> IO ()
setAlarmVar mv wakeUpTime = tryTakeMVar mv >>= \case
  Nothing          -> putMVar mv      wakeUpTime
  Just wakeUpTime' -> putMVar mv (min wakeUpTime wakeUpTime')

What happens when another thread executes and completes setAlarmVar after tryTakeMVar delivered Nothing? I think the putMVar will result in a dead-lock then.

@DaveCTurner
Copy link
Author

The reaper thread itself is also repeatedly calling takeMVar, so isn't it OK at least until after calling destroyAlarmClock?

That jazz was to avoid a rare deadlock that took weeks to track down, but which I now think cannot occur for other reasons, so I will try and simplify it.

@DaveCTurner
Copy link
Author

Ok, I've made that simplification to alarmclock, which led me to use a TBMQueue of length 1 rather than my own hand-rolled thing.

@DaveCTurner
Copy link
Author

A starter for ten is at DaveCTurner@d434623

Comments welcomed. Issues I'm aware of are:

  • no labelling of the reaper thread (easy enough to replace)
  • no mask/restore for async exceptions (is that still necessary now no async exceptions are thrown?)
  • I do not like the clunky IORef-based knot-tying used to pass the alarmclock to the LocalPools and vice versa (but lack a bright idea on how that might be fixed without more major changes).

I'm concerned that I don't fully understand the performance constraints so may have done something awful in terms of performance.

@DaveCTurner
Copy link
Author

Now a PR.

Have been having trouble with high quiescent CPU usage in a low-usage-but-large-heap service caused by the idle GC which we've traced back to using resource-pool. Because of the large heap the idle GC was expensive, and because of the reaper thread waking up every second the idle GC was also running every second.

Switching off the idle GC cuts the quiescent CPU usage from 30% down to 0.3% which makes the difference between having to use multiple t2.large instances and a single t2.micro. This PR gives a similar improvement by only running the reaper when there's something to reap.

@winterland1989
Copy link

winterland1989 commented Jul 5, 2016

Is this package maintained by @bos anymore? your code looks good to me, can you contact @bos to review this?

@basvandijk
Copy link
Collaborator

I think I should be maintaining pool but unfortunately I don't have much time to spend on it these days. Maybe @bos can be convinced to also make @DaveCTurner a maintainer.

I did review the PR and except for the following it looks good to me:

In the alarmclock package there looks to be the possibility for a deadlock in newAlarmClock'. If an exception gets raised in the forked thread before the finaliser gets registered the joinVarwill never be put anymore and then the readMVar will deadlock. A better version would mask asynchronous exceptions before forking:

newAlarmClock' :: TimeScale t => (AlarmClock t -> t -> IO ()) -> IO (AlarmClock t)
newAlarmClock' onWakeUp = do
  joinVar <- newEmptyMVar
  ac <- atomically $ AlarmClock (readMVar joinVar) <$> newTVar AlarmNotSet <*> newTVar False
  void $ mask $ \restore -> forkIO $ restore (runAlarmClock ac (onWakeUp ac)) 
                                       `finally` putMVar joinVar ()
  return ac

An even better alternative is to use either the async or threads package.

@DaveCTurner
Copy link
Author

Good point, I'll fix that.

@DaveCTurner
Copy link
Author

Ok, fixed in alarmclock-0.4.0.2 by using the async package.

Also, apropos of #27, this version of alarmclock supports a system monotonic clock from the clock package as well as UTCTime. I agree that this would be preferable to using UTCTime for reasons of correctness.

@basvandijk
Copy link
Collaborator

@DaveCTurner thanks! Could you please bundle the commits in this PR as a single one? That makes the history a bit easier to read.

@DaveCTurner
Copy link
Author

Sure, there you go.

@DaveCTurner
Copy link
Author

Tidying up by closing some of my stale PRs. Sorry for the noise.

@DaveCTurner DaveCTurner closed this Nov 8, 2017
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.

3 participants