Skip to content

Commit b251078

Browse files
committed
Kotlin: Implement lockless TRAP writing
Rather than using lock files and rewriting TRAP file, and storing the metadata in a .metadata file, we now encode the metadata in the filename and rename all but the newest TRAP file so that the importer doesn't see them. So we might end up with e.g. Text.members#0.0-1664381081060-java.trap.gz Text.members#55.0-1658481279000-java.trap-old.gz Text.members#55.0-1664381081060-java.trap-old.gz For now, you can go back to the old system by setting CODEQL_EXTRACTOR_JAVA_TRAP_LOCKING=true in the environment.
1 parent caaee26 commit b251078

File tree

1 file changed

+144
-28
lines changed

1 file changed

+144
-28
lines changed

java/kotlin-extractor/src/main/java/com/semmle/extractor/java/OdasaOutput.java

Lines changed: 144 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
import java.io.File;
55
import java.io.IOException;
66
import java.util.Arrays;
7+
import java.util.Collections;
8+
import java.util.Comparator;
79
import java.util.Enumeration;
810
import java.util.HashMap;
911
import java.util.LinkedHashMap;
12+
import java.util.LinkedList;
13+
import java.util.List;
1014
import java.util.Map;
1115
import java.util.Objects;
16+
import java.util.regex.Matcher;
1217
import java.util.regex.Pattern;
1318
import java.util.zip.ZipEntry;
1419
import java.util.zip.ZipFile;
@@ -29,6 +34,7 @@
2934

3035
import com.semmle.util.concurrent.LockDirectory;
3136
import com.semmle.util.concurrent.LockDirectory.LockingMode;
37+
import com.semmle.util.data.Pair;
3238
import com.semmle.util.exception.CatastrophicError;
3339
import com.semmle.util.exception.NestedError;
3440
import com.semmle.util.exception.ResourceError;
@@ -43,6 +49,9 @@
4349
import com.semmle.util.trap.pathtransformers.PathTransformer;
4450

