Bug 520041 - Hot code replace does not replace all instances in multi-classloader scenario
Summary: Hot code replace does not replace all instances in multi-classloader scenario
Status: REOPENED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: Debug (show other bugs)
Version: 4.6   Edit
Hardware: All All
: P3 normal (vote)
Target Milestone: ---   Edit
Assignee: JDT-Debug-Inbox CLA
QA Contact:
URL:
Whiteboard:
Keywords: helpwanted
Depends on:
Blocks:
 
Reported: 2017-07-21 16:08 EDT by Michael Schierl CLA
Modified: 2024-02-20 18:12 EST (History)
2 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Michael Schierl CLA 2017-07-21 16:08:16 EDT
To Reproduce:

1. Create two Java Projects called P1 and P2 in a (new) workspace. Do not set up any dependencies between them.

2. Paste this class to P1 project:

////////////////////////////////////
package p1package;

import java.io.*;
import java.net.*;
import java.util.*;

public class P1Class {
	public static void main(String[] args) throws Exception {
		List<Class<?>> classes = new ArrayList<>();
		while (true) {
			System.out.println("====");
			for (Class<?> clazz : classes) {
				clazz.newInstance();
			}
			try (URLClassLoader ucl = new URLClassLoader(new URL[] { new File("../P2/bin").toURI().toURL() })) {
				classes.add(ucl.loadClass("p2package.P2Class"));
			}
			Thread.sleep(5000);
		}
	}
}
////////////////////////////////////

3. Paste this small class to P2 project:

////////////////////////////////////
package p2package;

public class P2Class {
	public P2Class() {
		System.out.println("Change Me!");
	}
}
////////////////////////////////////

4. Run the P1 class in debugger. While it is running (wait until it prints at least 3 lines first), change and save the P2 class (change the text). 


Expected behaviour: The program pauses for 5 seconds. When editing within these 5 seconds, the output of one batch after the pause should output all identical strings, as all classes have been Hot Code replaced with the same new bytecode.

Actual behaviour: Most of the time, at least one of the printed lines in a batch still holds the old value. When not touching the file afterwards, this one line will remain being different "forever". Therefore I conclude that Hot Code replace must have missed one of the instances.


Tested with both Java 7 and Java 8, behaviour is consistent.
Comment 1 Andrey Loskutov CLA 2017-07-24 09:40:08 EDT
Did it ever worked for you? I see same behavior in 4.6.3 on OpenJDK 1.8.0_131.

I honestly don't know semantic details of the JVM HCR implementation and classloader magic, but it looks like we call com.sun.jdi.VirtualMachine.redefineClasses(Map<? extends ReferenceType, byte[]>) with right argument, so probably there are some problems in the JVM to re-define classes with *same name* loaded at *same time* in different classloader instances. May be even a bug in the JVM? Have you tried to search the sun/oracle bug database?
Comment 2 Michael Schierl CLA 2017-07-24 15:28:04 EDT
I cannot tell you how old this bug is, only that a colleague of mine has been seeing it for a few years already when debugging webapps running in Tomcat. As it seems to depend on how you interact with the application, he has run into the issue more often than me - I have seen and reproduced this bug last week for the first time.

To rule out JDI issues, I tried my own quick&dirty (definitely *not* production quality) JDI debugger mockup, and it seems that as long as you ignore the event queue and only redefine classes that are returned by vm.allClasses(), it behaves like Eclipse behaves currently.

But when adding a ClassPrepareRequest matching the class name, and redefining the newly prepared classes whenever the ClassPrepareRequest fires, I can no longer reproduce the issue.


I am not sure whether this is a bug or a (missing) feature, i. e. whether JDI guarantees that all classes that have already started loading are included in allClasses() or not, and I also do not know whether it is feasible in Eclipse to add a ClassPrepareRequest for every class where hot code was replaced.


