Commit 4bbc9cd2 authored by Tristan Rice's avatar Tristan Rice Committed by Facebook Github Bot

SmartExceptionTracer: get stack traces for smart exceptions

Summary:
The current folly ExceptionTracer only keeps the stack traces in thread local storage for as long as the exception is currently active or caught. When using folly futures we almost never handle exceptions within a catch block or even the same thread with thenError.

With C++ 11, we have exception_ptr which ref counts the exception so being able to keep the stack trace as long as something still holds a pointer to the exception is very useful. This allows us to get a stack trace for an exception returned by futures.

This tracer adds a callback that runs when an exception is thrown and captures the stack trace in a synchronized map. To manage the lifetime, we override the deleter function with our own wrapper that will remove the trace from the map when the exception is no longer needed.

Two things of note:
* We're attempting to allocate memory in the exception thrown callback so if we run out of memory this code won't be able to handle it and trigger a terminate via the noexcept.
* Every exception thrown requires acquiring a lock. We should be able to mitigate this via folly::ConcurrentHashMap if it becomes a problem. Initial benchmarking seems fine as is. I don't believe it'll become a problem since we only throw exceptions in exceptional cases and they shouldn't be performance sensitive.

Reviewed By: yfeldblum

Differential Revision: D16533296

fbshipit-source-id: 7d8832c24c79bde98d1bb213d4b70f259fb3d4bc
parent 3c2162df
/*
* Copyright 2019-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/experimental/exception_tracer/SmartExceptionTracer.h>
#include <folly/MapUtil.h>
#include <folly/ScopeGuard.h>
#include <folly/Synchronized.h>
#include <folly/experimental/exception_tracer/ExceptionTracerLib.h>
#include <folly/experimental/exception_tracer/StackTrace.h>
#include <folly/experimental/symbolizer/Symbolizer.h>
namespace folly {
namespace exception_tracer {
namespace {
struct ExceptionMeta {
void (*deleter)(void*);
StackTrace trace;
};
Synchronized<std::unordered_map<void*, ExceptionMeta>>& getMeta() {
// Leaky Meyers Singleton
static Indestructible<Synchronized<std::unordered_map<void*, ExceptionMeta>>>
meta;
return *meta;
}
void metaDeleter(void* ex) noexcept {
auto deleter = getMeta().withWLock([&](auto& locked) noexcept {
auto iter = locked.find(ex);
auto* innerDeleter = iter->second.deleter;
locked.erase(iter);
return innerDeleter;
});
// If the thrown object was allocated statically it may not have a deleter.
if (deleter) {
deleter(ex);
}
}
// This callback runs when an exception is thrown so we can grab the stack
// trace. To manage the lifetime of the stack trace we override the deleter with
// our own wrapper.
void throwCallback(
void* ex,
std::type_info*,
void (**deleter)(void*)) noexcept {
// Make this code reentrant safe in case we throw an exception while
// handling an exception. Thread local variables are zero initialized.
static FOLLY_TLS bool handlingThrow;
if (handlingThrow) {
return;
}
SCOPE_EXIT {
handlingThrow = false;
};
handlingThrow = true;
getMeta().withWLockPtr([&](auto wlock) noexcept {
// This can allocate memory potentially causing problems in an OOM
// situation so we catch and short circuit.
try {
auto& meta = (*wlock)[ex];
auto rlock = wlock.moveFromWriteToRead();
// Override the deleter with our custom one and capture the old one.
meta.deleter = std::exchange(*deleter, metaDeleter);
ssize_t n = symbolizer::getStackTrace(meta.trace.addresses, kMaxFrames);
if (n != -1) {
meta.trace.frameCount = n;
}
} catch (const std::bad_alloc&) {
}
});
}
struct Initialize {
Initialize() {
registerCxaThrowCallback(throwCallback);
}
};
Initialize initialize;
} // namespace
ExceptionInfo getTrace(const std::exception_ptr& ptr) {
try {
// To get a pointer to the actual exception we need to rethrow the
// exception_ptr and catch it.
std::rethrow_exception(ptr);
} catch (std::exception& ex) {
return getTrace(ex);
} catch (...) {
return ExceptionInfo();
}
}
ExceptionInfo getTrace(const exception_wrapper& ew) {
if (auto* ex = ew.get_exception()) {
return getTrace(*ex);
}
return ExceptionInfo();
}
ExceptionInfo getTrace(const std::exception& ex) {
ExceptionInfo info;
info.type = &typeid(ex);
getMeta().withRLock([&](auto& locked) noexcept {
auto* meta = get_ptr(locked, (void*)&ex);
// If we can't find the exception, return an empty stack trace.
if (!meta) {
return;
}
info.frames.assign(
meta->trace.addresses, meta->trace.addresses + meta->trace.frameCount);
});
return info;
}
} // namespace exception_tracer
} // namespace folly
/*
* Copyright 2019-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <folly/ExceptionWrapper.h>
#include <folly/experimental/exception_tracer/ExceptionTracer.h>
namespace folly {
namespace exception_tracer {
ExceptionInfo getTrace(const std::exception_ptr& ex);
ExceptionInfo getTrace(const std::exception& ex);
ExceptionInfo getTrace(const exception_wrapper& ew);
} // namespace exception_tracer
} // namespace folly
/*
* Copyright 2019-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/experimental/exception_tracer/SmartExceptionTracer.h>
#include <folly/portability/GTest.h>
using namespace folly::exception_tracer;
[[noreturn]] FOLLY_NOINLINE void testThrowException() {
throw std::runtime_error("test exception");
}
TEST(SmartExceptionTracer, ExceptionPtr) {
auto ew = folly::try_and_catch<std::exception>(testThrowException);
auto info = getTrace(ew.to_exception_ptr());
std::ostringstream ss;
ss << info;
ASSERT_TRUE(ss.str().find("testThrowException") != std::string::npos);
}
TEST(SmartExceptionTracer, ExceptionWrapper) {
auto ew = folly::try_and_catch<std::exception>(testThrowException);
auto info = getTrace(ew);
std::ostringstream ss;
ss << info;
ASSERT_TRUE(ss.str().find("testThrowException") != std::string::npos);
}
TEST(SmartExceptionTracer, StdException) {
try {
testThrowException();
} catch (std::exception& ex) {
auto info = getTrace(ex);
std::ostringstream ss;
ss << info;
ASSERT_TRUE(ss.str().find("testThrowException") != std::string::npos);
}
}
TEST(SmartExceptionTracer, EmptyExceptionWrapper) {
auto ew = folly::exception_wrapper();
auto info = getTrace(ew);
std::ostringstream ss;
ss << info;
ASSERT_TRUE(
ss.str().find("Exception type: (unknown type) (0 frames)") !=
std::string::npos);
}
TEST(SmartExceptionTracer, InvalidException) {
try {
throw 10;
} catch (...) {
auto info = getTrace(std::current_exception());
std::ostringstream ss;
ss << info;
ASSERT_TRUE(
ss.str().find("Exception type: (unknown type) (0 frames)") !=
std::string::npos);
}
}
TEST(SmartExceptionTracer, UnthrownException) {
std::runtime_error ex("unthrown");
auto info = getTrace(ex);
std::ostringstream ss;
ss << info;
ASSERT_TRUE(
ss.str().find("Exception type: std::runtime_error (0 frames)") !=
std::string::npos);
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment