-
Notifications
You must be signed in to change notification settings - Fork 27
[monitorlib, mock_uss] Improve SynchronizedValue #1207
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
[monitorlib, mock_uss] Improve SynchronizedValue #1207
Conversation
There is one code style finding that puzzles me. The error is: @the-glu , do you have any ideas on this? |
return flask.jsonify(response), 412 | ||
logger.info(f"Deleted ISA {deleted_isa.dss_query.isa.id}") | ||
else: | ||
deleted_isa = None |
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: I would have the shorted branch on top of the if, to be able have everything close (here you need to backtrack almost 20 lines to check the condition again)
else: | ||
deleted_isa = None | ||
|
||
result = ChangeTestResponse(version=record.version, injected_flights=record.flights) |
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: I would move result on line 137 and merge both if (138/158)
|
||
def _unlock(self, exc_type=None, exc_val=None, exc_tb=None): | ||
self._lock.__exit__(exc_type, exc_val, exc_tb) | ||
self._locked = False |
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 _value also be set to None for consistency?
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.
Yes, I think so -- thanks
|
||
def transact(self) -> Transaction[TValue]: | ||
return Transaction[TValue]( | ||
self._lock, lambda: self._get_value(), lambda v: self._set_value(v) |
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.
Are lambdas needed? They don't seems to add 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.
No; removed -- thanks
self._current_value = self._get_value() | ||
return self._current_value | ||
if self._transaction: | ||
raise RuntimeError( |
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.
Wouldn't it be nice to have it on the new __enter__
as well ?
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.
Yes; added check for self._locked
I tried to debug locally on your branch, but basedpyright didn't gave me any error, was there some caching somewhere? |
I added the error to the baseline for this PR, but I don't think I should have had to do that. |
Ho, no worries, I disabled it for testing. I think the linter is complaining because he cannot be sure Switching to
Make it happy :) (The whole variable is needed, even storing just tx.value.flights raise the error, so I'm not sure my explanation is right. Maybe it doesn't know it's a lock and assume another thread my edit the dict between the two lines?) |
Based on microsoft/pyright#703 (comment) , it's indeed treating |
Ah, I think that makes some sense -- thanks. It is interesting what the type checker thinks can be assumed and what can't, since, e.g., field access ( |
Yes, behind the scene it does use some patterns, and with type narrowing we can also use it to help typing, should it make sense at some point, e.g. in implicitdict :) |
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 with some nits and modulo clarification of checks for value
being None
.
) | ||
result["query"] = notification.query | ||
|
||
if record.isa_version is not None: |
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.
Checking ridsp_create_test
, isa_version
is optional only because we receive the ISA version after creating the record. However when accessing it from ridsp_delete_test
it should never happen that isa_version
is None
.
Here assert instead that it is not None by raising an exception if it is? Or alternatively remove the optional on isa_version
? And/or create the record
only after the ISA was successfully created?
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 it probably happens that the current implementation may ensure isa_version
is not None, but I don't think that is a guaranteed behavior from the perspective of the deletion handler. In the deletion handler, the question is what to do if we have (for whatever reason) a flight record with an isa_version
that is None. The type annotation of isa_version
explicitly allows for it to be None, so the deletion handler should handle that situation as gracefully as possible. Since it is definitely possible to delete a flight even if an ISA wasn't emplaced in a DSS, it seems like this check is what we would want to do.
We could remove the optional on isa_version
, consistently guaranteeing that isa_version
is not None at this point, which would have impacts in multiple places including this deletion handler. But, 1) I don't think that change would belong in this PR since that would be a behavioral/expectations change in mock_uss which doesn't relate to SynchronizedValue and its transactions, and 2) I could imagine some circumstances in which we may want to enhance mock_uss to create flights even when it can't or doesn't create an ISA in a DSS (e.g., off-nominal behavior when a DSS is down). I'm hoping to replace RID injection with calls to the flight_planning API when the flight_planning API can specify UA positions in which case there would presumably often be cases when a flight record has no isa_version
.
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.
Since it is definitely possible to delete a flight even if an ISA wasn't emplaced in a DSS, it seems like this check is what we would want to do.
Indeed!
|
||
|
||
# Note: attempts to change the below to SynchronizedValue[TValue] causes problems because IntelliJ does not reliably | ||
# understand the newer syntax and therefore fails to provide contextual information for specific TValues. |
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: seems to be a known issue: https://docs.astral.sh/ruff/rules/non-pep695-generic-class/#known-problems
Link this?
@property | ||
def value(self) -> TValue: | ||
"""Value exposed for this transaction. Mutate or set it to make changes during the transaction.""" | ||
if not self._locked or self._value is None: |
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.
Does this imply that self_value
may never be None
? What if TValue
is Something | None
? I don't see this scenario prevented by something.
Also, I'm not sure I understand the intent behind checking for self._value is None
: is it to ensure that a write has been performed before? Or to handle a race with a concurrent thread being between lines 40 & 41?
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 fair -- my implicit assumption was that TValue would always be a non-Noneable concrete value, so value=None could be used to communicate absence of value. But, thinking about things a little more, it doesn't seem like that's an important assumption -- I'll adjust to make _value a pure TValue (rather than TValue | None
) and I think that will take care of both this comment and the following one.
if not self._locked: | ||
return | ||
try: | ||
if self.value is not None and exc_type is None: |
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.
Same as above: value
being None
is not a valid scenario?
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 and adjusted
In a number of places, we synchronize data across multiple threads using SynchronizedValue. This tool ensures read-modify-write transactions are not interrupted by changes from another thread. Currently, however, the
__enter__
/__exit__
pattern used does not provide useful type-hinting for IDEs nor for correctness checks like ruff/basedpyright, and it also prevents certain operations like replacing content rather than mutating it during a transaction. This PR attempts to address these shortcomings by adding atransact()
method to SynchronizedValue so that usage is nowwith db.transact() as tx:
rather than the more confusing and less inspectablewith db as tx:
.The old behavior is retained as deprecation-annotated methods to avoid making this PR huge by changing all usages at once. However, the flights storage of mock_uss is entirely switched to the new usage to verify functionality.
A number of ruff/basedpyright errors revealed by the improved inspectability of SynchronizedValue types are fixed, though one is added; see comment below.