Skip to content

Commit d35a426

Browse files
committed
Fix getMetaInfo when another process holds the lock, Fixes #79
1 parent ac876e3 commit d35a426

File tree

3 files changed

+183
-5
lines changed

3 files changed

+183
-5
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package net.openhft.affinity.common;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import java.io.ByteArrayOutputStream;
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.lang.management.ManagementFactory;
10+
import java.lang.management.RuntimeMXBean;
11+
import java.nio.charset.Charset;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.ArrayList;
15+
import java.util.Arrays;
16+
import java.util.List;
17+
import java.util.stream.Collectors;
18+
19+
public class ProcessRunner {
20+
21+
private static final Logger LOGGER = LoggerFactory.getLogger(ProcessRunner.class);
22+
23+
/**
24+
* Spawn a process running the main method of a specified class
25+
*
26+
* @param clazz The class to execute
27+
* @param args Any arguments to pass to the process
28+
* @return the Process spawned
29+
* @throws IOException if there is an error starting the process
30+
*/
31+
public static Process runClass(Class<?> clazz, String... args) throws IOException {
32+
// Because Java17 must be run using various module flags, these must be propagated
33+
// to the child processes
34+
// https://stackoverflow.com/questions/1490869/how-to-get-vm-arguments-from-inside-of-java-application
35+
final RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
36+
37+
// filter out javaagent params, or this confuses the IntelliJ debugger
38+
final List<String> jvmArgsWithoutJavaAgents = runtimeMxBean.getInputArguments().stream()
39+
.filter(arg -> !arg.startsWith("-javaagent:"))
40+
.filter(arg -> !arg.startsWith("-agentlib:"))
41+
.collect(Collectors.toList());
42+
43+
String classPath = System.getProperty("java.class.path");
44+
String className = clazz.getName();
45+
String javaBin = findJavaBinPath().toString();
46+
List<String> allArgs = new ArrayList<>();
47+
allArgs.add(javaBin);
48+
allArgs.addAll(jvmArgsWithoutJavaAgents);
49+
allArgs.add("-cp");
50+
allArgs.add(classPath);
51+
allArgs.add(className);
52+
allArgs.addAll(Arrays.asList(args));
53+
ProcessBuilder processBuilder = new ProcessBuilder(allArgs.toArray(new String[]{}));
54+
// processBuilder.inheritIO(); // this doesn't place nice with surefire
55+
return processBuilder.start();
56+
}
57+
58+
/**
59+
* Log stdout and stderr for a process
60+
* <p>
61+
* ProcessBuilder.inheritIO() didn't play nicely with Maven failsafe plugin
62+
* <p>
63+
* https://maven.apache.org/surefire/maven-failsafe-plugin/faq.html#corruptedstream
64+
*/
65+
public static void printProcessOutput(String processName, Process process) {
66+
LOGGER.info("\n"
67+
+ "Output for " + processName + "\n"
68+
+ "stdout:\n"
69+
+ copyStreamToString(process.getInputStream()) + "\n"
70+
+ "stderr:\n"
71+
+ copyStreamToString(process.getErrorStream()));
72+
}
73+
74+
/**
75+
* Copies a stream to a string, up to the point where reading more would block
76+
*
77+
* @param inputStream The stream to read from
78+
* @return The output as a string
79+
*/
80+
private static String copyStreamToString(InputStream inputStream) {
81+
ByteArrayOutputStream os = new ByteArrayOutputStream();
82+
byte[] buffer = new byte[1024];
83+
int read;
84+
try {
85+
while (inputStream.available() > 0 && (read = inputStream.read(buffer)) >= 0) {
86+
os.write(buffer, 0, read);
87+
}
88+
} catch (IOException e) {
89+
// Ignore
90+
}
91+
return new String(os.toByteArray(), Charset.defaultCharset());
92+
}
93+
94+
/**
95+
* Try and work out what the java executable is cross platform
96+
*
97+
* @return the Path to the java executable
98+
* @throws IllegalStateException if the executable couldn't be located
99+
*/
100+
private static Path findJavaBinPath() {
101+
final Path javaBinPath = Paths.get(System.getProperty("java.home")).resolve("bin");
102+
final Path linuxJavaExecutable = javaBinPath.resolve("java");
103+
if (linuxJavaExecutable.toFile().exists()) {
104+
return linuxJavaExecutable;
105+
} else {
106+
Path windowsJavaExecutable = javaBinPath.resolve("java.exe");
107+
if (windowsJavaExecutable.toFile().exists()) {
108+
return windowsJavaExecutable;
109+
}
110+
}
111+
throw new IllegalStateException("Couldn't locate java executable!");
112+
}
113+
}

affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,21 @@ public String getMetaInfo(int id) throws IOException {
134134
}
135135

136136
LockReference lr = locks[id];
137-
if (lr == null) {
138-
return null;
137+
if (lr != null) {
138+
return readMetaInfoFromLockFileChannel(file, lr.channel);
139+
} else {
140+
try (FileChannel fc = FileChannel.open(file.toPath(), READ)) {
141+
return readMetaInfoFromLockFileChannel(file, fc);
142+
}
139143
}
140-
FileChannel fc = lr.channel;
144+
}
145+
146+
private String readMetaInfoFromLockFileChannel(File lockFile, FileChannel lockFileChannel) throws IOException {
141147
ByteBuffer buffer = ByteBuffer.allocate(64);
142-
int len = fc.read(buffer, 0);
148+
int len = lockFileChannel.read(buffer, 0);
143149
String content = len < 1 ? "" : new String(buffer.array(), 0, len);
144150
if (content.isEmpty()) {
145-
LOGGER.warn("Empty lock file {}", file.getAbsolutePath());
151+
LOGGER.warn("Empty lock file {}", lockFile.getAbsolutePath());
146152
return null;
147153
}
148154
return content.substring(0, content.indexOf("\n"));
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package net.openhft.affinity;
2+
3+
import net.openhft.affinity.common.ProcessRunner;
4+
import net.openhft.affinity.lockchecker.FileLockBasedLockChecker;
5+
import org.junit.Test;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import java.io.IOException;
10+
import java.util.concurrent.TimeUnit;
11+
12+
import static org.junit.Assert.assertNotEquals;
13+
import static org.junit.Assert.fail;
14+
15+
public class MultiProcessAffinityTest {
16+
17+
@Test
18+
public void shouldNotAcquireLockOnCoresLockedByOtherProcesses() throws IOException, InterruptedException {
19+
// run the separate affinity locker
20+
final Process last = ProcessRunner.runClass(AffinityLockerProcess.class, "last");
21+
try {
22+
int lastCpuId = AffinityLock.PROCESSORS - 1;
23+
24+
// wait for the CPU to be locked
25+
long endTime = System.currentTimeMillis() + 5_000;
26+
while (FileLockBasedLockChecker.getInstance().isLockFree(lastCpuId)) {
27+
Thread.sleep(100);
28+
if (System.currentTimeMillis() > endTime) {
29+
fail("Timed out waiting for the sub-process to acquire the lock");
30+
}
31+
}
32+
33+
try (AffinityLock lock = AffinityLock.acquireLock("last")) {
34+
assertNotEquals(lastCpuId, lock.cpuId());
35+
}
36+
} finally {
37+
last.destroy();
38+
if (!last.waitFor(5, TimeUnit.SECONDS)) {
39+
fail("Sub-process didn't terminate!");
40+
}
41+
}
42+
}
43+
44+
static class AffinityLockerProcess {
45+
46+
private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLockerProcess.class);
47+
48+
public static void main(String[] args) {
49+
String cpuIdToLock = args[0];
50+
51+
try (final AffinityLock affinityLock = AffinityLock.acquireLock(cpuIdToLock)) {
52+
LOGGER.info("Got affinity lock " + affinityLock);
53+
Thread.sleep(Integer.MAX_VALUE);
54+
} catch (InterruptedException e) {
55+
// expected, just end
56+
}
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)