-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add fiber safety to __crystal_once
& class_[getter|property]?(&)
macros
#15340
base: master
Are you sure you want to change the base?
Add fiber safety to __crystal_once
& class_[getter|property]?(&)
macros
#15340
Conversation
By duplicating the |
Wonderful. CI decided to blow up 😮💨
MacOS is broken
That was easy to fix, |
f4347db
to
d4fb901
Compare
d4fb901
to
1925c7f
Compare
We need a Mutex to protect against recursion and to make sure the lazy initializers only run once, but Mutex depends on the current fiber, and indirectly the current thread, which themselves may not have been initialized yet and will lead to an infinite recursion once we protect the class getter and property helpers.
Reuses the logic for `__crystal_once`.
Protects against recursion and adds thread (parallelism) and fiber (concurrency) safety to class var initialization.
The initializer block is now captured, and we can't return from a captured block. This outlines that the previous commit is a BREAKING CHANGE! The `class_getter?` version used skip over the intent to cache the result in `@@supports_ipv6` (it returned from the generated function, not from the block), so whenever IPv6 was supported every test was creating yet-another TCPServer (oops).
1. it fails to translate symbol to the enum value 2. it doesn't call `__crystal_once_init`
1925c7f
to
cabbc9c
Compare
# :nodoc: | ||
# Using @[NoInline] so LLVM optimizes for the hot path (var already | ||
# initialized). | ||
@[NoInline] | ||
def self.once(flag : OnceState*, initializer : Void*) : Nil | ||
def self.once(flag : OnceState*, initializer : Void*, closure_data : Void*) : Nil |
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.
question: Why pass the function pointer and closure data as separate values instead of a Proc
instance?
\{% end %} | ||
|
||
\{% name = names[0] %} | ||
|
||
\{% if name.is_a?(TypeDeclaration) %} | ||
{{var_prefix}}\{{name.var.id}} : \{{name.type}}? | ||
{% if macro_prefix == "class_" %} |
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.
thought: With this specialization I'm wondering if it even makes sense anymore to generate these macros from a template. Perhaps we should just evaluate the first macro pass and have much simpler code.
This would be a task for a follow-up though.
in .initialized? | ||
return | ||
in .uninitialized? | ||
flag.value = OnceState::Processing |
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.
issue: Shouldn't we unlock the mutex at this point? The initialization code might be expensive and delay execution of other initializers. I figure it ought to be possible to execute different initializers concurrently.
By setting the flag value to processing
we have reserved exclusive write access for the current fiber, so we might not even need to reacquire it for setting the state to initialized (assuming the i8 assignment is atomic).
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 need the mutex reentrancy for thread A to notice the recursion but also to block thread B from accessing the value until thread A has properly initialized it.
Otherwise thread B could lock the mutex, notice that the flag is :processing and fail with a recursion issue (oops) instead of waiting.
To solve this, we'd need a checked mutex (not a reentrant one) per constant and class variable, which might not be a bad idea 🤔
I'm wondering if we should split this change into a couple of individual PRs, so we retain self-contained and descriptive commits in master. ab8b994 and 86ff3c4 would be great as independent pre-refactors. I presume dc635fb could also easily be extracted as a follow-up. |
Back to draft: as per the suggestions + latest discussion in the issue (capturing the block might be a good breaking change), I'll split some commits into their own PR, and rework things further. |
Fixes a couple issues:
__crystal_once
isn't fiber safe (concurrency issues); the initializer can be invoked multiple times from multiple fibers (despite usingMutex
).This issue is fixed by always using the
Mutex
not only when MT is enabled.class_getter
,class_getter?
,class_property
andclass_property?
are neither thread nor fiber safe (parallelism & concurrency issues).This issue is fixed by reusing
Crystal.once
.NOTE: calls to the aforementioned macros had to be dropped in Fiber and Thread because Mutex depends on them and we need the later to implement said macros (chicken/egg => infinite recursion => stack overflows).
This is a breaking change because the block is now captured, and we can'tThe block is no longer captured. We might want to introduce a compile time flag to enable the new behavior as it could help with some LLVM inlining behavior (inline the check).return
from it anymore. This is outlined by the commit that fixesSocketSpecHelper.supports_ipv6?
that didn't work as expected anyway (the@@supports_ipv6
class var was never set totrue
).builds on top of #15333
closes #14905