4551
public class OdasaOutput {
52+
// By default we use lockless TRAP writing, but this can be set
53+
// if we want to use the old TRAP locking for any reason.
54+
private final boolean use_trap_locking = Env.systemEnv().getBoolean("CODEQL_EXTRACTOR_JAVA_TRAP_LOCKING", false);
4655

4756
// either these are set ...
4857
private final File trapFolder;
@@ -261,21 +270,39 @@ private void deleteTrapFileAndDependencies(IrDeclaration sym, String signature)
261270
* For functions for example, this means its parameter signature.
262271
*/
263272
private TrapFileManager getMembersWriterForDecl(File trap, IrDeclaration sym, String signature) {
264-
TrapClassVersion currVersion = TrapClassVersion.fromSymbol(sym, log);
265-
String shortName = sym instanceof IrDeclarationWithName ? ((IrDeclarationWithName)sym).getName().asString() : "(name unknown)";
266-
if (trap.exists()) {
267-
// Only re-write an existing trap file if we encountered a newer version of the same class.
268-
TrapClassVersion trapVersion = readVersionInfo(trap);
269-
if (!currVersion.isValid()) {
270-
log.warn("Not rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap);
271-
} else if (currVersion.newerThan(trapVersion)) {
272-
log.trace("Rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap);
273-
deleteTrapFileAndDependencies(sym, signature);
273+
if (use_trap_locking) {
274+
TrapClassVersion currVersion = TrapClassVersion.fromSymbol(sym, log);
275+
String shortName = sym instanceof IrDeclarationWithName ? ((IrDeclarationWithName)sym).getName().asString() : "(name unknown)";
276+
if (trap.exists()) {
277+
// Only re-write an existing trap file if we encountered a newer version of the same class.
278+
TrapClassVersion trapVersion = readVersionInfo(trap);
279+
if (!currVersion.isValid()) {
280+
log.warn("Not rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap);
281+
} else if (currVersion.newerThan(trapVersion)) {
282+
log.trace("Rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap);
283+
deleteTrapFileAndDependencies(sym, signature);
284+
} else {
285+
return null;
286+
}
274287
} else {
275-
return null;
288+
log.trace("Writing trap file for: " + shortName + " " + currVersion + " " + trap);
276289
}
277290
} else {
278-
log.trace("Writing trap file for: " + shortName + " " + currVersion + " " + trap);
291+
// If the TRAP file already exists then we
292+
// don't need to write it.
293+
if (trap.exists()) {
294+
log.warn("Not rewriting trap file for " + trap.toString() + " as it exists");
295+
return null;
296+
}
297+
// If the TRAP file was written in the past, and
298+
// then renamed to its trap-old name, then we
299+
// don't need to rewrite it only to rename it
300+
// again.
301+
File trapOld = new File(trap.getParentFile(), trap.getName().replace(".trap.gz", ".trap-old.gz"));
302+
if (trapOld.exists()) {
303+
log.warn("Not rewriting trap file for " + trap.toString() + " as the trap-old exists");
304+
return null;
305+
}
279306
}
280307
return trapWriter(trap, sym, signature);
281308
}
@@ -328,19 +355,24 @@ public void close() {
328355
}
329356

330357
writeTrapDependencies(trapDependenciesForClass);
331-
// Record major/minor version information for extracted class files.
332-
// This is subsequently used to determine whether to re-extract (a newer version of) the same class.
333-
File metadataFile = new File(trapFile.getAbsolutePath().replace(".trap.gz", ".metadata"));
334-
try {
335-
Map<String, String> versionMap = new LinkedHashMap<>();
336-
TrapClassVersion tcv = TrapClassVersion.fromSymbol(sym, log);
337-
versionMap.put(MAJOR_VERSION, String.valueOf(tcv.getMajorVersion()));
338-
versionMap.put(MINOR_VERSION, String.valueOf(tcv.getMinorVersion()));
339-
versionMap.put(LAST_MODIFIED, String.valueOf(tcv.getLastModified()));
340-
versionMap.put(EXTRACTOR_NAME, tcv.getExtractorName());
341-
FileUtil.writePropertiesCSV(metadataFile, versionMap);
342-
} catch (IOException e) {
343-
log.warn("Could not save trap metadata file: " + metadataFile.getAbsolutePath(), e);
358+
359+
// If we are using TRAP locking then we
360+
// need to write a metadata file.
361+
if (use_trap_locking) {
362+
// Record major/minor version information for extracted class files.
363+
// This is subsequently used to determine whether to re-extract (a newer version of) the same class.
364+
File metadataFile = new File(trapFile.getAbsolutePath().replace(".trap.gz", ".metadata"));
365+
try {
366+
Map<String, String> versionMap = new LinkedHashMap<>();
367+
TrapClassVersion tcv = TrapClassVersion.fromSymbol(sym, log);
368+
versionMap.put(MAJOR_VERSION, String.valueOf(tcv.getMajorVersion()));
369+
versionMap.put(MINOR_VERSION, String.valueOf(tcv.getMinorVersion()));
370+
versionMap.put(LAST_MODIFIED, String.valueOf(tcv.getLastModified()));
371+
versionMap.put(EXTRACTOR_NAME, tcv.getExtractorName());
372+
FileUtil.writePropertiesCSV(metadataFile, versionMap);
373+
} catch (IOException e) {
374+
log.warn("Could not save trap metadata file: " + metadataFile.getAbsolutePath(), e);
375+
}
344376
}
345377
}
346378
private void writeTrapDependencies(TrapDependencies trapDependencies) {
@@ -414,6 +446,10 @@ public TrapLocker getTrapLockerForDecl(IrDeclaration sym, String signature) {
414446
public class TrapLocker implements AutoCloseable {
415447
private final IrDeclaration sym;
416448
private final File trapFile;
449+
// trapFileBase is used when doing lockless TRAP file writing.
450+
// It is trapFile without the #metadata.trap.gz suffix.
451+
private File trapFileBase = null;
452+
private TrapClassVersion trapFileVersion = null;
417453
private final String signature;
418454
private TrapLocker(IrDeclaration decl, String signature) {
419455
this.sym = decl;
@@ -422,7 +458,16 @@ private TrapLocker(IrDeclaration decl, String signature) {
422458
log.error("Null symbol passed for Kotlin TRAP locker");
423459
trapFile = null;
424460
} else {
425-
trapFile = getTrapFileForDecl(sym, signature);
461+
File normalTrapFile = getTrapFileForDecl(sym, signature);
462+
if (use_trap_locking) {
463+
trapFile = normalTrapFile;
464+
} else {
465+
// We encode the metadata into the filename, so that the
466+
// TRAP filenames for different metadatas don't overlap.
467+
trapFileVersion = TrapClassVersion.fromSymbol(sym, log);
468+
trapFileBase = new File(normalTrapFile.getParentFile(), normalTrapFile.getName().replace(".trap.gz", ""));
469+
trapFile = new File(trapFileBase.getPath() + '#' + trapFileVersion.toString() + ".trap.gz");
470+
}
426471
}
427472
}
428473
private TrapLocker(File jarFile) {
@@ -437,20 +482,80 @@ private TrapLocker(String moduleName) {
437482
}
438483
public TrapFileManager getTrapFileManager() {
439484
if (trapFile!=null) {
440-
lockTrapFile(trapFile);
485+
if (use_trap_locking) {
486+
lockTrapFile(trapFile);
487+
}
441488
return getMembersWriterForDecl(trapFile, sym, signature);
442489
} else {
443490
return null;
444491
}
445492
}
493+
494+
private final Pattern selectClassVersionComponents = Pattern.compile("(.*)#(-?[0-9]+)\\.(-?[0-9]+)-(-?[0-9]+)-(.*)\\.trap\\.gz");
495+
446496
@Override
447497
public void close() {
448498
if (trapFile!=null) {
449499
try {
450-
unlockTrapFile(trapFile);
500+
if (use_trap_locking) {
501+
unlockTrapFile(trapFile);
502+
}
451503
} catch (NestedError e) {
452504
log.warn("Error unlocking trap file " + trapFile.getAbsolutePath(), e);
453505
}
506+
507+
// If we are writing TRAP file locklessly, then now that we
508+
// have finished writing our TRAP file, we want to rename
509+
// and TRAP file that matches our trapFileBase but doesn't
510+
// have the latest metadata.
511+
// Renaming it to trap-old means that it won't be imported,
512+
// but we can still use its presence to avoid future
513+
// invocations rewriting it, and it means that the information
514+
// is in the TRAP directory if we need it for debugging.
515+
if (!use_trap_locking && sym != null) {
516+
File trapFileDir = trapFileBase.getParentFile();
517+
String trapFileBaseName = trapFileBase.getName();
518+
519+
List<Pair<File, TrapClassVersion>> pairs = new LinkedList<Pair<File, TrapClassVersion>>();
520+
for (File f: FileUtil.list(trapFileDir)) {
521+
String name = f.getName();
522+
Matcher m = selectClassVersionComponents.matcher(name);
523+
if (m.matches() && m.group(1).equals(trapFileBaseName)) {
524+
TrapClassVersion v = new TrapClassVersion(Integer.valueOf(m.group(2)), Integer.valueOf(m.group(3)), Long.valueOf(m.group(4)), m.group(5));
525+
pairs.add(new Pair<File, TrapClassVersion>(f, v));
526+
}
527+
}
528+
if (pairs.isEmpty()) {
529+
log.error("Wrote TRAP file, but no TRAP files exist for " + trapFile.getAbsolutePath());
530+
} else {
531+
Comparator<Pair<File, TrapClassVersion>> comparator = new Comparator<Pair<File, TrapClassVersion>>() {
532+
@Override
533+
public int compare(Pair<File, TrapClassVersion> p1, Pair<File, TrapClassVersion> p2) {
534+
TrapClassVersion v1 = p1.snd();
535+
TrapClassVersion v2 = p2.snd();
536+
if (v1.equals(v2)) {
537+
return 0;
538+
} else if (v1.newerThan(v2)) {
539+
return 1;
540+
} else {
541+
return -1;
542+
}
543+
}
544+
};
545+
TrapClassVersion latestVersion = Collections.max(pairs, comparator).snd();
546+
547+
for (Pair<File, TrapClassVersion> p: pairs) {
548+
if (!latestVersion.equals(p.snd())) {
549+
File f = p.fst();
550+
File fOld = new File(f.getParentFile(), f.getName().replace(".trap.gz", ".trap-old.gz"));
551+
// We aren't interested in whether or not this succeeds;
552+
// it may fail because a concurrent extractor has already
553+
// renamed it.
554+
f.renameTo(fOld);
555+
}
556+
}
557+
}
558+
}
454559
}
455560
}
456561

@@ -505,6 +610,17 @@ private TrapClassVersion(int majorVersion, int minorVersion, long lastModified,
505610
this.lastModified = lastModified;
506611
this.extractorName = extractorName;
507612
}
613+
614+
@Override
615+
public boolean equals(Object obj) {
616+
if (obj instanceof TrapClassVersion) {
617+
TrapClassVersion other = (TrapClassVersion)obj;
618+
return majorVersion == other.majorVersion && minorVersion == other.minorVersion && lastModified == other.lastModified && extractorName.equals(other.extractorName);
619+
} else {
620+
return false;
621+
}
622+
}
623+
508624
private boolean newerThan(TrapClassVersion tcv) {
509625
// Classes being compiled from source have major version 0 but should take precedence
510626
// over any classes with the same qualified name loaded from the classpath

0 commit comments

Comments
 (0)