-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
thread: add Windows implementation #5072
Conversation
Why not use std::thread? |
Historically, std::thread eats exceptions and prints useless crash/abort messages. |
@jmarantz @curiouserrandy can you take a look to see how this might fit in with the thread factory stuff? Not sure if it matters or not. |
@eziskind is working on the thread factory. |
@sesmith177 can you PTAL at #5055 ? It probably doesn't make sense to have both of these in flight at once. |
I don't think I have relevant expertise on this PR. I'll unassign myself (if I can figure out how); please feel free to assign me or ping me if there's some reason I should be included. |
@jmarantz not a problem, we'll wait for that to be merged and then rework this PR |
This looks good so far; might get a little simpler if you are adjusting only the implementation of ThreadSystem; hopefully #5055 will get merged soon. |
For the record, I am against singletons in general, and believe it's more maintainable to pass context objects through the call/instantiation stack, as is done in Envoy production code. However the test system employs a deep system of mocks that are constructed without context. As a result, any new context that may be needed in the future (e.g. SymbolTable in #4980, ThreadSystem in #5055 and #5072 and FileSystem in #5031) need a mechanism to plumb data into the system as instantiated during tests. I believe Api::OsSysCalls() is another example that might benefit from this. This class provides a mechanism to have a bounded-scope singleton, such that anywhere an instance of a type T is desired, the same instance will be available. Once all references to a Global go out of scope, the shared instance is destroyed and the pointer to it nulled. Thus, this singleton pattern has the property that between tests, the state will be completely reset, as long as there are no memory leaks. The new class, Test::Global, creates a potential conflict with tests that had declared using testing::Test;, so this PR also cleans those up and adds a format check to avoid having that pattern re-occur. Risk Level: the main risk here is that this class will be abused and used in lieu of proper object passing when that's most appropriate. However, the new library is declared as a bazel test library and is in the test/... tree, so hopefully -- with proper code reviews, the usage will be limited. Testing: //test/... Signed-off-by: Joshua Marantz <jmarantz@google.com>
#5055 is now merged; and might make this change to support an alternate thread-system much more surgical. Check out Thread::threadFactoryForTest() in test/test_common/utility.cc which you could probably just ifdef. And check out the origin of the production thread-factory in source/exe/main_common.cc in the main_common() function. You could make your windows binary share MainCommonBase, but split off the main_common() function to another file which is not compiled in windows, and instantiate an alternate ThreadFactory in your new main(). |
|
||
ThreadImpl::ThreadImpl(std::function<void()> thread_routine) : thread_routine_(thread_routine) { | ||
RELEASE_ASSERT(Logger::Registry::initialized(), ""); | ||
thread_handle_ = ::CreateThread(NULL, 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drive-by question: should this be _beginthreadex? (from the docs: A thread in an executable that calls the C run-time library (CRT) should use the _beginthreadex
)
Signed-off-by: Sam Smith <sesmith177@gmail.com>
include/envoy/thread/thread.h
Outdated
@@ -18,6 +37,8 @@ class Thread { | |||
* Join on thread exit. | |||
*/ | |||
virtual void join() PURE; | |||
|
|||
virtual ThreadHandle handle() const PURE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to expose the thread handle? I don't see this method getting called anywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have some code in our fork (specifically the Windows version of Filesystem::WatcherImpl) that needs access to the thread handle. We haven't PR'd those changes back upstream yet but plan to eventually
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this PR can we remove the handle, as it's not used yet?
Then in the next PR we can discuss the handle and its usage in FilesystemImpl. I predict that I'm would suggest you make a windows-specific subclass of that too, but can't tell until I see it :)
In general I hope we can approach portability with injection of alternate class implementations rather than ifdefs, particularly ifdefs in an interface header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a problem.
The specific usage is our workaround for the fact that libevent can only watch sockets on Windows (as opposed to arbitrary). But we'll get to that in a future PR
Signed-off-by: Sam Smith <sesmith177@gmail.com>
Signed-off-by: Matthew Horan <mhoran@pivotal.io> Signed-off-by: Sam Smith <sesmith177@gmail.com>
namespace Envoy { | ||
namespace Thread { | ||
|
||
ThreadImpl::ThreadImpl(std::function<void()> thread_routine) : thread_routine_(thread_routine) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this is the exact same name as the posix one, so the decision is made at link-time, and if you link both files in you'd get a duplicate symbol conflict.
That's not really a concern for windows vs posix, but you may also want an alternate thread system even if more than one is possible on the platform. That's something that we need (google has some custom thread infrastructure).
I'll give you another example: you may want a stats-collecting thread-factory decorator that keeps a gauge of the active thread-count or other metrics. I'd keep that separate from the platform-specific impls, so (e.g.) you could use that to decorate either windows or posix one.
Can we inject the right platform-specific ThreadFactory where needed rather than having this be a link-time decision?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, we broke out separate main.cc files for Windows / POSIX and inject the correct thread factory there
Signed-off-by: Matthew Horan <mhoran@pivotal.io> Signed-off-by: Sam Smith <sesmith177@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking a lot better; left a few more comments.
source/exe/main_common_base.cc
Outdated
} | ||
|
||
} // namespace Envoy | ||
} // namespace Envoy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add newline
test/test_common/utility.cc
Outdated
@@ -347,7 +347,11 @@ MockedTestAllocator::~MockedTestAllocator() {} | |||
namespace Thread { | |||
|
|||
ThreadFactory& threadFactoryForTest() { | |||
static ThreadFactoryImpl* thread_factory = new ThreadFactoryImpl(); | |||
#if !defined(WIN32) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flip if/else and invert the conditional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far, we've been following this pattern (if !defined(WIN32)
) in our PRs. We don't have a strong opinion on which way the conditional should be written, but do think we should be consistent about which way we pick.
There's about ~8 other places we'd have to change as well, all but 1 in test code. If we want to make the change as part of this PR, that's fine with us.
include/envoy/thread/thread.h
Outdated
namespace Envoy { | ||
namespace Thread { | ||
|
||
#if !defined(WIN32) | ||
using ThreadId = int32_t; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can avoid ifdefs in the interface here and instead have ThreadId be an abstract base class, which I'd give two or three methods:
-- compare to another ThreadID
-- determine if it's the current thread
-- serialize to a string for debug purposes
in the past I've done this this way: https://github.com/apache/incubator-pagespeed-mod/blob/20fbc5575c8fc825a137f6f1b2015829a9768f81/pagespeed/kernel/base/thread_system.h#L117
Note referenced there: https://linux.die.net/man/3/pthread_self
POSIX.1 allows an implementation wide freedom in choosing the type used to represent a
thread ID; for example, representation using either an arithmetic type or a structure is
permitted. Therefore, variables of type pthread_t can't portably be compared using the
C equality operator (==); use pthread_equal(3) instead.
#elif defined(__APPLE__) | ||
uint64_t tid; | ||
pthread_threadid_np(NULL, &tid); | ||
return static_cast<int32_t>(tid); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is probably pre-existing, but IMO further evidence that I think we want an abstract base class for ThreadID.
I could see also a subclass for Apple and a subclass for Linux I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this is pre-existing. We will probably hold off on splitting Apple from Linux, as we don't really have a set up for testing on OSX
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed; no need to add complexity here.
*/ | ||
class ThreadImplPosix : public Thread { | ||
public: | ||
ThreadImplPosix(std::function<void()> thread_routine); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I suspect you don't need ThreadImplPosix to be exposed in the header -- it can be private to the ThreadFactoryImplPosix impl and in the cc file. This is not a big deal but I feel like it's nice to minimize what has to go into h files, for compile-speed and for human understanding of interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that we don't need ThreadImplPosix exposed in the header, but we do need ThreadImplWin32 exposed (so one of our Windows-specific classes can access data we don't want to put on the interface). It may be more consistent to have both exposed rather than one exposed and one placed in the cc file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK leave it exposed then, this is not a big deal, and if we do subclass Apple vs Linux in the future we'll need the shared posix impl exposed at least as a class declaration.
Signed-off-by: Sam Smith <sesmith177@gmail.com>
Signed-off-by: Sam Smith <sesmith177@gmail.com>
Signed-off-by: Sam Smith <sesmith177@gmail.com>
ping when ready for another round. |
Signed-off-by: Yechiel Kalmenson <ykalmenson@pivotal.io> Signed-off-by: Sam Smith <sesmith177@gmail.com>
@jmarantz I think we're good to go for another round. |
test/test_common/utility.cc
Outdated
const int rc = ::symlink(target.c_str(), link.c_str()); | ||
ASSERT_EQ(rc, 0); | ||
#else | ||
#if defined(WIN32) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've seen you use _MSC_VER
elsewhere and WIN32 here. Why's that? If possible can we use just one condition?
Also why #if defined(WIN32)
rather than the more concise #ifdef WIN32
?
Usually I see the define(xxx)
form in the context of a more complex logical expression: if defined(WIN32) || defined(APPLE)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_MSC_VER
is automatically defined if the compiler is MSVC, while we manually define WIN32
in our Bazel setup. The reason for the distinction is that some changes (e.g. the PACKED_STRUCT definition) depend on the specific compiler being used (MSVC vs. mingw vs. clang, etc.), while others depend on the specific platform (Windows vs. Linux, etc.)
Of course, right now, we only plan on doing the work to build Envoy with MSVC on Windows, but we could imagine a situation where someone wanted to build Envoy on Windows with a different compiler.
As for #if defined
vs #ifdef
, no specific reason
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _MSC_VER
vs WIN32
came from feedback on a previous PR: #4645 (comment)
// use MoveFileEx, since ::rename will not overwrite an existing file. See | ||
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/rename-wrename?view=vs-2017 | ||
const BOOL rc = ::MoveFileEx(old_name.c_str(), new_name.c_str(), MOVEFILE_REPLACE_EXISTING); | ||
ASSERT_NE(0, rc); | ||
#else | ||
const int rc = ::rename(old_name.c_str(), new_name.c_str()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all this should be inside the filesystem abstraction (include/envoy/filesystem/filesystem.h). Can you at least add a TODO for now?
Note that the Filesystem and ThreadFactory are both part of Api::Api so you can see the convergence of all this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this broken out here since we didn't want to add production code that is only called from tests. If moving this code into the filesystem abstraction is the way to go, we will do that as a part of the filesystem PR that we are currently working on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Leaving these in the test-utility makes sense as long as the functionality is not needed in production.
source/exe/main_common.cc
Outdated
: options_(argc, argv, &MainCommon::hotRestartVersion, spdlog::level::info), | ||
base_(options_, real_time_system_, default_test_hooks_, prod_component_factory_, | ||
std::make_unique<Runtime::RandomGeneratorImpl>(), thread_factory_) {} | ||
std::make_unique<Runtime::RandomGeneratorImpl>(), platform_main.threadFactory()) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this fits into createApiForTest but may require some auditing. The caller of that should ensure the returned Api::ApiPtr lives as long as needed.
Or is it a problem of some sort to have the ctor/dtor called on every unit test within a single process?
source/exe/posix/platform_main.h
Outdated
#ifndef __APPLE__ | ||
// absl::Symbolize mostly works without this, but this improves corner case | ||
// handling, such as running in a chroot jail. | ||
absl::InitializeSymbolizer(argv[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we have posix and windows implementions inherit from a common base-class that calls the symbolizer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...alternatively, you could just leave the call to InitializeSymbolizer in main() where it is now, and then you don't need to pass in argc,argv to PlatformMain.
All my above suggeestions are for TODOs in this PR; the only thing I'd like to see changed in this PR is consistent use of a |
/retest |
🔨 rebuilding |
We will replace |
Signed-off-by: Natalie Arellano <narellano@pivotal.io> Signed-off-by: Sam Smith <sesmith177@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great; just a couple of nits left. Let's get this done.
*/ | ||
class ThreadFactoryImplPosix : public ThreadFactory { | ||
public: | ||
ThreadFactoryImplPosix() {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
omit empty ctor.
source/exe/posix/platform_main.h
Outdated
#ifndef __APPLE__ | ||
// absl::Symbolize mostly works without this, but this improves corner case | ||
// handling, such as running in a chroot jail. | ||
absl::InitializeSymbolizer(argv[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...alternatively, you could just leave the call to InitializeSymbolizer in main() where it is now, and then you don't need to pass in argc,argv to PlatformMain.
Signed-off-by: Sam Smith <sesmith177@gmail.com>
@jmarantz added changes as requested |
Hi -- I'm a relatively new maintainer and don't totally understand the flow yet. It looks like I can't merge this PR because there are still requested changes (although @htuch has LGTM'd so I think I should be able to merge). See #5072 (review) It looks like you've addressed @htuch's suggestions but maybe you didn't click 'resolve conversation' for each comment? |
I think (though I don't know for sure) @htuch may need to explicitly approve the changes as well |
@sesmith177 @jmarantz I'll take another pass now and merge if it looks all good! |
source/exe/posix/platform_main.h
Outdated
|
||
namespace Envoy { | ||
|
||
class PlatformMain { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still uncomfortable with this naming. PlatformMain
to me should be a method, it's essentially an entry point, not a noun. Can we just make this PlatformImpl
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM modulo this bikeshed :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, one nit.
@jmarantz see https://github.com/envoyproxy/envoy/blob/master/CONTRIBUTING.md#pr-review-policy-for-maintainers so I think we need @htuch's approval :)
source/common/api/api_impl.cc
Outdated
@@ -16,7 +16,7 @@ Impl::Impl(std::chrono::milliseconds file_flush_interval_msec, | |||
file_system_(file_flush_interval_msec, thread_factory, stats_store) {} | |||
|
|||
Event::DispatcherPtr Impl::allocateDispatcher(Event::TimeSystem& time_system) { | |||
return Event::DispatcherPtr{new Event::DispatcherImpl(time_system)}; | |||
return Event::DispatcherPtr{new Event::DispatcherImpl(time_system, *this)}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: make_unique
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks!
Needs master merge @sesmith177, will merge as soon as that happens. |
Signed-off-by: Sam Smith <sesmith177@gmail.com>
Also, removes thread safety assertions due to changes introduced in (envoyproxy#5072); it is no longer possible to statically call functions to obtain/check the current thread ID. I will reintroduce these checks once static accessors are available again. Signed-off-by: Andres Guedez <aguedez@google.com>
For the record, I am against singletons in general, and believe it's more maintainable to pass context objects through the call/instantiation stack, as is done in Envoy production code. However the test system employs a deep system of mocks that are constructed without context. As a result, any new context that may be needed in the future (e.g. SymbolTable in envoyproxy#4980, ThreadSystem in envoyproxy#5055 and envoyproxy#5072 and FileSystem in envoyproxy#5031) need a mechanism to plumb data into the system as instantiated during tests. I believe Api::OsSysCalls() is another example that might benefit from this. This class provides a mechanism to have a bounded-scope singleton, such that anywhere an instance of a type T is desired, the same instance will be available. Once all references to a Global go out of scope, the shared instance is destroyed and the pointer to it nulled. Thus, this singleton pattern has the property that between tests, the state will be completely reset, as long as there are no memory leaks. The new class, Test::Global, creates a potential conflict with tests that had declared using testing::Test;, so this PR also cleans those up and adds a format check to avoid having that pattern re-occur. Risk Level: the main risk here is that this class will be abused and used in lieu of proper object passing when that's most appropriate. However, the new library is declared as a bazel test library and is in the test/... tree, so hopefully -- with proper code reviews, the usage will be limited. Testing: //test/... Signed-off-by: Joshua Marantz <jmarantz@google.com> Signed-off-by: Fred Douglas <fredlas@google.com>
Adds a Win32 implementation of the ThreadImpl class Risk Level: Low Testing: bazel build //source/... && bazel test //test/.... Also ran the tests on our Windows fork Signed-off-by: Sam Smith <sesmith177@gmail.com> Signed-off-by: Fred Douglas <fredlas@google.com>
Signed-off-by: Sam Smith sesmith177@gmail.com
Description:
Adds a Win32 implementation of the ThreadImpl class
Risk Level:
Low
Testing:
bazel build //source/... && bazel test //test/...
. Also ran the tests on our Windows forkDocs Changes:
N/A
Release Notes:
N/A