Skip to content

Commit 345865a

Browse files
committed
Make lock acquisition atomic, Fixes #81
1 parent e8ff714 commit 345865a

File tree

8 files changed

+146
-68
lines changed

8 files changed

+146
-68
lines changed

affinity/src/main/java/net/openhft/affinity/LockCheck.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,10 @@ static int getProcessForCpu(int core) throws IOException {
109109
return EMPTY_PID;
110110
}
111111

112-
static void updateCpu(int cpu) {
112+
static void updateCpu(int cpu) throws IOException {
113113
if (!canOSSupportOperation())
114114
return;
115-
try {
116-
replacePid(cpu, getPID());
117-
} catch (IOException e) {
118-
LOGGER.warn("Failed to update lock file for cpu " + cpu, e);
119-
e.printStackTrace();
120-
}
115+
replacePid(cpu, getPID());
121116
}
122117

123118
public static void releaseLock(int cpu) {

affinity/src/main/java/net/openhft/affinity/LockInventory.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.slf4j.Logger;
2323
import org.slf4j.LoggerFactory;
2424

25+
import java.io.IOException;
2526
import java.util.NavigableMap;
2627
import java.util.TreeMap;
2728

@@ -69,9 +70,22 @@ private static boolean isAnyCpu(final int cpuId) {
6970
return cpuId == AffinityLock.ANY_CPU;
7071
}
7172

72-
private static void updateLockForCurrentThread(final boolean bind, final AffinityLock al, final boolean b) {
73-
al.assignCurrentThread(bind, b);
74-
LockCheck.updateCpu(al.cpuId());
73+
/**
74+
* Update the lock for the current thread
75+
*
76+
* @param bind Whether to also bind the thread to the core
77+
* @param al The lock to update
78+
* @param wholeCore Whether to bind the whole core
79+
* @return true if the lock was acquired, false otherwise
80+
*/
81+
private static boolean updateLockForCurrentThread(final boolean bind, final AffinityLock al, final boolean wholeCore) {
82+
try {
83+
LockCheck.updateCpu(al.cpuId());
84+
al.assignCurrentThread(bind, wholeCore);
85+
return true;
86+
} catch (IOException e) {
87+
return false;
88+
}
7589
}
7690

7791
public final synchronized CpuLayout getCpuLayout() {
@@ -106,8 +120,9 @@ public final synchronized AffinityLock acquireLock(boolean bind, int cpuId, Affi
106120
final boolean specificCpuRequested = !isAnyCpu(cpuId);
107121
if (specificCpuRequested && cpuId != 0) {
108122
final AffinityLock required = logicalCoreLocks[cpuId];
109-
if (required.canReserve(true) && anyStrategyMatches(cpuId, cpuId, strategies)) {
110-
updateLockForCurrentThread(bind, required, false);
123+
if (required.canReserve(true)
124+
&& anyStrategyMatches(cpuId, cpuId, strategies)
125+
&& updateLockForCurrentThread(bind, required, false)) {
111126
return required;
112127
}
113128
LOGGER.warn("Unable to acquire lock on CPU {} for thread {}, trying to find another CPU",
@@ -119,8 +134,9 @@ public final synchronized AffinityLock acquireLock(boolean bind, int cpuId, Affi
119134
// if you have only one core, this library is not appropriate in any case.
120135
for (int i = logicalCoreLocks.length - 1; i > 0; i--) {
121136
AffinityLock al = logicalCoreLocks[i];
122-
if (al.canReserve(false) && (isAnyCpu(cpuId) || strategy.matches(cpuId, al.cpuId()))) {
123-
updateLockForCurrentThread(bind, al, false);
137+
if (al.canReserve(false)
138+
&& (isAnyCpu(cpuId) || strategy.matches(cpuId, al.cpuId()))
139+
&& updateLockForCurrentThread(bind, al, false)) {
124140
return al;
125141
}
126142
}
@@ -136,8 +152,8 @@ public final synchronized AffinityLock tryAcquireLock(boolean bind, int cpuId) {
136152
return null;
137153

138154
final AffinityLock required = logicalCoreLocks[cpuId];
139-
if (required.canReserve(true)) {
140-
updateLockForCurrentThread(bind, required, false);
155+
if (required.canReserve(true)
156+
&& updateLockForCurrentThread(bind, required, false)) {
141157
return required;
142158
}
143159

@@ -156,8 +172,9 @@ public final synchronized AffinityLock acquireCore(boolean bind, int cpuId, Affi
156172
continue LOOP;
157173

158174
final AffinityLock al = als[0];
159-
updateLockForCurrentThread(bind, al, true);
160-
return al;
175+
if (updateLockForCurrentThread(bind, al, true)) {
176+
return al;
177+
}
161178
}
162179
}
163180

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public String getMetaInfo(int id) throws IOException {
7878

7979
@NotNull
8080
protected File toFile(int id) {
81+
assert id >= 0;
8182
return new File(tmpDir(), "cpu-" + id + ".lock");
8283
}
8384
}

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.nio.channels.FileLock;
1212
import java.nio.channels.OverlappingFileLockException;
1313
import java.nio.file.Files;
14+
import java.nio.file.NoSuchFileException;
1415
import java.nio.file.StandardOpenOption;
1516
import java.nio.file.attribute.FileAttribute;
1617
import java.nio.file.attribute.PosixFilePermission;
@@ -29,7 +30,7 @@ public class FileLockBasedLockChecker extends FileBasedLockChecker {
2930
private static final String OS = System.getProperty("os.name").toLowerCase();
3031

3132
private static final LockChecker instance = new FileLockBasedLockChecker();
32-
private static final HashSet<StandardOpenOption> openOptions = new HashSet<>(Arrays.asList(CREATE_NEW, WRITE, READ, SYNC));
33+
private static final HashSet<StandardOpenOption> openOptions = new HashSet<>(Arrays.asList(CREATE, WRITE, READ, SYNC));
3334
private static final FileAttribute<Set<PosixFilePermission>> fileAttr = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-rw-rw-"));
3435
private final LockReference[] locks = new LockReference[MAX_CPUS_SUPPORTED];
3536

@@ -47,11 +48,6 @@ public boolean isLockFree(int id) {
4748
}
4849

4950
private boolean isLockFree(File file, int id) {
50-
//if no file exists - nobody has the lock for sure
51-
if (!file.exists()) {
52-
return true;
53-
}
54-
5551
//do we have the lock already?
5652
LockReference existingLock = locks[id];
5753
if (existingLock != null) {
@@ -60,23 +56,27 @@ private boolean isLockFree(File file, int id) {
6056

6157
//does another process have the lock?
6258
try {
63-
FileChannel fc = FileChannel.open(file.toPath(), WRITE);
64-
FileLock fileLock = fc.tryLock();
65-
if (fileLock == null) {
66-
return false;
59+
// only take a shared lock to test if the file is locked,
60+
// this means processes testing the lock concurrently
61+
// won't interfere with each other
62+
try (FileChannel fc = FileChannel.open(file.toPath(), READ);
63+
FileLock fileLock = fc.tryLock(0, Long.MAX_VALUE, true)) {
64+
if (fileLock == null) {
65+
return false;
66+
}
6767
}
68-
} catch (IOException | OverlappingFileLockException e) {
68+
// file is present but no process is holding the lock
69+
return true;
70+
} catch (OverlappingFileLockException e) {
71+
// another process has the lock
72+
return false;
73+
} catch (NoSuchFileException e) {
74+
// the file doesn't exist, nobody has the lock
75+
return true;
76+
} catch (IOException e) {
6977
LOGGER.error(String.format("Exception occurred whilst trying to check lock on file %s : %s%n", file.getAbsolutePath(), e));
78+
return true; // maybe we should re-throw?
7079
}
71-
72-
//file is present but nobody has it locked - delete it
73-
boolean deleted = file.delete();
74-
if (deleted)
75-
LOGGER.info(String.format("Deleted %s as nobody has the lock", file.getAbsolutePath()));
76-
else
77-
LOGGER.warn(String.format("Nobody has the lock on %s. Delete failed", file.getAbsolutePath()));
78-
79-
return true;
8080
}
8181

8282
@Override
@@ -117,7 +117,6 @@ public boolean releaseLock(int id) {
117117
locks[id] = null;
118118
lock.lock.release();
119119
lock.channel.close();
120-
toFile(id).delete();
121120
return true;
122121
} catch (IOException e) {
123122
LOGGER.error(String.format("Couldn't release lock for id %d due to exception: %s%n", id, e.getMessage()));
@@ -157,13 +156,14 @@ private String readMetaInfoFromLockFileChannel(File lockFile, FileChannel lockFi
157156
@NotNull
158157
@Override
159158
protected File toFile(int id) {
159+
assert id >= 0;
160160
File file = super.toFile(id);
161161
try {
162162
if (file.exists() && OS.startsWith("linux")) {
163163
Files.setPosixFilePermissions(file.toPath(), PosixFilePermissions.fromString("rwxrwxrwx"));
164164
}
165165
} catch (IOException e) {
166-
LOGGER.warn("Unable to set file permissions \"rwxrwxrwx\" for {} due to {}", file.toString(), e);
166+
LOGGER.warn("Unable to set file permissions \"rwxrwxrwx\" for {} due to {}", file, e);
167167
}
168168
return file;
169169
}

affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -247,20 +247,6 @@ public void shouldReturnLockForSpecifiedCpu() {
247247
assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity());
248248
}
249249

250-
@Test
251-
public void lockFilesShouldBeRemovedOnRelease() {
252-
if (!Utilities.ISLINUX) {
253-
return;
254-
}
255-
final AffinityLock lock = AffinityLock.acquireLock();
256-
257-
assertThat(Files.exists(Paths.get(lockChecker.doToFile(lock.cpuId()).getAbsolutePath())), is(true));
258-
259-
lock.release();
260-
261-
assertThat(Files.exists(Paths.get(lockChecker.doToFile(lock.cpuId()).getAbsolutePath())), is(false));
262-
}
263-
264250
private void displayStatus() {
265251
System.out.println(Thread.currentThread() + " on " + Affinity.getCpu() + "\n" + AffinityLock.dumpLocks());
266252
}

affinity/src/test/java/net/openhft/affinity/FileLockLockCheckTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public void before() {
5353

5454
@After
5555
public void after() {
56+
LockCheck.releaseLock(cpu);
5657
System.setProperty("java.io.tmpdir", TMP);
5758
}
5859

affinity/src/test/java/net/openhft/affinity/LockCheckTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public void before() {
5555
@After
5656
public void after() {
5757
System.setProperty("java.io.tmpdir", TMP);
58+
LockCheck.releaseLock(cpu);
5859
}
5960

6061
@Test

affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
import java.time.LocalDateTime;
1212
import java.util.ArrayList;
1313
import java.util.List;
14-
import java.util.concurrent.ThreadLocalRandom;
15-
import java.util.concurrent.TimeUnit;
14+
import java.util.concurrent.*;
15+
import java.util.stream.Collectors;
16+
import java.util.stream.IntStream;
1617

1718
import static java.lang.String.format;
1819
import static net.openhft.affinity.LockCheck.IS_LINUX;
@@ -70,42 +71,104 @@ public void shouldNotAcquireLockOnCoresLockedByOtherProcesses() throws IOExcepti
7071
}
7172
}
7273

73-
@Ignore("https://github.com/OpenHFT/Java-Thread-Affinity/issues/81")
7474
@Test
7575
public void shouldAllocateCoresCorrectlyUnderContention() throws IOException, InterruptedException {
76-
final int numberOfLockers = Math.max(8, Runtime.getRuntime().availableProcessors());
76+
Assume.assumeTrue(IS_LINUX);
77+
final int numberOfLockers = Math.max(2, Math.min(8, Runtime.getRuntime().availableProcessors())) / 2;
7778
List<Process> lockers = new ArrayList<>();
79+
LOGGER.info("Running test with {} locker processes", numberOfLockers);
7880
for (int i = 0; i < numberOfLockers; i++) {
79-
lockers.add(ProcessRunner.runClass(RepeatedAffinityLocker.class, "last", "100"));
81+
lockers.add(ProcessRunner.runClass(RepeatedAffinityLocker.class,
82+
new String[]{"-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()},
83+
new String[]{"last", "30", "2"}));
8084
}
8185
for (int i = 0; i < numberOfLockers; i++) {
82-
if (!lockers.get(i).waitFor(10, TimeUnit.SECONDS)) {
83-
throw new IllegalStateException("Locker process didn't end in time");
86+
final Process process = lockers.get(i);
87+
if (!process.waitFor(20, TimeUnit.SECONDS)) {
88+
ProcessRunner.printProcessOutput("Stalled locking process", process);
89+
fail("Locker process didn't end in time");
90+
}
91+
if (process.exitValue() != 0) {
92+
ProcessRunner.printProcessOutput("Failed locking process", process);
93+
fail("At least one of the locking processes failed, see output above");
8494
}
85-
assertEquals(0, lockers.get(i).exitValue());
95+
assertEquals(0, process.exitValue());
96+
}
97+
}
98+
99+
@Test
100+
public void shouldBeAbleToAcquireLockLeftByOtherProcess() throws IOException, InterruptedException {
101+
Assume.assumeTrue(IS_LINUX);
102+
final Process process = ProcessRunner.runClass(AffinityLockerThatDoesNotReleaseProcess.class,
103+
new String[]{"-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()},
104+
new String[]{"last"});
105+
if (!process.waitFor(5, TimeUnit.SECONDS)) {
106+
ProcessRunner.printProcessOutput("locker process", process);
107+
fail("Locker process timed out");
108+
}
109+
if (process.exitValue() != 0) {
110+
ProcessRunner.printProcessOutput("locker process", process);
111+
fail("Locker process failed");
112+
}
113+
// We should be able to acquire the lock despite the other process not explicitly releasing it
114+
try (final AffinityLock acquired = AffinityLock.acquireLock("last")) {
115+
assertEquals(AffinityLock.PROCESSORS - 1, acquired.cpuId());
86116
}
87117
}
88118

89119
/**
90120
* Repeatedly acquires and releases a lock on the specified core
91121
*/
92-
static class RepeatedAffinityLocker {
122+
static class RepeatedAffinityLocker implements Callable<Void> {
93123

124+
private static final Logger LOGGER = LoggerFactory.getLogger(RepeatedAffinityLocker.class);
94125
private static final long PID = LockCheck.getPID();
126+
private final int iterations;
127+
private final String cpuIdToLock;
95128

96-
public static void main(String[] args) throws IOException, InterruptedException {
129+
public static void main(String[] args) throws InterruptedException, ExecutionException {
97130
String cpuIdToLock = args[0];
98131
int iterations = Integer.parseInt(args[1]);
132+
int threads = Integer.parseInt(args[2]);
133+
134+
LOGGER.info("Acquiring lock with {} threads, {} iterations", threads, iterations);
135+
ExecutorService executorService = Executors.newFixedThreadPool(threads);
136+
final List<Future<Void>> futures = executorService.invokeAll(IntStream.range(0, threads)
137+
.mapToObj(tid -> new RepeatedAffinityLocker(cpuIdToLock, iterations))
138+
.collect(Collectors.toList()));
139+
for (Future<Void> future : futures) {
140+
future.get();
141+
}
142+
executorService.shutdown();
143+
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
144+
throw new IllegalStateException("Executor service didn't shut down");
145+
}
146+
}
147+
148+
public RepeatedAffinityLocker(String cpuIdToLock, int iterations) {
149+
this.iterations = iterations;
150+
this.cpuIdToLock = cpuIdToLock;
151+
}
99152

153+
@Override
154+
public Void call() throws Exception {
100155
for (int i = 0; i < iterations; i++) {
156+
LOGGER.info("******* Starting iteration {} at {}", i, LocalDateTime.now());
101157
try (final AffinityLock affinityLock = AffinityLock.acquireLock(cpuIdToLock)) {
102-
long lockPID = Long.parseLong(FileLockBasedLockChecker.getInstance().getMetaInfo(affinityLock.cpuId()));
103-
if (lockPID != PID) {
104-
throw new IllegalStateException(format("PID in lock file is not mine (lockPID=%d, myPID=%d)", lockPID, PID));
158+
if (affinityLock.isAllocated()) {
159+
final String metaInfo = FileLockBasedLockChecker.getInstance().getMetaInfo(affinityLock.cpuId());
160+
LOGGER.info("Meta info is: " + metaInfo);
161+
long lockPID = Long.parseLong(metaInfo);
162+
if (lockPID != PID) {
163+
throw new IllegalStateException(format("PID in lock file is not mine (lockPID=%d, myPID=%d)", lockPID, PID));
164+
}
165+
Thread.sleep(ThreadLocalRandom.current().nextInt(50));
166+
} else {
167+
LOGGER.info("Couldn't get a lock");
105168
}
106-
Thread.sleep(ThreadLocalRandom.current().nextInt(50));
107169
}
108170
}
171+
return null;
109172
}
110173
}
111174

@@ -129,4 +192,18 @@ public static void main(String[] args) {
129192
}
130193
}
131194
}
195+
196+
/**
197+
* Acquires a lock then ends
198+
*/
199+
static class AffinityLockerThatDoesNotReleaseProcess {
200+
private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLockerProcess.class);
201+
202+
public static void main(String[] args) {
203+
String cpuIdToLock = args[0];
204+
205+
final AffinityLock affinityLock = AffinityLock.acquireLock(cpuIdToLock);
206+
LOGGER.info("Got affinity lock " + affinityLock + " at " + LocalDateTime.now() + ", CPU=" + affinityLock.cpuId());
207+
}
208+
}
132209
}

0 commit comments

Comments
 (0)