refactor(nns): Switch NNS Governance global state from static to thread_local #2844
+35
−17
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.
Why
governance()
andgovernance_mut()
are badRepresenting the canister global state with
thread_local!
avoids using unsafe blocks to access it. When using unsafe blocks to access it, one can easily write code with undefined behavior by retaining references across await boundary (more precisely, after an inter-canister call).Why this PR
The NNS Governance heavily depends on the accessing the global state as
static
, and there will be a lot of refactoring needed in order to get away with the current pattern. This PR is the first step towards getting rid of those bad access patterns - with this change, we can gradually migrate from usinggovernance()
/governance_mut()
to usingGOVERNANCE.with()
. When all the usage ofgovernance()
/governance_mut()
are replaced, we can delete them and declare victory.What
Define the global state with
thread_local!
(LocalKey<RefCell<Governance>>
) while returning the raw pointer for the existing access pattern.Why it is safe
The
LocalKey<RefCell<T>>
is set once and only once duringpost_upgrade
orinit
, so the pointer should remain constant, given that the canister code is single threaded. When accessing throughgovernance()
orgovernance_mut()
one can still write code with undefined behavior, but it is the same danger as what we have now.Why
Governance::new_uninitialized
This is needed in order for the
thread_local!
state to beGovernance
instead ofOption<Governance>
. We know the "uninitialized" version won't be used except for the code in the init/post_upgrade before the "initialized" state is set. However, there doesn't seem to be a good way to express that understanding. An alternative is to still useOption<Governance>
andunwrap()
every time, but it seems more cumbersome.