-
Notifications
You must be signed in to change notification settings - Fork 6.4k
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 IsMutable method back to Configurable class #8928
Open
mrambacher
wants to merge
6
commits into
facebook:main
Choose a base branch
from
mrambacher:IsPrepared
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6a939a2
Add tests
mrambacher 513259b
Add tests
mrambacher 2f0deee
Merge branch 'main' into IsPrepared
mrambacher 7741cb2
Add IsMutable to Configurable class
mrambacher c22ee4b
make format
mrambacher a6d1141
Attempt to fix Windows build
mrambacher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 understand the problem we're trying to solve but I don't think this method really helps solve it. The problem is that there is an inherent race condition in this interface: in a multi-threaded context, the value returned by
IsMutable
is potentially stale by the time the caller acts on it. That is, the following snippet does not guarantee thatPrepareOptions
will be called only once since another thread might have swooped in between us checkingIsMutable
's return value and callingPrepareOptions
:The way around this is to perform the check and prepare the options atomically (under the same mutex lock) in
PrepareOptions
. Seems to me this is already done in e.g.BlockBasedTableFactory::PrepareOptions
, so I don't thinkIsMutable
itself buys us anything.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 having
IsMutable
in the interface would also eliminate the question of what the default implementation is supposed to do.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.
If you have a better ide
There are two issues that this PR attempts to address:
The first issue could/can be solved in a thread-safe way without the IsMutable being in the interface. The solution there is to do a create/configure/prepare in a single step where there can be no other threads accessing the new object. There is no race and this is how I implemented it in the MemoryAllocator and Cache customizable PRs. The TableFactory methods theoretically still could have this potential race, though in reality I think most of that is avoided (since I do not believe a TableFactory can be shared between databases and will be prepared and validated at DB setup).
The only way I can see to solve the second issue is to put IsMutable into the API (either public or protected). Ideally this would simply set a bool (atomic ?). If we allow non-mutable options to be changed, then the values used by the system may not be updated correctly. For example, you should not be able to set the capacity of the Cache the same way once it is initialized and you cannot update any setting of the JEMallocator once it is initialized.
Do you have an alternative way of solving the second issue?
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 might be missing something but I don't see a conceptual difference between the two issues. I think the second issue could be solved (in a thread-safe fashion) the same way as the first: by performing the mutability check and configuring the object in a single "atomic" operation
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.
P.S. The current
Configurable::ConfigureFromMap
code in the PR seems thread-unsafe for the same reason as aboveThere 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.
Options can be configured (or serialized or compared) after an object is made immutable by many APIs. The DB code does exactly this internally currently and only allows mutable options to be updated. There is nothing that would prevent the user from changing immutable ones outside of the DB however.
For the objects that are "initialize only once" (Cache, MemoryAllocator, etc), the process is create, configure, prepare, lock, return the new object. There are no threading issues to deal with. There can be no race because the object cannot yet be shared amongst multiple objects. But to prevent the object from being configured later, the Configurable class (or each "initialize only once" class -- which would require a lot of overridden methods instead of a single one) needs to know that the object has been prepared.
Yes, there is the potential for some objects (like TableFactory) to have Configure and Validate/Prepare methods called concurrently by multiple threads. Since this is a highly unlikely event (since these objects are not shared between databases), we could probably live with it. For TableFactory, there are typically two ways they are created -- either NewXXXFactory or loaded from the Options file (a two-stage process). For the former, we could "lock" the configurations (Prepare and mark immutable without ramifications -- outside of a few odd tests that do not expect "PreparedOptions"). For the latter, we could mark them immutable when they are loaded from the options file.
Note that the user typically has to go out of their way to create objects that are not prepared. The default option is to prepare options when they are created, so the potential race condition could be documented as a warning to the user on a potentially undefined behavior.
In other words, the race conditions you are concerned about are things we can prevent from happening by default, prevent from happening in many internal cases, and document as potential undefined behavior if the user choses to override the defaults.
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, I understand the issue. What I'm saying is that the two problems you outlined are similar in the sense that some configuration operations are not allowed after a certain point, namely once the configurable is prepared. And in the presence of multiple threads, we would need method(s) that combine the check and the operation in question in order to avoid race conditions. This is the same kind of interface problem that affects multi-threaded data structures like queues or stacks: there's no point in providing, say, separate
IsEmpty
andPop
methods because the value returned byIsEmpty
can go stale by the time the caller acts on it and calls (or doesn't call)Pop
. (The solution is the same there too: fold the emptiness check and the pop operation into aTryPop
method.)I believe we do support sharing a single
TableFactory
between multiple databases.In my opinion, the chances of the application attempting to "illegally" reconfigure an object via a pointer they hold are very slim to start with. In addition, even if we plug every hole in the
Configurable
framework (which this PR doesn't do), the application can still mess with the objects behind the framework's back, and there's no way we can prevent that. Given all this, I'm not sure if this partial solution is worth the extra complexity.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.
P.S. One way to completely eliminate unwanted reconfigurations would be to clone the objects provided by the application. Unfortunately, this would also preclude us from sharing objects across DBs etc., so it's not really an option.
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 this PR does not combine the "check" and "operate" for TableFactories -- this gap is currently there for backwards compatibility. But for TableFactory there is nothing that goes completely haywire if the operation goes stale. For the objects where it goes stale (MemoryAllocator, Cache), the "check" and "operate" are combined (by always returning an object that is already prepared and cannot be later modified) and there is no race. Without the changes to the Configurable class for IsMutable, I cannot prevent the Configure/Prepare methods from being called when they should not be unless I override all of them for every class that falls under this pattern, which seems like more code that is harder to maintain than to put it into one base class.
The chances of an application "illegally" reconfiguring an object are much greater than hitting the threading issues (empty/pop). The API is there for every object and can be called from outside of the DB
The only way I am aware of for objects being messed with behind the framework's back is by getting a pointer to the options (Configurable::GetOptions) and changing the values, which is (and was before this framework existed) documented as unsupported behavior.
I fully agree with you that this change does not completely plug the mutable holes for the TableFactory, but it was also not intended to do so. But I believe this change does allow the hole to be plugged completely for objects like MemoryAllocator and Cache in a simpler manner than could otherwise be implemented. Without this change, I believe those classes will be much more complex and harder to make Customizable and fit into the framework.