introduce a transaction reconciliation framework for model listeners#1764
introduce a transaction reconciliation framework for model listeners#1764dalbrecht1 wants to merge 1 commit intoJetBrains:maintenance/mps20223from
Conversation
|
Also, I presume that you'd like tests for this piece of functionality, but so far I was only able to "play-test" this interactively. I'd appreciate some help / input on creating a fully-automated test that incorporates concept declarations, model listeners, transactions, concurrency, all within the framework of an MPS test case. I can certainly come up with ideas on my own, but this is bit too deep into "dark arts" right now that I'd feel comfortable with such tests or be able to write them quickly. |
IMO, model listeners are misused outside the MPS codebase most of the time (the async update mechanism of feature models of IETS3 is a good example for me). They often lead to unpredictable changes and often deal with MPS models on a level that is too low (people then sometimes even write MPS threading code to fix certain issues). Sometimes they are even used to interact with the open editor (e.g., to update the selection), which is also not a good idea because they know nothing about editors. People try to solve issues with them that could be solved in other ways:
|
I'm totally with you on most if not all of these points, still - considering the user base of some tools - there are few alternatives in some scenarios. And yes, given Sascha Lißon already (rightly) blamed some strange side effects of syncing model data using Modelix on model listeners, I'm definitely looking into replacing them as often as I can. But just a few thoughts on some of your alternatives.
At least in regards to the intentions I have to say, we have a hard time teaching our domain-focused users how and when to use intentions. As a developer, they quickly became second nature for me. But our users just don't seem to on the same wave length with intentions at all most of the time.
Definitely my go-to if the data and control work flow works out! But here again, Modelix first syncs all children and properties of (new) nodes, and then updates/sets all references in a second pass. That still leaves issues if you want to react to property changes. These can happen before references are configured which your followup-logic might want to check. Sure, I can place my debouncing-logic in the property-constraint then instead of the model listener, but that doesn't solve the issue that I might still need debouncing anyway.
As I included, node factories aren't triggered by Modelix. That can be a good or a bad thing based on your exact context / use case. And tools like substitute menus or action maps or such only trigger when the user explicitly takes action in these places (and you don't forget to configure them in other places where the user can interact with the same instances). So, we offload the "automation" to the developer who has to be very meticulous when it comes to touching the language down the line. How quickly can that be forgotten in a PR review for example...
That leaves follow-up actions in a sort of limbo, where once you open the chunk, they apply, but in the meantime the model might report errors. Especially in the context of Modelix and web clients to your model, this is usually undesirable. The web UIs are happy to only render consistent data, where MPS is more tolerant towards broken stuff.
I guess that goes roughly in the direction of query-lists and "dynamic domain-link resolution"? Sure, but not ideal in a tool with already sub-par performance. But I guess that should then be the primary focus of any maintenance efforts...
That so far was both out of my focus, but definitely an interesting situation to think about. Again, I agree to remove them when possible in favor of other ways, sometimes keeping the "automatic magic" aspect, sometimes converting them to "additional and explicit user actions". But that only helps so far, if your users have their own minds and assumptions of how something (is supposed to) work(s). And sometimes a sprinkle of magic is just what it takes to make a tool feel welcoming/lively, even if that has to be accomplished with model listeners. |
|
Without looking at the code yet, MPS provides a batch event listener (RepositoryChangeTracker) which you can use to react to all events that happened in a command. Are you aware of it? Would it help in your case or do you need to react to events during a command? See UsageModelTracker for a (trivial and only) usage example. |
No, I wasn't aware of that type. And even looking at that type/usage for a while now doesn't really shed a lot of light on how to make this work. Am I correct that this would serve as a complete replacement of the model listeners and I would spin my own "reaction engine" in base language? While the initial description of this PR was already complex, here are some further thoughts that guided this development:
If in the end it turns out that my tool is redundant then I'm definitely not mad/sad. It was a interesting exercise to experience transaction scopes, concurrency, undo-processes, AI-brainstorming/-conceptualization and the like. But (a) I hadn't found anything that served my use case, (b) I already found a few work flows that would violate any of the stated principles, (c) even now seeing this alternative doesn't really bring any ideas to mind how that is used / superior (other than, it exists and is already proven to work). |
a21722c to
b469183
Compare
|
If you want to pursue this further, I do want to see some tests or examples of this functionality, to understand it better and to keep it from breaking during migrations or other kind of maintenance. Also, I would appreciate it if this PR was based on one of the actively maintained versions, i.e. from 2024.1 onwards. |
Understandable. Though, I haven't had the time to delve into the realm of automated testing for this particular idea. But yes, in general I'd really want to follow up on my initial proposal and at least get some feedback here.
Given that we would want to integrate an enriched release version of the extensions into a project that (currently) is based on this exact MPS version, there is very little I can do to get this more up-to-date / more recent / in a more acceptable state. In fact, to give some more context, I was not trying to utilize this framework as a way to promote more usage of model listeners! If you read my description carefully, you'll realize that I'm fully aware of the drawbacks in more ways than one. Instead we are in the situation that we already have some (small finite number of) model listeners which we don't see a way to replace reasonably with anything else. But which also break existing workflows in combination with Modelix sync processes that we and our users start to face more and more often. These NEED debouncing! I wasn't trying to get you to maintain my new framework by offloading it to the extensions project. But the circumstances are the way that we will need this or something like this, and rather sooner than later. And putting my proposed solution for discussion here first and foremost was meant as a way to gather feedback. This idea apparently didn't work out as intended, as you are busy yourself... That's mostly on me for not providing examples/tests that would have simplified the review process by giving a good starting point. |
|
Ok I will wait for the tests or an example (a sandbox), then I might be able to give some feedback. |
|
Closing this for now due to no activity, please reopen or recreate when you have an example or tests. |
Motivation
Model Listeners sometimes are in a bit of a tricky situation. They can be the most ideal way to define certain kinds of automation, but operate on a quite low level of abstraction and interfere with other routines. In regards to the level of abstraction, I'd compare them to using keymaps in your editors instead of action maps to define user interactions. In regards to the interference, I'd like to point to Modelix, that I had a lot of trouble with at first when applying it to existing projects and workflows recently.
But to be more concrete let me present you with two examples:
So, while in both/all cases you should first do your best in optimizing/refactoring model listeners to (a) not be necessary at all, (b) not alter the model in unnecessary ways, and/or (c) be idempotent/re-runnable at least, there are these cases where even the ideal model listener will struggle. For these cases, this PR introduces a utility model, which wants to give more power to the developer in expressing intent in the current model listener framework, by means of simply wrapping your previous listener body in a single, minimalistic "debouncing"-command. Ideally, very little else has to change in the actual code.
Proposal
Most if not all user interactions are encapsuled in a command or an undo-transparent action (I'll use "command" as the shorthand going forward, but remember that this means these two types of transactions). The same is true for any operation Modelix takes while fetching and integrating external changes into your local project. Hence, the MPS platform already "knows", that there are more atomic changes to come before the whole transaction finalizes. Therefore, within the model listener you are dealing with an intermediate state, but the ultimate state after the whole transaction can look significantly different. Hence, model listeners only see a snapshot at the exact times certain operations are performed, but do not consider a change in the big picture of the whole command. And while you certainly could just make use of "invokeLater"-calls or spin up your own boilerplate solution to apply in all model listeners, the strength of this proposal is a complete "action reconciliation runtime" that comes at almost no cost to you as the developer or the runtime (with no proof for that claim so far).
Whenever a model listener hook is executed, and the hook is executed in the context of a transaction that can have "user intent", you are now able to not run your code right away, but have it scheduled to only run at the end of the current transaction, when this delayed execution (the debouncing principle) will allow your code to consider the entirety of the operations being included in the command.
Usage Example
Imagine the context of a sort of truth table like use case. But one where there isn't just "true" and "false", but where the user can specify any terminals they like. But based on these terminals the user also should be able to model a binary operator in the form
terminal op terminal => terminal. Here, if the table editor is done right, you do not have to take any actions when a new terminal is added. However, you might have to remove any "truth table entries" that feature a specific terminal, if the user wants to delete this particular terminal.Hence, you might have a model listener like:
But if a terminal is just manually reordered, then the table will still drop these entries regardless. With this new framework, you can instead do the following:
As you see, you receive one benefit at the cost of putting yourself in the timeline where the node is already gone for good, hence having to shift to checking the underlying node-ids. Which you could circumvent by pre-querying the entries-to-be-removed prior to scheduling the removal and within that callback to simply detach them all. I'll show an example of that in the following code snippet.
Or, using another feature of this framework, you can make use of a "strategy" to already express intent of "only run on actual removal", like that:
And the final gap between the original listener and the last one could even be bridged by defining specific callback types that capture even more intent (the one used here is already included in the framework, but they are just implementations of a given (functional) interface). Then it really looks very much the same as before.
If at any point this new delaying engine isn't able to retrieve a fitting instance or finds itself outside a command - aka in a situation where there is no broader "user intent" to consider - the schedule method will simply immediately execute the provided hook, should the strategy call for it. And if you want to perform specific actions on move- and remove-only, you are free to include both definitions in the same model listener. Then, either on command finalization, or right away, only the "correct" one is executed. This ensures that the utility is safe to use in unit tests or headless scripts as well where a Command context might not be present, maintaining the 'no callback lost' guarantee.
Further Work
This PR for now only proposes a "code/runtime only" solution which expects the developer to call the right classes and methods to let their callbacks be managed by my new system. Functionality-wise this might already be as refined as it has to be, though I'm open for any comments and discussions. But what this proposal still lacks in my eyes is a nice syntax that highlights the intent better. See, I spent some time optimizing this such that the intent of the written listener code is better captured at runtime, but at the cost of "silly static calls" that are meaningless to the uninitiated.
And while it might not be much work to elevate the principles to an intent-focused syntax/DSL, I just lacked the time right now and first wanted to let you give feedback on the principle behind my concept. But in the end, I'd wish for a syntax that is closer to the "command / read action / write action with repository" statements or the "with _ models in scope do" statement. Which should be a somewhat trivial translation task, I assume.
One example, where such a DSL could still improve on the current state, is to create meaningful aliases for the strategies that adapt to the type of listener. While technically the "IF_ABSENT" strategy perfectly captures the state of the node, within a "child-removed" listener and to better communicate intent, the same states could be made available as "node moved" vs "node removed" strategies within the DSL.