For reference, here is my quick & dirty POC (add to a third project in the same workspace and run it:

////////////////////////////////////
package jdimaster;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import com.sun.jdi.Bootstrap;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.Connector.Argument;
import com.sun.jdi.connect.LaunchingConnector;
import com.sun.jdi.event.ClassPrepareEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.request.ClassPrepareRequest;

public class JDITest {
	public static void main(String[] args) throws Exception {
		Path classFile = new File("../P2/bin/p2package/P2Class.class").toPath();
		final byte[] classBytes = Files.readAllBytes(classFile);
		int stringPos = new String(classBytes, StandardCharsets.ISO_8859_1).indexOf("Change ") + 7;
		VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
		LaunchingConnector connector = vmm.defaultConnector();
		Map<String, Argument> cArgs = connector.defaultArguments();
		cArgs.get("options").setValue("-cp ../P1/bin");
		cArgs.get("main").setValue("p1package.P1Class");
		final VirtualMachine vm = connector.launch(cArgs);
		forward(vm.process().getInputStream(), System.out);
		forward(vm.process().getErrorStream(), System.err);
		ClassPrepareRequest req = vm.eventRequestManager().createClassPrepareRequest();
		req.addClassFilter("p2package.P2Class");
		req.enable(); // remove this to have Eclipse's current behaviour
		vm.resume();
		EventQueue q = vm.eventQueue();
		new Thread(() -> {
			try {
				while (true) {
					for (Event e : q.remove()) {
						if (e instanceof ClassPrepareEvent) {
							ClassPrepareEvent cpe = (ClassPrepareEvent) e;
							Map<ReferenceType, byte[]> map = new HashMap<>();
							map.put(cpe.referenceType(), classBytes);
							vm.redefineClasses(map);
							vm.resume();
						}
						System.out.println(">>> " + e.toString());
					}
				}
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}).start();
		for (int i = 0;; i++) {
			Thread.sleep(7000);
			classBytes[stringPos] = (byte) ('0' + (i / 10 % 10));
			classBytes[stringPos + 1] = (byte) ('0' + (i % 10));
			Files.write(classFile, classBytes);
			Map<ReferenceType, byte[]> types = vm.allClasses().stream()
					.filter(rt -> rt.name().equals("p2package.P2Class"))
					.collect(Collectors.<ReferenceType, ReferenceType, byte[]>toMap(rt -> rt, rt -> classBytes));
			vm.redefineClasses(types);
			System.out.println(">>> Redefined " + types.size());
		}
	}

	private static void forward(InputStream in, PrintStream out) throws IOException {
		new Thread(() -> {
			try {
				byte[] buf = new byte[4096];
				int len;
				while ((len = in.read(buf)) != -1) {
					out.write(buf, 0, len);
					out.flush();
				}
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}).start();
	}
}
////////////////////////////////////
Comment 3 Eclipse Genie CLA 2020-02-26 13:07:00 EST
This bug hasn't had any activity in quite some time. Maybe the problem got resolved, was a duplicate of something else, or became less pressing for some reason - or maybe it's still relevant but just hasn't been looked at yet. As such, we're closing this bug.

If you have further information on the current state of the bug, please add it and reopen this bug. The information can be, for example, that the problem still occurs, that you still want the feature, that more information is needed, or that the bug is (for whatever reason) no longer relevant.

--
The automated Eclipse Genie.
Comment 4 Michael Schierl CLA 2020-02-26 14:44:14 EST
This bug is still present and relevant. In fact, I've hit it multiple times this month, and last time yesterday. I kind of learned to live with it and restart the debugging session when it happens, but I'm not happy about it since it sometimes takes 15 minutes or more to get back to the point where I started.

In fact, of all the "stalebugs" this is the most relevant for me, and it is one of the points which makes me reconsider switching to IntelliJ sometimes (but perhaps I should have done so long time ago instead of watching Eclipse die a slow death with no real innovation any more...)
Comment 5 Sarika Sinha CLA 2020-02-27 00:05:48 EST
Anyone willing to work on this?
Comment 6 Eclipse Genie CLA 2022-02-17 05:52:27 EST
This bug hasn't had any activity in quite some time. Maybe the problem got resolved, was a duplicate of something else, or became less pressing for some reason - or maybe it's still relevant but just hasn't been looked at yet.

If you have further information on the current state of the bug, please add it. The information can be, for example, that the problem still occurs, that you still want the feature, that more information is needed, or that the bug is (for whatever reason) no longer relevant.

--
The automated Eclipse Genie.
Comment 7 Michael Schierl CLA 2022-02-17 16:50:39 EST
Bug is still relevant.

[It is slightly creepy as I got hit by that exact bug at work today, and talked with a colleague about it (that I opened a bug report at Eclipse some years ago for it). Now when I come home and check my private email address, I see the Eclipse Genie message...]
Comment 8 Eclipse Genie CLA 2024-02-20 01:40:33 EST
This bug hasn't had any activity in quite some time. Maybe the problem got resolved, was a duplicate of something else, or became less pressing for some reason - or maybe it's still relevant but just hasn't been looked at yet.

If you have further information on the current state of the bug, please add it. The information can be, for example, that the problem still occurs, that you still want the feature, that more information is needed, or that the bug is (for whatever reason) no longer relevant.

--
The automated Eclipse Genie.
Comment 9 Michael Schierl CLA 2024-02-20 18:12:37 EST
Bug is still relevant.

Happened to me about a month ago on latest stable Eclipse. Was quite obvious as I had added a line before and the breakpoint stopped on an empty line. So I don't even need to actually try to reproduce it.