-
Notifications
You must be signed in to change notification settings - Fork 183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Parameter/variable name lookup confusion #659
Comments
The script calls line 23 every iteration but only The parameter name of the |
Your mental interpreter did not execute this correctly. If you try running it you will see that the container is created when I agree that the name of the parameter to the |
Here is a revised minimal test script that isolates the problem a bit better. No loops, no if's to distract the focus. test1(String c) { // Change String c to String d and it works
return this;
}
container() {
System.err.println("allocate container");
ylds = new ArrayList();
return this;
}
Object c = null;
{
{
c = container();
}
// c = container(); // uncomment this line and it works correctly
c.ylds.add(test1("hello")); // Line 17, Error here. Comment out and it will work
}
c.ylds.add(test1("hello")); // Line 19, This works Something happens when allocations are made within block namespaces. Note that as the comments state that this script can be modified to work correctly by either
You don't have to change more than one condition. Any of these three changes independently will avoid the error. Of course this is a a toy problem and you could just say "get rid of the blocks, you don't need them, that will fix your problem". True, but think of them as placeholders for more complex structures (such as loops and conditionals) that are commonly required for everyday tasks. |
Can reproduce: allocate container
// Error: Evaluation Error: Attempt to resolve method: add() on undefined variable or class name: c.ylds : at Line: 18 : in file: <unknown file> : c .ylds .add ( test1 ( "hello" ) )
Called from method: global/BlockNameSpace : at Line: 18 : in file: <unknown file> : test1 ( "hello" ) |
Parent #660 |
A method inherited from a parent namespace refers to a variable changed in the local namespace, we should override the method declared namespace with the child's namespace. Recurse test was affected because the black namespaces did not correctly inherit their parent's isMethod state. Improved the debugging output for recurse.bsh.
Make sure that invokeMethod does not set overrideChild flag to true, methods need to have their own namespaces. Make enumBlocks local so to be thread safe Format and comment code Make sure that namespaces are removed off the stack Adjust tests accordingly.
Merge of work done at and resolves beanshell#672 Fix issues with block name space cache may help beanshell#655 Fix beanshell#659 methods returning This reference Fix beanshell#676 with additional tests Fixes beanshell#508 again commands to exclude namespace overrid Improve for, enhanced for, while, and try-catch use of block name spaces Added more debug info for methods, commands and variables Fixed some generics for class types Refactored this and super implementations and addressed commented concerns
Merge of work done at and resolves beanshell#672 Fix issues with block name space cache may help beanshell#655 Fix beanshell#659 methods returning This reference Fix beanshell#676 with additional tests Fixes beanshell#508 again commands to exclude namespace overrid Improve for, enhanced for, while, and try-catch use of block name spaces Added more debug info for methods, commands and variables Fixed some generics for class types Refactored this and super implementations and addressed commented concerns
Merge of work done at and resolves beanshell#672 Fix issues with block name space cache may help beanshell#655 Fix beanshell#659 methods returning This reference Fix beanshell#676 with additional tests Fixes beanshell#508 again commands to exclude namespace overrid Improve for, enhanced for, while, and try-catch use of block name spaces Added more debug info for methods, commands and variables Fixed some generics for class types Refactored this and super implementations and addressed commented concerns
There is still a problem here that is failing in my real world test. I have tried to make some test cases unfortunately I cannot seem to make one to reproduce the problem. I suspect it has something to do with caching and the isMethod flag. |
So I see that when a new BlockNameSpace is created for the first time the isMethod value from the parent is propogated. Is this something that needs to also be checked when recycling a BlockNameSpace object from the cache? |
@nickl- Ok, I was able to isolate the problem that I am having and reduce it in to a small test case: Object closure1() {
list = new ArrayList();
return this;
}
double doesntMatter() {
return 1;
}
try {
t = closure1();
System.err.println(t.list.size());
} catch (Exception ex) {
ex.printStackTrace();
} Running the above script results in:
I don't have an explanation and I would be only guessing to try to provide one. |
How do you eat an elephant? |
I don't think that this is the right analogy. An elephant is a big but well-defined and finite thing. When you have to eat an elephant you know that there is a big task ahead and you can plan out how to break the problem down into bite sized pieces in order to get it done. Generally there is steady forward progress. But this thing with the namespace recycling isn't like that. It's more like an an iceberg: you are only seeing the tip of it and are addressing one small piece at a time in a piecemeal fashion. There is more under the surface that has not been revealed yet. As more parts of the problem become revealed and you can see that there is a different shape compared to what you first thought. What initially seemed like a good approach when you only saw the tip of the iceberg might end up being the wrong approach when you see more of the problem. Maybe it's time to take a look at what is emerging. First of all let's acknowledge that the bugs that we are seeing did not manifest before the ReferenceCache was added four years ago c42361a. We can agree on this, yes? The ReferenceCache is intended to avoid the cost of object allocation and garbage collection, in other words to outsmart the gc. This kind of object recycling can sometimes improve performance in specific cases but it is easy to overestimate the benefits and underestimate the difficulty of getting it right. Your approach to implementing namespace recycling seems to have been: "initially implement it for almost all namespace allocations, then incrementally back out special cases when breakage occurs", and this has ended up being a game of whack-a-mole. Have you thought about tackling this differently, such as: "identify the special cases where namespace recycling is proven to be not a problem, and only implement it for those cases"? For example, with anonymous class and scripted objects all namespaces from the declaring namespace up to the topmost parent cannot be recycled. So let's flip your approach around, and say that only namespaces where there are no descendants (children, grandchildren, etc) that leak If you did it this way you would have an elephant. The way it is now is more like the gift that keeps on giving. |
Also, if you have a step after the parse to go through the AST you can do interesting things like constant folding, dead code elimination and semantic analysis. It looks like semantic analysis will be required for the lambda implementation, but it is also useful for checking things like |
Just to add some more clarity, the |
You eat an elephant bite for bite. We will get through this elephant too... Thanx for the ideas, will need pondering before I remark. |
As per discussions on beanshell#659 made block name space cache optional and only implemented it for the loops, for, for enhanced, and while. Refactored and simplified loop node implementations which was overly complex due to the use of a switch statement to deal with control statements making break unusable. Improved coverage on the loop nodes also implementing the Thread interrupt which the loops are respecting.
Ok so after pondering the suggestions I opted to make the block name space cache optional. Using a Boolean type for the override name space flag it can define 3 states:
The cached name space was then only implemented for the loops, for, for enhanced, and while, as they are the ones reaping most of the benefit since the same block definitely gets processed multiple times. They also have closed scope so no need to worry about anything else needing or reusing the block name space. If we find any other blocks that might benefit from a cache strategy we can implement it for them on a case by case basis. For the loops doing 100000 repetitions there is a 500% performance improvement using the cache compared to going without. |
However even though we are now creating new block name space instances un-cached for all the blocks in the example at: #659 (comment) it still doesn't work. So this particular problem obviously lies elsewhere. |
Fixes beanshell#707 generated return types are reloaded Fixes beanshell#708 generated parameter types are reloaded Related to beanshell#659 improved scripted object detection, fixes is method bug
Fixes beanshell#707 generated return types are reloaded Fixes beanshell#708 generated parameter types are reloaded Related to beanshell#659 improved scripted object detection, fixes is method bug
Fixes beanshell#707 generated return types are reloaded Fixes beanshell#708 generated parameter types are reloaded Related to beanshell#659 improved scripted object detection, fixes is method bug
Fixes beanshell#707 generated return types are reloaded Fixes beanshell#708 generated parameter types are reloaded Related to beanshell#659 improved scripted object detection, fixes is method bug
The doesntMatter bug is also resolved now: beanshell/src/test/resources/test-scripts/scripted_object2.bsh Lines 6 to 44 in 7a5f878
|
Closed: all reported issues are now resolved...until further notice |
The problem still exists. I have a somewhat complicated script, but here is where the failure occurs: for (int i=0; i<4; i++) {
if (i>=4) // <----- this is line 386
System.err.println("i="+i); Here is the error message that I get:
I will try to reduce this to a test script, but it does seem a bit random in that the unchanged script will fail in different places on successive runs. The failing script had run for several minutes doing quite a bit of I/O and repeat method calls. I am guessing that the seemingly random nature of the error might have to do with some kind of reference cache reuse issue. |
I spent the past few days trying to crack this nut, but with no success. I instrumented the code, followed traces, etc, but I cannot seem to figure out what is going wrong. I tried an experiment and commented out the call to get recycled namespaces from the reference cache in BSHBLock. Previously it looked like: if ( null == overrideNamespace )
enclosingNameSpace = callstack.swap(
BlockNameSpace.getInstance(callstack.top(), blockId));
else if ( !overrideNamespace )
enclosingNameSpace = callstack.swap(
new BlockNameSpace(callstack.top(), blockId));
else enclosingNameSpace = null; and I changed it to if ( null == overrideNamespace || !overrideNamespace )
enclosingNameSpace = callstack.swap(
new BlockNameSpace(callstack.top(), blockId));
else enclosingNameSpace = null; This mostly gets rid of the recycled namespaces (they are still in use in enhanced for loops). After making the change the error about disappearing variables went away. I cannot explain what is going on, but at least there is a smoking gun. If you have any suggestions on what to look for I can try again. |
I have a reproducible test case that demonstrates a symptom. I don't know if this is the root of the problem but it seems like it might be related. I tried running the bshdoc tool bshdoc.bsh.txt and got the following error:
The coding style in this file is out of date, but I am not sure why the error occurs. The namespace names that are shown in the error message are possibly a bit suspect. For example, shouldn't line 173 be called from the global namespace? |
I reduced the above problem down to a simple reproducible example and opened a new issue #731. |
Nope this looks like it gets rid of them completely. Doesn't the loops pass null for exactly this reason? |
This is strange, if it was another variable it may make sense but I am sure we update |
That is very good news indeed!!! Wheeee \o/ |
Here is an interesting traceback:
The code at the point of error is public static NameSpace getInstance(NameSpace parent, int blockId ) {
BlockNameSpace ns = (BlockNameSpace) blockspaces.get(
new UniqueBlock(parent, blockId));
if (1 < ns.used.getAndIncrement()) ns.clear(); // Line 95
return ns;
} I can't see an obvious reason for
But always The reference monitor queue is running in a separate thread and may have reaped the the reference entry just as it was being retrieved. Or maybe the key was retrieved in the program thread and the gc thread kicked in and cleared the value just before the entry was dereferenced. Or perhaps it was related to the lazy instantiation. There are a few complicated interactions here and possibly a window where it is not protected from concurrent operations. Maybe this is at the root of the problems that I have been seeing with variables disappearing during long running scripts. Perhaps the timings lined up that a reference unexpectedly got cleared out of sequence. They seemed like non-deterministic (timing-based) errors. Hitting this error is a rare thing. If it is happening in the ReferenceCache and it is related to concurrent operations then it will be very hard (impossible?) to write a reproducible test case. Any suggestions on where to look or how to test? |
I spent a bit more time looking at the implementation of ReferenceCache and how it is used to cache BlockNameSpace objects, and I found a few problems. The first obvious one is that the blockspaces cache is declared with weak keys and weak values. But in the call to The next one is due to the fact that the values are held in the cache as weak references and don't get a chance to be bound to a hard reference until after I cannot see an easy way to alter this ReferenceCache scheme to make it fully reliable. Running the executor task in one thread and then retrieving the result in the other thread leaves a tiny window of time when there are no hard references to the newly allocated BlockNameSpace, so there will always be a risk of the value reference being cleared and the Besides these issues there are a few others that I mentioned previously. Value references are enqueued to the Is there a strong reason to be storing the BlockNameSpace objects in a map by namespace and sequence count? Aren't BlockNameSpace objects substitutable because they are being fully cleared before reuse? What state do they contain that cannot easily and cheaply be restored ( |
Also, the @Override
public boolean equals(Object o) {
if (o == this)
return true;
else if (o instanceof UniqueBlock) {
UniqueBlock ub = (UniqueBlock) o;
return ns == ub.ns &&
id == ub.id;
} else
return false;
} However, it looks like the equality test is overriden in the public boolean equals(final Object obj) {
return hashCode == obj.hashCode();
} The hashcode calculation is: hashCode = key.hashCode() + key.toString().chars().sum(); This is unsafe because it is possible for two different objects to have the same hash code. Maybe this is allowing hash collisions and the wrong namespace objects getting retrieved. |
Happy day, I think that I found the problem! I am pretty sure that it is what I mentioned previously about the equality test being overridden by a weak hashcode comparison, and this was allowing collisions to happen. I implemented a simpler weak value reference hashmap and it repeatedly runs successfully on my long running test cases. Of course with intermittent non-deterministic bugs it is hard to conclusively say that the bug has been squashed, but I am feeling pretty confident because
I will clean up my code and post a PR when I get a chance. |
I also had a look at the MemberCache implementation that uses the ReferenceCache and it also has a few problems. The MemberCache uses class references as keys. In normal practice the class references will not be collected by the gc unless the ClassLoader that they come from is also gc'ed. This means that for most situations the soft references on the keys will never be cleared. It is possible under memory pressure for the soft value references to the MemberCache objects to be gc'ed and cleared, but entries in the map will never be removed because the overridden Also, it looks like that the Executor is not fully effective at parallelizing the instantiation of the MemberCache values. The issue shows up in the MemberCache constructor: public MemberCache(Class<?> clazz) {
Class<?> type = clazz;
while (type != null) {
if (isPackageAccessible(type)
&& ((isPackageScope(type) && !isPrivate(type))
|| isPublic(type) || haveAccessibility())) {
for (Field f : type.getDeclaredFields())
if (isPublic(f) || haveAccessibility())
cacheMember(Invocable.get(f));
for (Method m : type.getDeclaredMethods())
if (isPublic(m) || haveAccessibility())
if (clazz == type) cacheMember(Invocable.get(m));
else cacheMember(memberCache.get(type) // line 127
.findMethod(m.getName(), m.getParameterTypes()));
for (Constructor<?> c: type.getDeclaredConstructors())
if (clazz == type) cacheMember(Invocable.get(c));
else cacheMember(memberCache.get(type) // line 131
.findMethod(c.getName(), c.getParameterTypes()));
}
processInterfaces(type.getInterfaces());
type = type.getSuperclass();
memberCache.init(type); // line 136
}
} I instrumented the code to see how often the main thread had to pause while waiting for the executor task to finish. The There is also the potential for the reference to get cleared in the gap between the executor completing the task and the get method returning the value, just like in the blockSpaces cache. This is a little bit less likely because the soft references require memory pressure, but there is a clear risk of returning a null. This is a problem that I cannot see fixing easily. I suggest to not use the ReferenceCache class for MemberCache objects and instead use something simpler like the value reference cache that I added in PR #741. |
Make sure that invokeMethod does not set overrideChild flag to true, methods need to have their own namespaces. Make enumBlocks local so to be thread safe Format and comment code Make sure that namespaces are removed off the stack Adjust tests accordingly.
Merge of work done at and resolves beanshell#672 Fix issues with block name space cache may help beanshell#655 Fix beanshell#659 methods returning This reference Fix beanshell#676 with additional tests Fixes beanshell#508 again commands to exclude namespace overrid Improve for, enhanced for, while, and try-catch use of block name spaces Added more debug info for methods, commands and variables Fixed some generics for class types Refactored this and super implementations and addressed commented concerns
As per discussions on beanshell#659 made block name space cache optional and only implemented it for the loops, for, for enhanced, and while. Refactored and simplified loop node implementations which was overly complex due to the use of a switch statement to deal with control statements making break unusable. Improved coverage on the loop nodes also implementing the Thread interrupt which the loops are respecting.
Fixes beanshell#707 generated return types are reloaded Fixes beanshell#708 generated parameter types are reloaded Related to beanshell#659 improved scripted object detection, fixes is method bug
As I was putting together a test script for #655 I noticed a problem with parameter/variable name lookups. The following script fails at line 23 with the error message:
Attempt to resolve method: add() on undefined variable or class name: c.ylds : at Line: 23
However, if I change the formal parameter name in the
test1
method fromc
tod
the script will work successfully.What am I missing here?
The text was updated successfully, but these errors were encountered: