Unexpected behaviour skips Agent.send method second time through when delegating methods to Agent protected value

65 views Asked by At

I have been trying to do a small Groovy project, and wanted a ConcurrentLinkedHashSet, but Java doesn't provide one. So I set about creating my own using a Gpars agent to 'protect' an ordinary LinkedHashSet. I then created my wrapper class to hold the agent, and delegated the methods on my class to the internal Agent like this (this version is using methodMissing as delegation approach). I tried Groovy interceptable/invokeMethod and could get it to work either

class ConcurrentLinkedHashSet /*implements GroovyInterceptable*/  {

    Agent $concurrentHashSet = new Agent (new LinkedHashSet())

    /*Object[] toArray () {
        $concurrentHashSet.val.asType(LinkedHashSet).toArray()
    }*/

    def asType (Set) {
        $concurrentHashSet.val
    }

    //set up delegation action to pass all methods to the agent to execute
    def /*invokeMethod*/methodMissing (String name, args){
        def result
        System.out.println "methodMissing with $name and $args on $this"
        System.out.println "agent value is : ${$concurrentHashSet.val.dump()} is instance of: ${$concurrentHashSet.getClass()}"
        $concurrentHashSet {

            System.out.println "\t\tconcHashSet methodMissing: it is of type ${it.getClass()} so called $it.invokemethod ($name, $args) "
            if (args == []) {
                System.out.println "\t\tconcHashSet methodMissing: invoke method '$name' with no args "
                result = it."$name"()//it.invokeMethod (name)
            } else {
                System.out.println "\t\tconcHashSet methodMissing: invoke method '$name' with args $args"
                result = it.invokeMethod(name, *args)//"$name" (*args)//it.invokeMethod(name, args)
            }
            System.out.println "\tconcHashSet methodMissing: 'it' is now showing as >  '$it'"
            "success"
       }
        //System.out.println "agent protected value is now : " + $concurrentHashSet.val + " and result now $result"
        System.out.println "agent protected value is now : " + $concurrentHashSet.val.dump() + " and result now $result"
        $concurrentHashSet.val
    }
}

however when I try to use this - it works first time through, my strings is added, but on the second call on the same missing method the agent.send call is never made, and gets skipped.

So my simple script consumer looks like this

// delegates add method via agent.send first time through but not after !
ConcurrentLinkedHashSet conhs = new ConcurrentLinkedHashSet ()

conhs.add ('col1')
println "1st conHashSet as list : ${conhs.toArray()}"

conhs.add ('col2')
println "2nd conHashSet as list : ${conhs.toArray()}"


// direct calls on an agent using invokeMethod 
Agent myHash = new Agent (new LinkedHashSet())
myHash {it.invokeMethod ('add',  'col1')}
println "1st agentHashSet as list : ${myHash.val.toArray()}"

myHash {it.invokeMethod ('add','col2')}
println "2nd agentHashSet as list : ${myHash.val.toArray()}"

and my simple trace log looks like this on the console output

methodMissing with add and [col1] on org2.softwood.rules.utils.ConcurrentLinkedHashSet@3b0090a4
agent value is : <java.util.LinkedHashSet@0 map=[:]> is instance of: class groovyx.gpars.agent.Agent
        concHashSet methodMissing: it is of type class java.util.LinkedHashSet so called [] (add, [col1]) 
        concHashSet methodMissing: invoke method 'add' with args [col1]
    concHashSet methodMissing: 'it' is now showing as >  '[col1]'
agent protected value is now : <java.util.LinkedHashSet@2eaeb1 map=[col1:java.lang.Object@6a2f6f80]> and result now true
methodMissing with toArray and [] on org2.softwood.rules.utils.ConcurrentLinkedHashSet@3b0090a4
agent value is : <java.util.LinkedHashSet@2eaeb1 map=[col1:java.lang.Object@6a2f6f80]> is instance of: class groovyx.gpars.agent.Agent
agent protected value is now : <java.util.LinkedHashSet@2eaeb1 map=[col1:java.lang.Object@6a2f6f80]> and result now null
1st conHashSet as list : [col1]
methodMissing with add and [col2] on org2.softwood.rules.utils.ConcurrentLinkedHashSet@3b0090a4
agent value is : <java.util.LinkedHashSet@2eaeb1 map=[col1:java.lang.Object@6a2f6f80]> is instance of: class groovyx.gpars.agent.Agent
agent protected value is now : <java.util.LinkedHashSet@2eaeb1 map=[col1:java.lang.Object@6a2f6f80]> and result now null
methodMissing with toArray and [] on org2.softwood.rules.utils.ConcurrentLinkedHashSet@3b0090a4
agent value is : <java.util.LinkedHashSet@2eaeb1 map=[col1:java.lang.Object@6a2f6f80]> is instance of: class groovyx.gpars.agent.Agent
agent protected value is now : <java.util.LinkedHashSet@2eaeb1 map=[col1:java.lang.Object@6a2f6f80]> and result now null
2nd conHashSet as list : [col1]
1st agentHashSet as list : [col1]
2nd agentHashSet as list : [col1, col2]

As you can see on the first attempt at delegation you can see the 'concHashSet methodMissing:' trace and the agent calls invokeMethod on it in the agent to effect adding the element.

On the second call to conhs.add ('col2') the agent.sand call never happens and so my extra item never gets added.

This is annoying as I thought I had an easy way to create my ConcurrentLinkedHashSet, but the code doesn't work. What mechanism could I use to get the right outcome?

As you can see when I invokeMethod (add) directly on an Agent<LinkedHashSet> it works just fine. In my real consuming class if I replace my ConcurrentLinkedHashSet with an ordinary LinkedHashSet it works a dream, but isn't thread safe. I wanted create a thread safe version which depended on trying to get this to work.

I guess I could try and replace the Agent and just use synchronize blocks round my LinkedHashSet - but its a bit ugly - I thought the Gpars Agent would sort all this for me as a general solution pattern as a wrapper with delegation.

PS I tried another tack and this sort of works I think, but doesn't feel right - it uses @Synchronise on invokeMethod on class that implements GroovyInterceptable to achieve a thread safe call when delegating. I am not sure if this truly thread safe or not.

class ConcurrentLinkedHashSet2 implements GroovyInterceptable{
@Delegate private LinkedHashSet $mySet = new LinkedHashSet()

   @Synchronized
    def invokeMethod (String name, args) {
        System.out.println "call to $name intercepted invoke using metaclass invoke"

        ConcurrentLinkedHashSet2.metaClass.getMetaMethod(name).invoke (this, *args)
    }
}
2

There are 2 answers

2
Vaclav Pech On

It works as expected after commenting out the trace output: System.out.println "\t\tconcHashSet methodMissing: it is of type ${it.getClass()} so called $it.invokemethod ($name, $args) "

This line causes an unhandled exception to be thrown from the agent, which terminates the agent.

0
WILLIAM WOODMAN On

Vaclav spotted my error - my System.out.println had an error that caused an exception inside the body of the agent.

So I've built a corrected version of my original example:

/**
 * using methodMissing to delegate calls
 *
 * Created by William on 29/12/2016.
 */

    
class ConcurrentLinkedHashSet2 implements GroovyInterceptable {
    Agent $concurrentHashSet = new Agent (new LinkedHashSet())

    //set up delegation action to pass all methods to the agent to execute
    def invokeMethod (String name, args){
        DataflowVariable invokeResult = new DataflowVariable()
         $concurrentHashSet {
             invokeResult << it?.invokeMethod(name, args)
        }
        invokeResult.val
    }

}

This led to a more generalised answer in the Genie class - I hope this maybe of use to others. In this version decided to delegate equals, and toString to wrapped variable, as class was hanging, this fixes that. This version takes any Class or an instance variable and wraps it with thread safe Agent, and then delegates method calls to the wrapped variable - hence the 'Genie' moniker.

/**
 * Thread safe container for instance of any type, where the
 * genies invokeMethod is used to delegate calls to agent protected
 * value.  If the genie provides local methods, then the
 * delegation calls the local method
 *
 * Created by William on 29/12/2016.
 */


class Genie<T> implements GroovyInterceptable {
    Agent $concurrentVariable = new Agent ()

    Genie (Class clazz) {
        assert clazz instanceof Class
        def instance = clazz.newInstance()
        $concurrentVariable {
            updateValue (instance)
        }
        def ref = $concurrentVariable.val
        assert clazz.is (ref.getClass() )
    }

    Genie (instanceVariable) {
        $concurrentVariable { updateValue (instanceVariable)}
    }

    void setVariable (instanceVariable) {
        $concurrentVariable { updateValue (instanceVariable)}
    }

    def getVariable () {
        $concurrentVariable.val
    }

    def getWrappedClass () {
        $concurrentVariable.val?.getClass()
    }

    //set up delegation action to pass all methods to the agent to execute
    def invokeMethod (String name, args) throws MissingMethodException {
        DataflowVariable invokeResult = new DataflowVariable()
        //if holding reference to a null then just try the call on the Genie instance
        if ($concurrentVariable.val == null) {
            invokeResult << this.metaClass.getMetaMethod("$name", args).invoke (this, args)
            return invokeResult.val
        }

        //else look and see if method is expected to be called on the Genie instance
        def listOfMethods = this.metaClass.getMethods().collect {it.name}
        //we want delegation of toString() and equals(), and hashCode() to wrapped variable so remove from check of methods on Genie
        listOfMethods.remove ("toString")
        listOfMethods.remove ("equals")
        listOfMethods.remove ("hashCode")
        boolean methodMatched = listOfMethods.find {it == name}
        if (methodMatched && this instanceof Genie) {
            //see if there's a local method in Genie in scope, then call it
            invokeResult << this.metaClass.getMetaMethod("$name", args).invoke (this, args)
        } else {
            def method = $concurrentVariable.val.metaClass.getMetaMethod(name, args)
            if (method == null) {
                invokeResult << $concurrentVariable.val.metaClass.invokeMissingMethod($concurrentVariable.val, name, args)
            } else {
                //delegate the method to the variable wrapped by the Agent<T>
                $concurrentVariable {
                    MetaMethod validMethod = it.metaClass.getMetaMethod(name, args)
                    if (validMethod) {
                        invokeResult << validMethod.invoke(it, args)
                    } else invokeResult << null
                }
            }
        }
        invokeResult.val
    }

}