How can I make external methods interruptable?

后端 未结 8 912
南旧
南旧 2021-02-05 02:32

The Problem

I\'m running multiple invocations of some external method via an ExecutorService. I would like to be able to interrupt these methods, but unfortunately the

相关标签:
8条回答
  • 2021-02-05 03:31

    You wrote:

    Another option I've tried is to mark myMethod() and similar methods with a special "Interruptable" annotation and then use AspectJ (which I am admittedly a newbie at) for catching all method invocations there - something like:

    @Before("call(* *.*(..)) && withincode(@Interruptable * *.*(..))")
    public void checkInterrupt(JoinPoint thisJoinPoint) {
        if (Thread.interrupted()) throw new ForcefulInterruption();
    }
    

    But withincode isn't recursive to methods called by the matching methods, so I would have to edit this annotation into the external code.

    The AspectJ idea is good, but you need to

    • use cflow() or cflowbelow() in order to recursively match a certain control flow (e.g. something like @Before("cflow(execution(@Interruptable * *(..)))")).
    • make sure to also weave your external library, not just you own code. This can be done by either using binary weaving, instrumenting the JAR file's classes and re-packaging them into a new JAR file, or by applying LTW (load-time weaving) during application start-up (i.e. during class loading).

    You might not even need your marker annotation if your external library has a package name you can pinpoint with within(). AspectJ is really powerful, and often there is more than one way to solve a problem. I would recommend using it because it was made for such endeavours as yours.

    0 讨论(0)
  • 2021-02-05 03:34

    An option is to:

    1. Make the VM connect to itself using JDI.
    2. Look up the thread that's running your task. This isn't trivial, but since you have access to all the stack frames, it is certainly doable. (If you put a unique id field in your task objects, you will be able to identify the thread that is executing it.)
    3. Stop the thread asynchronously.

    Although I don't think a stopped thread would seriously interfere with executors (they should be fail-safe after all), there is an alternative solution that doesn't involve stopping the thread.

    Provided that your tasks don't modify anything in other parts of the system (which is a fair assumption, otherwise you wouldn't be trying to shoot them down), what you can do is to use JDI to pop unwanted stack frames off and exit the task normally.

    public class StoppableTask implements Runnable {
    
    private boolean stopped;
    private Runnable targetTask;
    private volatile Thread runner;
    private String id;
    
    public StoppableTask(TestTask targetTask) {
        this.targetTask = targetTask;
        this.id = UUID.randomUUID().toString();
    }
    
    @Override
    public void run() {
        if( !stopped ) {
            runner = Thread.currentThread();
            targetTask.run();
        } else {
            System.out.println( "Task "+id+" stopped.");
        }
    }
    
    public Thread getRunner() {
        return runner;
    }
    
    public String getId() {
        return id;
    }
    }
    

    This is the runnable that wraps all your other runnables. It stores a reference to the executing thread (will be important later) and an id so we can find it with a JDI call.

    public class Main {
    
    public static void main(String[] args) throws IOException, IllegalConnectorArgumentsException, InterruptedException, IncompatibleThreadStateException, InvalidTypeException, ClassNotLoadedException {
        //connect to the virtual machine
        VirtualMachineManager manager = Bootstrap.virtualMachineManager();
        VirtualMachine vm = null;
        for( AttachingConnector con : manager.attachingConnectors() ) {
            if( con instanceof SocketAttachingConnector ) {
                SocketAttachingConnector smac = (SocketAttachingConnector)con;
                Map<String,? extends Connector.Argument> arg = smac.defaultArguments();
                arg.get( "port" ).setValue( "8000");
                arg.get( "hostname" ).setValue( "localhost" );
                vm = smac.attach( arg );
            }
        }
    
        //start the test task
        ExecutorService service = Executors.newCachedThreadPool();
        StoppableTask task = new StoppableTask( new TestTask() );
        service.execute( task );
        Thread.sleep( 1000 );
    
        // iterate over all the threads
        for( ThreadReference thread : vm.allThreads() ) {
            //iterate over all the objects referencing the thread
            //could take a long time, limiting the number of referring
            //objects scanned is possible though, as not many objects will
            //reference our runner thread
            for( ObjectReference ob : thread.referringObjects( 0 ) ) {
                //this cast is safe, as no primitive values can reference a thread
                ReferenceType obType = (ReferenceType)ob.type();
                //if thread is referenced by a stoppable task
                if( obType.name().equals( StoppableTask.class.getName() ) ) {
    
                    StringReference taskId = (StringReference)ob.getValue( obType.fieldByName( "id" ));
    
                    if( task.getId().equals( taskId.value() ) ) {
                        //task with matching id found
                        System.out.println( "Task "+task.getId()+" found.");
    
                        //suspend thread
                        thread.suspend();
    
                        Iterator<StackFrame> it = thread.frames().iterator();
                        while( it.hasNext() ) {
                            StackFrame frame = it.next();
                            //find stack frame containing StoppableTask.run()
                            if( ob.equals( frame.thisObject() ) ) {
                                //pop all frames up to the frame below run()
                                thread.popFrames( it.next() );
                                //set stopped to true
                                ob.setValue( obType.fieldByName( "stopped") , vm.mirrorOf( true ) );
                                break;
                            }
                        }
                        //resume thread
                        thread.resume();
    
                    }
    
                }
            }
        }
    
    }
    }
    

    And for reference, the "library" call I tested it with:

    public class TestTask implements Runnable {
    
        @Override
        public void run() {
            long l = 0;
            while( true ) {
                l++;
                if( l % 1000000L == 0 )
                    System.out.print( ".");
            }
    
        }
    }
    

    You can try it out by launching the Main class with the command line option -agentlib:jdwp=transport=dt_socket,server=y,address=localhost:8000,timeout=5000,suspend=n. It works with two caveats. Firstly, if there is native code being executed (thisObject of a frame is null), you'll have to wait until it's finished. Secondly, finally blocks are not invoked, so various resources may potentially be leaking.

    0 讨论(0)
提交回复
热议问题