core/kernel/source/jetbrains/mps/smodel/ModelAccess.java (186 lines of code) (raw):
/*
* Copyright 2003-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.smodel;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.smodel.references.ImmatureReferences;
import jetbrains.mps.smodel.references.UnregisteredNodes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* This if front-end for legacy code that deals with a single instance of MA (available through MA.instance()).
* There are 2 implementations generally available, DefaultModelAccess and WorkbenchModelAccess. Neither is an openapi.ModelAccess available
* from SRepository#getModelAccess() call, opeanpi.MA instances from repository now merely delegate to the singleton available from #instance() method.
*
* For now, WMA provides implementation of methods that deal with Project (i.e. undo support), therefore we keep methods with Project as part of this class
* implementation API. Instead, we shall implement execute* methods in respective openapi.MA implementations bound to project repositories and remove
* Project-aware methods from this class altogether. We may want to keep this class for another release as DMA and WMA have different perspective on
* platform locking (latter adds IDEA platform locks), and with that, we may still delegate general read/write actions of repository's MA to this singleton.
*
* The actual implementation of {@link org.jetbrains.mps.openapi.module.ModelAccess} interface methods
* Probably it is better to merge it with
* {@link jetbrains.mps.project.ProjectModelAccess} and
* {@link jetbrains.mps.smodel.ModelAccessBase}
* which currently simply delegate all methods to this class
*
* @see org.jetbrains.mps.openapi.module.ModelAccess
*/
public abstract class ModelAccess extends AbstractModelAccess implements org.jetbrains.mps.openapi.module.ModelAccess, ModelCommandContext.Provider {
protected static final Logger LOG = Logger.getLogger(ModelAccess.class);
protected static ModelAccess ourInstance = newInstance();
/**
* INTERNAL, TRANSITION CODE, DON'T USE!
*/
public static ModelAccess newInstance() {
return new DefaultModelAccess();
}
private final ReentrantReadWriteLockEx myReadWriteLock = new ReentrantReadWriteLockEx();
private final CommandContextProvider myCommandContextProvider = new CommandContextProvider();
protected ModelAccess() {
}
/**
* It is better to use {@link org.jetbrains.mps.openapi.module.SRepository#getModelAccess()} method to get
* the repository access.
* @deprecated
* @since 3.1
*/
@Deprecated(since = "3.3", forRemoval = true)
public static ModelAccess instance() {
return ourInstance;
}
/*package*/ static void setInstance(@NotNull ModelAccess modelAccess) {
ourInstance = modelAccess;
}
protected Lock getReadLock() {
return myReadWriteLock.readLock();
}
protected Lock getWriteLock() {
return myReadWriteLock.writeLock();
}
protected final void assertNotWriteFromRead() {
if (canRead()) {
throw new IllegalStateException("deadlock prevention: do not start write action from read");
}
}
public boolean hasScheduledWrites() {
return myReadWriteLock.hasScheduledWrites();
}
@Override
public boolean canRead() {
return isReadEnabledFlag() || myReadWriteLock.getReadHoldCount() != 0 || myReadWriteLock.isWriteLockedByCurrentThread();
}
@Override
public boolean canWrite() {
return myReadWriteLock.isWriteLockedByCurrentThread();
}
// ExecuteCommandStatement with repo == null generates into executeCommand(Runnable)
// left abstract method (though could have deleted method) as there might be references from MPS code to the implementation that used to be here
@Override
public abstract void executeCommand(Runnable r);
@Override
public final void executeCommandInEDT(Runnable r) {
// this method is not invoked from generated code (generated code uses MA.instance().runCommandInEDT(R, P)), and hand-written shall not
// use MA.instance() any longer. Therefore neither DefaultModelAccess nor WorkbenchModelAccess shall override this method.
throw new UnsupportedOperationException();
}
@Override
public final void executeUndoTransparentCommand(Runnable r) {
// see executeCommandInEDT() above for reasons why it's final. Templates generate repo.getModelAccess().executeUndoTC(), never MA.instance().eUTC()
throw new UnsupportedOperationException();
}
@Override
public boolean isCommandAction() {
return canWrite() && myCommandActionDispatcher.isInsideAction();
}
protected void onCommandStarted() {
myCommandContextProvider.engage();
}
protected void onCommandFinished() {
myCommandContextProvider.discard();
}
@Nullable
@Override
public ModelCommandContext getCommandContext(SModel model) {
// isCommandAction might be excessive, just want to make sure there's not access to MCC from a thread other than the command one.
return isCommandAction() ? myCommandContextProvider.get(model, getUndoHandler(model)) : null;
}
@Nullable
protected abstract UndoHandler getUndoHandler(/*NotNull*/ SModel model);
protected final void sharedReadIsOver() {
ReadAccessToken token = myReadFlagTokens.get();
if (token != null) {
token.revoke();
myReadFlagTokens.remove();
myAllReadFlagTokens.remove(token);
}
}
/*package*/ ReadAccessToken shareRead() {
if (!canRead()) {
throw new IllegalModelAccessException("Can share a read in progress only!");
}
return acquireSharedReadTokenNoCheck();
}
/**
* Acquires a shared read token for the current thread, ensuring thread-bound read access.
* If no token is already associated with the current thread, a new token is created,
* registered, and returned. The shared read token allows multiple threads to have
* concurrent read access, adhering to specific usage and lifecycle constraints.
* <p>
* Note: this method does not check if the current thread has read access, to establish
* that fact, and to ensure that all the necessary constraints are satisfied, is on the conscience
* of the caller.
*
* @return an instance of {@link ReadAccessToken} representing the shared read access
* bound to the current thread. If a token already exists for the thread, the
* existing token is returned.
*/
protected ReadAccessToken acquireSharedReadTokenNoCheck() {
ReadAccessToken token = myReadFlagTokens.get();
// XXX not sure about sharing original read, need to give it more thought/investigation
// e.g. what happens if/when 'nested' read reports sharedReadIsOver(). If this is the case, perhaps, need "usage counter" for the token?
// Keep in mind, token is bound to the current thread, another thread (running under 'read enabled') would get another instance.
// ^^^ sounds like we can get into a state when original thread releases platform read, but there are 2 threads with 'read enabled' state.
// (thread0 shared its read for thread1, thread1 was in 'read enabled' and shared its state for thread2, thread0 ends, platform lock gone)
if (token == null) {
token = new ReadAccessToken();
myReadFlagTokens.set(token);
myAllReadFlagTokens.add(token);
}
return token;
}
private final ConcurrentLinkedQueue<ReadAccessToken> myAllReadFlagTokens = new ConcurrentLinkedQueue<>();
private final ThreadLocal<ReadAccessToken> myReadFlagTokens = new ThreadLocal<>();
private boolean isReadEnabledFlag() {
// FIXME I wonder if we shall filter isActive (or make it part of isReadInProgressCurrentThread)
return myAllReadFlagTokens.stream().anyMatch(ReadAccessToken::isReadInProgressCurrentThread);
}
private static class ReentrantReadWriteLockEx extends ReentrantReadWriteLock {
public ReentrantReadWriteLockEx() {
super(true);
}
public boolean hasScheduledWrites() {
return !this.getQueuedWriterThreads().isEmpty();
}
}
private static class CommandContextProvider {
// don't care about multi-threaded access as command are executed inside 1 thread only
private boolean myEngaged = false;
private final Map<SModel, CommandContextImpl> myModel2Context = new IdentityHashMap<>();
/**/CommandContextProvider() {
}
void engage() {
assert !myEngaged;
myEngaged = true;
}
void discard() {
myModel2Context.values().forEach(CommandContextImpl::onCommandOver);
myModel2Context.clear();
assert myEngaged;
myEngaged = false;
}
ModelCommandContext get(SModel model, UndoHandler undoHandler) {
if (myEngaged) {
return myModel2Context.computeIfAbsent(model, m -> new CommandContextImpl(undoHandler, m));
}
return null;
}
}
private static class CommandContextImpl implements ModelCommandContext {
private final UndoHandler myUndoHandler;
private final SModel myModel;
private final UnregisteredNodes myUN;
private ImmatureReferences myIR;
public CommandContextImpl(@Nullable UndoHandler undoHandler, /*NotNull*/ SModel m) {
myUndoHandler = undoHandler == null ? new DefaultUndoHandler() : undoHandler;
myModel = m;
myUN = new UnregisteredNodes(myModel.getReference());
}
@Override
public void nodeAttached(/*NotNull*/ SNode node) {
myUN.remove(node);
}
@Override
public void nodeDetached(/*NotNull*/ SNode node) {
myUN.put(node);
}
@Override
public void associationSet(SNode node, SReferenceLink link, AssociationData association) {
if (association != null && association.isDirectNode()) {
if (myIR == null) {
myIR = new ImmatureReferences();
}
myIR.add(node, link);
}
}
@Nullable
@Override
public SNode resolveUnregistered(SNodeId nodeId) {
return myUN.get(myModel.getReference(), nodeId);
}
@Override
public void registerActionWithUndo(SNodeUndoableAction action) {
myUndoHandler.addUndoableAction(action);
}
@Override
public void registerActionWithUndo(ModelRenameUndoableAction action) {
myUndoHandler.addUndoableAction(action);
}
/*package*/void onCommandOver() {
if (myIR != null) {
myIR.cleanup();
}
}
}
}