Listeners on org.jetbrains.mps.openapi.module.ModelAccess have unpredictable behavior

I am creating an engine in a MPS-plugin using the MPS openapi. The engine is based on transaction memory and is uses ThreadLocals to hold the running transaction on specific threads. I tried to use the action listeners on ModelAccess to set and unset the transactions when necessary. This does not work.

According to the docs, jetbrains.mps.smodel.ActionDispatcher is using a counter to only signal listeners on the top-most actions. However, since a AtomicInteger is used for this purpose, this can lead to a start on one thread and a finish on another thread. If the counter would have been a ThreadLocal this would be symmetrically (on the same thread) and one top-most action at a time per thread.

Is this asynchronous listener behavior  intentional?

Is there another way to listen to the start and finish of actions on the same thread as where the  (top-most) action is run?

8 comments
Comment actions Permalink

My problem would also be solved if there was a way to know for which project a certain thread is running. For example, when it calls something that extends base-language.

0
Comment actions Permalink

The idea of listeners of ModelAccess (i.e. CommandListener, WriteActionListener, ReadActionListener) is to know the moment specific model access mode starts/finishes. By design, there's single thread for commands and model writes, therefore these listeners would get notified about start/finish of the action in the same thread. Read actions, indeed, may run in parallel, and it's possible to see start and finish coming in different threads. ReadActionListener is more about model state where SNode instances could be safely kept/cached (they could not get modified in between, as no write action is allowed until read action(s) finish). So far we didn't face a need to get this notifications on a per-thread basis, that's why it's the way it is, and this behaviour should be deemed intentional. I don't know any alternative mechanism to get a per-thread notifications.

Still, I'm not sure I get your scenario deep enough to suggest an alternative approach. I assumed you'd like to have each distinct read action recorded as a transaction, and that read action code doesn't access your transaction mechanism explicitly. Accessing top transaction only won't address the first part (your code won't notice a read starting in a parallel thread) unless read actions you care about access transaction mechanism explicitly. However, if they do, why not notify about transaction completion explicitly then? It sounds like what you need is per-thread read action notifications, i.e. like each thread keeping its own 'top' read.

> to know for which project a certain thread is running

Model access doesn't necessarily happen to a model which is part of a project, that's why project is sort of unexposed in model api. However, we are moving towards a state where models loaded from a project would belong to a ProjectRepository one could use to access Project.

 

0
Comment actions Permalink

Thanks for your replay Tikhomirov!

> I'm not sure I get your scenario deep enough to suggest an alternative approach

In our plugin we augment the state of models. There is one plugin per project. If that state is read (by some extensions to base-language) I need to find-out which project and/or corresponding plugin needs to be used. We only need to get notifications for top-level actions (read/write/command). I know that models can be shard among projects. We have project-specific augmentations for those models.

> I don't know any alternative mechanism to get a per-thread notifications.

Use a ThreadLocal instead of a Atomic for the counter in the ActionDispatcher to determine the top-level action. This wil have no effect on write-actions and commands (because indeed they are working on a single thread per design). However, read-actions will only be nested within one thread, causing the desired per-thread notifications. Listeners to read-actions that do not want the signals per thread can always use an Atomic counter to create the current behavior.

For example MPS's own CachingReferenceScopeHelper class could be simplified. I is working with a ThreadLocal that holds the cache, that would not be necessary anymore.

0
Comment actions Permalink

> Use a ThreadLocal instead of a Atomic for the counter in the ActionDispatcher to determine the top-level action.

With that, each thread would have own read start/finish notifications, right? Do we need a mechanism to register a ReadActionListener for a specific thread then (ModelAccess.addReadActionListener(ReadActionListener, Thread)? Troublesome API, imo. Otherwise, if MPS notifies all RALs it keeps for a ModelAccess instance, then it would be RAL implementation trouble to maintain thread-local state (i.e. same RAL instance would get multiple readStarted() from different threads). Usually it's quite error-prone to leave complicated thread-sensitive listener implementations to an API user - it's easy to make a mistake in such code, and the defect is usually manifested from inside some framework code, making it very hard to trace. That's why I'd prefer not to force API event subscribers to care about threading unless utterly necessary.

The quirks in CachingReferenceScopeHelper are rather about event dispatch mechanism running in parallel with reads from other threads (when read runs but an instance field, initialized from a listener, has not been initialized yet). Use of ThreadLocal  there is just a safe way to ensure that Scope author doesn't need to care about multi-threading (MPS never demanded that and therefore it was unsafe to assume Scope implementations out there are thread-aware). In perfect world, we'd better share Scope instance for all reads in all threads.

0
Comment actions Permalink

You are right, in my suggested solution a ThreadLocal is still needed in CachingReferenceScopeHelper, I did not see that.

I think that any implementer of a Listener to read-actions need to be aware of the multi-threaded nature of red-actions anyway. You do not prevent that with the current mechanism either.

I do not need extra additions to the listener API, just a different (multi-threading) behavior.

The only solution to my problem that I can think of currently is using a lock so that (top-level) read-actions (using the current listener mechanism) for different projects cannot run at the same time. Do you see any problems in that (apart from performance)?

0
Comment actions Permalink

Read action could start on a thread you can hardly attribute to any specific project (i.e. a thread from an app pool). How would you find out the project then?

0
Comment actions Permalink

I know that a specific thread is not mapped to one project. However, when running a (read/write/command)action it does. This is because I assume that actions can only be run by using a ModelAccess instance that is specific for a project. Is that a right assumption? I want to capture when an action is run on a certain thread for a specific project (only while the action is running). Another approach for me would be to prevent running (top-level)read-actions on different projects on the same time (by using a locking mechanism).

0
Comment actions Permalink

> actions can only be run by using a ModelAccess instance that is specific for a project. Is that a right assumption?

That's what we strive to get to for last few years. Unfortunately, we are not there yet, as you may find ModelAccess.instance() calls in projects that are built of top of MPS, and though MPS itself is not bound to global model access any more, we have to keep shared global ModelAccess instance for compatibility with other projects until they get refactored. With that, this assumption is correct if you use project.getModelAccess() or project.getRepository().getModelAccess() for you read/write actions, and could be plain wrong if code runs with ModelAccess.instance() or MPSModuleRepository.getInstance().getModelAccess().

0

Please sign in to leave a comment.