Running a generator inside a Project Check with/without a runWriteAction?
We managed to run a generator from a transformation menu (or other editor components), which allows us to inform the user that the result of their specification has changed in a not-backwards compatible way. We prefer doing this via the existing generator because otherwise we need to write and maintain the same transformation code twice (or write our own generator-language as well).
We ran into a problem when running the same generator from a checking rule (to have it detect a breaking change during an explicit Check Project). The generator produces its results with a call to TransientModelProvider.PublishAll(), which needs to be run inside a ModelAccess.runWriteAction(). However, any code (even when started from another thread) within a runWriteAction is ran after the Project Check finishes, which is to late to give the error/warning/info messages to the user. Is there a way to work around this or a better solution?
We believe the generator only writes transient models, which can refer to roots in project models but not the other way around, so it should not interfere with the root/models in the project that is being checked? We only need to inspect one root produced by the generator and compare it with an existing root in the project. Is there a way for the check project thread to not take the lock for writing transient models? Is there a maybe a way to separate the project and transients over different repositories?
Any experiences with this or advice is appreciated. Kind regards, Arjen
Please sign in to leave a comment.
That's an excellent example of m2m use I was longing for years! Unfortunately, right now this scenario is not fully supported due to very limited interest to use Generator out of regular make process. Indeed, repository for transient transformation models has to be separate from the one of the project, that's why TransientModelsProvider takes SRepository, so that one can create own TransientModelsProvider to use instead of regular TransientModelsComponent (the one associated with a project repository). However, we still have unresolved issues with read access in multiple repository scenario, and didn't pay attention to this functionality for quite some time (due to lack of interest from users, as I mentioned above).
I hope we can find a workaround for you now, as publishAll is necessary when you need transformation results to become public for wide audience. If you consume transformation results right away, there's no need to publish models into a repository. What API do you use to start transformation? In GenerationFacade, there's #process() method that takes List<GeneratorTask>. It needs 'read' for input model's repository, and doesn't publish transients itself (responsibility of client code). Your code would need to create transient module (TMC#createModule()) and associate (TMC#associate()) it with GeneratorTask prior to #process(). Once m2m is over, you can use transient module (TransientModelsProvider#getModule(GeneratorTask)) to look up your output, or pick GenerationStatus and output model it supplies from GeneratorTaskListener.
We needed publishAll to show the diff but ofcourse this is only usefull in an interactive context and not needed for a checking rule.
I tried to do what you described (transientModelProvider.getModule(task)), but I don't see any modules or modelRoots.
final ModelGenerationPlan plan = this.getGenerationPlan();
final GenerationOptions generationOptions = GenerationOptions.getDefaults().customPlan(model, plan).create();
final IMessageHandler messageHandler = getMessageHandler();
final TransientModelsProvider transientModelProvider = new TransientModelsProvider(this.repo, null);
final EmptyProgressMonitor progressMonitor = new EmptyProgressMonitor();
final GenerationTaskRecorder<GeneratorTask> taskHandler = new GenerationTaskRecorder<GeneratorTask>(null);
final GeneratorTaskBase task = new GeneratorTaskBase(model);
transientModelProvider.associate(task, transientModelProvider.createModule(moduleName));
modelAccess.runReadAction({ =>
GenerationFacade facade = new GenerationFacade(repo, generationOptions);
facade.messages(messageHandler); facade.transients(transientModelProvider); facade.taskHandler(taskHandler);
facade.process(progressMonitor, Collections.singletonList(task));
});
foreach module in transientModelProvider.getModules() {
System.out.println(module.getClass() + " : " + module.getModuleName());
}
Could you please have a look at my attempt and give me some more help? I also don't know how to get to GenerationStatus.
taskHandler.getAllRecorded() gives you GenerationStatus for all completed tasks
TMC.getModules() gives you access to published modules only. Use TMC.getModule(GeneratorTask task) to access modules that were not published yet. However, in your case it's easier to go with GenerationStatus.
Thank you for the quick reply! It works fine as long as we run it in a separate thread.
I suppose checking rule holds read lock for a repository with input models, and Generator doesn't need more, hence same thread as checking rule sounds as a possible approach. Might be not very effective provided transformation could take a while, but will do. The only idea I've got is that it get stuck with parallel transformations (separate threads get started in addition to the one you run transformation from). Did you find out why same thread didn't work? Any lock waiting? If you can create a thread dump at that moment, would be helpful to understand the cause. BTW, single-threaded transformation might be worth trying anyway.
This is the threaddump just after facade.process(progressMonitor, Collections.singletonList(task)) returned (isntantly). I'm not starting any threads and I removed all modelaccess.runReadAction (as Check Project already has read access). The taskHandler.getAllRecorded() returns an empty list. I don't know how to debug this; can you spot anything here? (Is there a way to attach a file to a post?)
I didn't notice anything suspicious in the thread dump.
I could imagine process() returns immediately if your progressMonitor happens to be cancelled (e.g. if it's an instance associate with UI element). However, there's no external progressMonitor inside checking rule, and I doubt you went a hard way and obtained IDEA's progress indicator or anything similar, I suppose it's just an EmptyProgressMonitor instance, right?
I double checked but it is indeed just a new EmptyProgressMonitor(), as shown in the code earlier. I did the threaddump programatically after process() returns (not in parallel, not sure how to time it right) and noticed that getAllRecorded() returned empty.
Well, another possible answer would be cancellable model read, although to confirm that I'd need complete stacktrace for the thread that attempts to start generator. I.e. if there's CancellableReadAction (subclass thereof), then model read could stop once a model write action comes (which may happen e.g. due to some vis background update). I'm aware of Highlighter code that uses cancellable model reads and may eventually get down to non-typesystem checks, however, from the thread dump it doesn't look like a highlighter-initiated activity (Highlighter got own named thread).
If cancellable reads are not the case, then I'm out of ideas, and only debug and stepping into process() would reveal the answer.