[monitorlib, mock_uss] Improve SynchronizedValue#1207
[monitorlib, mock_uss] Improve SynchronizedValue#1207BenjaminPelletier merged 7 commits intointeruss:mainfrom
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.
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.
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.
Should _value also be set to None for consistency?
There was a problem hiding this comment.
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.
Are lambdas needed? They don't seems to add anything ^^'
There was a problem hiding this comment.
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.
Wouldn't it be nice to have it on the new __enter__ as well ?
There was a problem hiding this comment.
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 :) |
mickmis
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
Same as above: value being None is not a valid scenario?
There was a problem hiding this comment.
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.