Recently I've come to discover that I've inherited one of the internal auxilliary programs used. I've made a few minor fixes and features to improve it in the past, but now I've been given a major feature that requires significant overhaul in order to implement.
This code has a recurring issue with overzealous try/catch patterns, catching all exceptions and writing them to log before quietly continuing. I've been working around these to avoid accidentally breaking anything, only narrowing them if I know all exceptions that could plausibly be thrown within the try block.
However, one occurrance includes a sizeably meaty finally block. It looks approximately like this:
void BigMethod() {
bool stuff_found = false;
bool lockTaken = false;
// other locals that are referenced in finally block...
try {
Monitor.TryEnter(m_lock, 500, ref lockTaken);
if (!lockTaken) {
Logger.WriteLine("Failed to acquire lock");
return; // This used to be a throw-as-goto
}
if (m_queue.TryDequeue(out StuffInfo inf))
return;
stuff_found = true;
// ...
// 200 lines of BigMethod stuff, down from previously 350 lines
// This includes creating some files and dirs, and several returns
// that used to be throw-as-goto
} catch (Exception _e) {
Logger.WriteLine("Error{0}", _e);
// This was clearly originally intended to be a catch-as-label but
// I'm unsure what I'd break if I were to remove it.
} finally {
if (stuff_found) {
try {
// Delete certain leftover files from outer try
} catch (Exception e) {
Logger.WriteLine("Error:{0}", e);
}
// PROBLEM: More stuff that could also plausibly throw,
// including logging and sending an email of the failure
}
if (lockTaken)
Monitor.Exit(m_lock); // FIXME: This might get skipped
}
}
The problem is that this finally block mixes together synchronization behavior (lock taking and releasing), file cleanup, error handling, and conclusion of the results of BigMethod, all at once.
The most immediate problem here is if any of the code at the PROBLEM:
comment throws, the lock is never released.
Monitor.Exit
needs to be called if lockTaken
is true, no matter what. It also has to occur after the PROBLEM
lines and activity, since this lock seems to prevent the creation and deletion of a file from multiple threads. (While I'm not 100% sure this method is called from multiple threads, it is a possibility.)
I can't just move the PROBLEM
code into the try block, since it does some file cleanup after a failure, and moving it to try would mean that cleanup doesn't happen if something throws. Only thing I can think of to ensure the lock is consistently released is to wrap the contents of the finally in yet another try/finally, or the entire body of the method, and that just seems really icky.
How can I reasonably fix this (at the very least the uncertain Monitor.Exit
) cleanly?
try
ing every time. Email exceptions could be caught higher up the call stack in a catch-all clause.