Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9341698
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Subscribers
None
View Options
diff --git a/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java b/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java
index b7399b0..3719ec5 100644
--- a/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java
+++ b/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java
@@ -1,260 +1,274 @@
package org.softwareheritage.graph.rpc;
import com.google.protobuf.FieldMask;
import com.martiansoftware.jsap.*;
import io.grpc.Server;
import io.grpc.Status;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.netty.shaded.io.netty.channel.ChannelOption;
import io.grpc.stub.StreamObserver;
import io.grpc.protobuf.services.ProtoReflectionService;
import it.unimi.dsi.logging.ProgressLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.softwareheritage.graph.SWHID;
import org.softwareheritage.graph.SwhBidirectionalGraph;
import org.softwareheritage.graph.compress.LabelMapBuilder;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
/**
* Server that manages startup/shutdown of a {@code Greeter} server.
*/
public class GraphServer {
private final static Logger logger = LoggerFactory.getLogger(GraphServer.class);
private final SwhBidirectionalGraph graph;
private final int port;
private final int threads;
private Server server;
public GraphServer(String graphBasename, int port, int threads) throws IOException {
this.graph = loadGraph(graphBasename);
this.port = port;
this.threads = threads;
}
public static SwhBidirectionalGraph loadGraph(String basename) throws IOException {
// TODO: use loadLabelledMapped() when https://github.com/vigna/webgraph-big/pull/5 is merged
SwhBidirectionalGraph g = SwhBidirectionalGraph.loadLabelled(basename, new ProgressLogger(logger));
g.loadContentLength();
g.loadContentIsSkipped();
g.loadPersonIds();
g.loadAuthorTimestamps();
g.loadCommitterTimestamps();
g.loadMessages();
g.loadTagNames();
g.loadLabelNames();
return g;
}
private void start() throws IOException {
server = NettyServerBuilder.forPort(port).withChildOption(ChannelOption.SO_REUSEADDR, true)
.executor(Executors.newFixedThreadPool(threads)).addService(new TraversalService(graph))
.addService(ProtoReflectionService.newInstance()).build().start();
logger.info("Server started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
GraphServer.this.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}));
}
private void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
/**
* Await termination on the main thread since the grpc library uses daemon threads.
*/
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
private static JSAPResult parseArgs(String[] args) {
JSAPResult config = null;
try {
SimpleJSAP jsap = new SimpleJSAP(LabelMapBuilder.class.getName(), "",
new Parameter[]{
new FlaggedOption("port", JSAP.INTEGER_PARSER, "50091", JSAP.NOT_REQUIRED, 'p', "port",
"The port on which the server should listen."),
new FlaggedOption("threads", JSAP.INTEGER_PARSER, "1", JSAP.NOT_REQUIRED, 't', "threads",
"The number of concurrent threads. 0 = number of cores."),
new UnflaggedOption("graphBasename", JSAP.STRING_PARSER, JSAP.REQUIRED,
"Basename of the output graph")});
config = jsap.parse(args);
if (jsap.messagePrinted()) {
System.exit(1);
}
} catch (JSAPException e) {
e.printStackTrace();
}
return config;
}
/**
* Main launches the server from the command line.
*/
public static void main(String[] args) throws IOException, InterruptedException {
JSAPResult config = parseArgs(args);
String graphBasename = config.getString("graphBasename");
int port = config.getInt("port");
int threads = config.getInt("threads");
if (threads == 0) {
threads = Runtime.getRuntime().availableProcessors();
}
final GraphServer server = new GraphServer(graphBasename, port, threads);
server.start();
server.blockUntilShutdown();
}
static class TraversalService extends TraversalServiceGrpc.TraversalServiceImplBase {
SwhBidirectionalGraph graph;
public TraversalService(SwhBidirectionalGraph graph) {
this.graph = graph;
}
@Override
public void stats(StatsRequest request, StreamObserver<StatsResponse> responseObserver) {
StatsResponse.Builder response = StatsResponse.newBuilder();
response.setNumNodes(graph.numNodes());
response.setNumEdges(graph.numArcs());
Properties properties = new Properties();
try {
properties.load(new FileInputStream(graph.getPath() + ".properties"));
properties.load(new FileInputStream(graph.getPath() + ".stats"));
} catch (IOException e) {
throw new RuntimeException(e);
}
response.setCompression(Double.parseDouble(properties.getProperty("compratio")));
response.setBitsPerNode(Double.parseDouble(properties.getProperty("bitspernode")));
response.setBitsPerEdge(Double.parseDouble(properties.getProperty("bitsperlink")));
response.setAvgLocality(Double.parseDouble(properties.getProperty("avglocality")));
response.setIndegreeMin(Long.parseLong(properties.getProperty("minindegree")));
response.setIndegreeMax(Long.parseLong(properties.getProperty("maxindegree")));
response.setIndegreeAvg(Double.parseDouble(properties.getProperty("avgindegree")));
response.setOutdegreeMin(Long.parseLong(properties.getProperty("minoutdegree")));
response.setOutdegreeMax(Long.parseLong(properties.getProperty("maxoutdegree")));
response.setOutdegreeAvg(Double.parseDouble(properties.getProperty("avgoutdegree")));
responseObserver.onNext(response.build());
responseObserver.onCompleted();
}
@Override
public void getNode(GetNodeRequest request, StreamObserver<Node> responseObserver) {
long nodeId;
try {
nodeId = graph.getNodeId(new SWHID(request.getSwhid()));
} catch (IllegalArgumentException e) {
responseObserver
.onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException());
return;
}
Node.Builder builder = Node.newBuilder();
NodePropertyBuilder.buildNodeProperties(graph.getForwardGraph(),
request.hasMask() ? request.getMask() : null, builder, nodeId);
responseObserver.onNext(builder.build());
responseObserver.onCompleted();
}
@Override
public void traverse(TraversalRequest request, StreamObserver<Node> responseObserver) {
SwhBidirectionalGraph g = graph.copy();
Traversal.SimpleTraversal t;
try {
t = new Traversal.SimpleTraversal(g, request, responseObserver::onNext);
} catch (IllegalArgumentException e) {
responseObserver
.onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException());
return;
}
t.visit();
responseObserver.onCompleted();
}
@Override
public void findPathTo(FindPathToRequest request, StreamObserver<Path> responseObserver) {
SwhBidirectionalGraph g = graph.copy();
Traversal.FindPathTo t;
try {
t = new Traversal.FindPathTo(g, request);
} catch (IllegalArgumentException e) {
responseObserver
.onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException());
return;
}
t.visit();
Path path = t.getPath();
if (path == null) {
responseObserver.onError(Status.NOT_FOUND.asException());
} else {
responseObserver.onNext(path);
responseObserver.onCompleted();
}
}
@Override
public void findPathBetween(FindPathBetweenRequest request, StreamObserver<Path> responseObserver) {
SwhBidirectionalGraph g = graph.copy();
Traversal.FindPathBetween t;
try {
t = new Traversal.FindPathBetween(g, request);
} catch (IllegalArgumentException e) {
responseObserver
.onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException());
return;
}
t.visit();
Path path = t.getPath();
if (path == null) {
responseObserver.onError(Status.NOT_FOUND.asException());
} else {
responseObserver.onNext(path);
responseObserver.onCompleted();
}
}
@Override
public void countNodes(TraversalRequest request, StreamObserver<CountResponse> responseObserver) {
- AtomicInteger count = new AtomicInteger(0);
+ AtomicLong count = new AtomicLong(0);
SwhBidirectionalGraph g = graph.copy();
TraversalRequest fixedReq = TraversalRequest.newBuilder(request)
// Ignore return fields, just count nodes
.setMask(FieldMask.getDefaultInstance()).build();
- var t = new Traversal.SimpleTraversal(g, request, n -> count.incrementAndGet());
+ Traversal.SimpleTraversal t;
+ try {
+ t = new Traversal.SimpleTraversal(g, fixedReq, n -> count.incrementAndGet());
+ } catch (IllegalArgumentException e) {
+ responseObserver
+ .onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException());
+ return;
+ }
t.visit();
CountResponse response = CountResponse.newBuilder().setCount(count.get()).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public void countEdges(TraversalRequest request, StreamObserver<CountResponse> responseObserver) {
- AtomicInteger count = new AtomicInteger(0);
+ AtomicLong count = new AtomicLong(0);
SwhBidirectionalGraph g = graph.copy();
TraversalRequest fixedReq = TraversalRequest.newBuilder(request)
// Force return empty successors to count the edges
- .setMask(FieldMask.newBuilder().addPaths("successor").build()).build();
- var t = new Traversal.SimpleTraversal(g, request, n -> count.addAndGet(n.getSuccessorCount()));
+ .setMask(FieldMask.newBuilder().addPaths("num_successors").build()).build();
+ Traversal.SimpleTraversal t;
+ try {
+ t = new Traversal.SimpleTraversal(g, fixedReq, n -> count.addAndGet(n.getNumSuccessors()));
+ } catch (IllegalArgumentException e) {
+ responseObserver
+ .onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException());
+ return;
+ }
t.visit();
CountResponse response = CountResponse.newBuilder().setCount(count.get()).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
}
diff --git a/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java b/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java
index 8a13e2b..c22563b 100644
--- a/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java
+++ b/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java
@@ -1,184 +1,193 @@
/*
* Copyright (c) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
package org.softwareheritage.graph.rpc;
import com.google.protobuf.ByteString;
import com.google.protobuf.FieldMask;
import com.google.protobuf.util.FieldMaskUtil;
import it.unimi.dsi.big.webgraph.labelling.Label;
import org.softwareheritage.graph.SwhUnidirectionalGraph;
import org.softwareheritage.graph.labels.DirEntry;
import java.util.*;
public class NodePropertyBuilder {
public static class NodeDataMask {
public boolean swhid;
public boolean successor;
public boolean successorSwhid;
public boolean successorLabel;
+ public boolean numSuccessors;
public boolean cntLength;
public boolean cntIsSkipped;
public boolean revAuthor;
public boolean revAuthorDate;
public boolean revAuthorDateOffset;
public boolean revCommitter;
public boolean revCommitterDate;
public boolean revCommitterDateOffset;
public boolean revMessage;
public boolean relAuthor;
public boolean relAuthorDate;
public boolean relAuthorDateOffset;
public boolean relName;
public boolean relMessage;
public boolean oriUrl;
public NodeDataMask(FieldMask mask) {
Set<String> allowedFields = null;
if (mask != null) {
mask = FieldMaskUtil.normalize(mask);
allowedFields = new HashSet<>(mask.getPathsList());
}
this.swhid = allowedFields == null || allowedFields.contains("swhid");
this.successorSwhid = allowedFields == null || allowedFields.contains("successor")
|| allowedFields.contains("successor.swhid");
this.successorLabel = allowedFields == null || allowedFields.contains("successor")
|| allowedFields.contains("successor.label");
this.successor = this.successorSwhid || this.successorLabel;
+ this.numSuccessors = allowedFields == null || allowedFields.contains("num_successors");
this.cntLength = allowedFields == null || allowedFields.contains("cnt.length");
this.cntIsSkipped = allowedFields == null || allowedFields.contains("cnt.is_skipped");
this.revAuthor = allowedFields == null || allowedFields.contains("rev.author");
this.revAuthorDate = allowedFields == null || allowedFields.contains("rev.author_date");
this.revAuthorDateOffset = allowedFields == null || allowedFields.contains("rev.author_date_offset");
this.revCommitter = allowedFields == null || allowedFields.contains("rev.committer");
this.revCommitterDate = allowedFields == null || allowedFields.contains("rev.committer_date");
this.revCommitterDateOffset = allowedFields == null || allowedFields.contains("rev.committer_date_offset");
this.revMessage = allowedFields == null || allowedFields.contains("rev.message");
this.relAuthor = allowedFields == null || allowedFields.contains("rel.author");
this.relAuthorDate = allowedFields == null || allowedFields.contains("rel.author_date");
this.relAuthorDateOffset = allowedFields == null || allowedFields.contains("rel.author_date_offset");
this.relName = allowedFields == null || allowedFields.contains("rel.name");
this.relMessage = allowedFields == null || allowedFields.contains("rel.message");
this.oriUrl = allowedFields == null || allowedFields.contains("ori.url");
}
}
public static void buildNodeProperties(SwhUnidirectionalGraph graph, NodeDataMask mask, Node.Builder nodeBuilder,
long node) {
if (mask.swhid) {
nodeBuilder.setSwhid(graph.getSWHID(node).toString());
}
switch (graph.getNodeType(node)) {
case CNT:
ContentData.Builder cntBuilder = ContentData.newBuilder();
if (mask.cntLength) {
cntBuilder.setLength(graph.getContentLength(node));
}
if (mask.cntIsSkipped) {
cntBuilder.setIsSkipped(graph.isContentSkipped(node));
}
nodeBuilder.setCnt(cntBuilder.build());
break;
case REV:
RevisionData.Builder revBuilder = RevisionData.newBuilder();
if (mask.revAuthor) {
revBuilder.setAuthor(graph.getAuthorId(node));
}
if (mask.revAuthorDate) {
revBuilder.setAuthorDate(graph.getAuthorTimestamp(node));
}
if (mask.revAuthorDateOffset) {
revBuilder.setAuthorDateOffset(graph.getAuthorTimestampOffset(node));
}
if (mask.revCommitter) {
revBuilder.setCommitter(graph.getCommitterId(node));
}
if (mask.revCommitterDate) {
revBuilder.setCommitterDate(graph.getCommitterTimestamp(node));
}
if (mask.revCommitterDateOffset) {
revBuilder.setCommitterDateOffset(graph.getCommitterTimestampOffset(node));
}
if (mask.revMessage) {
byte[] msg = graph.getMessage(node);
if (msg != null) {
revBuilder.setMessage(ByteString.copyFrom(msg));
}
}
nodeBuilder.setRev(revBuilder.build());
break;
case REL:
ReleaseData.Builder relBuilder = ReleaseData.newBuilder();
if (mask.relAuthor) {
relBuilder.setAuthor(graph.getAuthorId(node));
}
if (mask.relAuthorDate) {
relBuilder.setAuthorDate(graph.getAuthorTimestamp(node));
}
if (mask.relAuthorDateOffset) {
relBuilder.setAuthorDateOffset(graph.getAuthorTimestampOffset(node));
}
if (mask.relName) {
byte[] msg = graph.getMessage(node);
if (msg != null) {
relBuilder.setMessage(ByteString.copyFrom(msg));
}
}
if (mask.relMessage) {
byte[] msg = graph.getMessage(node);
if (msg != null) {
relBuilder.setMessage(ByteString.copyFrom(msg));
}
}
nodeBuilder.setRel(relBuilder.build());
break;
case ORI:
OriginData.Builder oriBuilder = OriginData.newBuilder();
if (mask.oriUrl) {
String url = graph.getUrl(node);
if (url != null) {
oriBuilder.setUrl(url);
}
}
nodeBuilder.setOri(oriBuilder.build());
}
}
public static void buildNodeProperties(SwhUnidirectionalGraph graph, FieldMask mask, Node.Builder nodeBuilder,
long node) {
NodeDataMask nodeMask = new NodeDataMask(mask);
buildNodeProperties(graph, nodeMask, nodeBuilder, node);
}
public static void buildSuccessorProperties(SwhUnidirectionalGraph graph, NodeDataMask mask,
Node.Builder nodeBuilder, long src, long dst, Label label) {
- if (nodeBuilder != null && mask.successor) {
+ if (nodeBuilder != null) {
Successor.Builder successorBuilder = Successor.newBuilder();
if (mask.successorSwhid) {
successorBuilder.setSwhid(graph.getSWHID(dst).toString());
}
if (mask.successorLabel) {
DirEntry[] entries = (DirEntry[]) label.get();
for (DirEntry entry : entries) {
EdgeLabel.Builder builder = EdgeLabel.newBuilder();
builder.setName(ByteString.copyFrom(graph.getLabelName(entry.filenameId)));
builder.setPermission(entry.permission);
successorBuilder.addLabel(builder.build());
}
}
- nodeBuilder.addSuccessor(successorBuilder.build());
+ Successor successor = successorBuilder.build();
+ if (successor != Successor.getDefaultInstance()) {
+ nodeBuilder.addSuccessor(successor);
+ }
+
+ if (mask.numSuccessors) {
+ nodeBuilder.setNumSuccessors(nodeBuilder.getNumSuccessors() + 1);
+ }
}
}
public static void buildSuccessorProperties(SwhUnidirectionalGraph graph, FieldMask mask, Node.Builder nodeBuilder,
long src, long dst, Label label) {
NodeDataMask nodeMask = new NodeDataMask(mask);
buildSuccessorProperties(graph, nodeMask, nodeBuilder, src, dst, label);
}
}
diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/CountEdgesTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/CountEdgesTest.java
new file mode 100644
index 0000000..7445671
--- /dev/null
+++ b/java/src/test/java/org/softwareheritage/graph/rpc/CountEdgesTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2022 The Software Heritage developers
+ * See the AUTHORS file at the top-level directory of this distribution
+ * License: GNU General Public License version 3, or any later version
+ * See top-level LICENSE file for more information
+ */
+
+package org.softwareheritage.graph.rpc;
+
+import com.google.protobuf.FieldMask;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import org.junit.jupiter.api.Test;
+import org.softwareheritage.graph.SWHID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class CountEdgesTest extends TraversalServiceTest {
+ private TraversalRequest.Builder getTraversalRequestBuilder(SWHID src) {
+ return TraversalRequest.newBuilder().addSrc(src.toString());
+ }
+
+ @Test
+ public void testSwhidErrors() {
+ StatusRuntimeException thrown;
+ thrown = assertThrows(StatusRuntimeException.class, () -> client
+ .countEdges(TraversalRequest.newBuilder().addSrc(fakeSWHID("cnt", 404).toString()).build()));
+ assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode());
+ thrown = assertThrows(StatusRuntimeException.class, () -> client.countEdges(
+ TraversalRequest.newBuilder().addSrc("swh:1:lol:0000000000000000000000000000000000000001").build()));
+ assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode());
+ thrown = assertThrows(StatusRuntimeException.class, () -> client.countEdges(
+ TraversalRequest.newBuilder().addSrc("swh:1:cnt:000000000000000000000000000000000000000z").build()));
+ assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode());
+ }
+
+ @Test
+ public void forwardFromRoot() {
+ CountResponse actual = client.countEdges(getTraversalRequestBuilder(new SWHID(TEST_ORIGIN_ID)).build());
+ assertEquals(13, actual.getCount());
+ }
+
+ @Test
+ public void forwardFromMiddle() {
+ CountResponse actual = client.countEdges(getTraversalRequestBuilder(fakeSWHID("dir", 12)).build());
+ assertEquals(7, actual.getCount());
+ }
+
+ @Test
+ public void forwardRelRev() {
+ CountResponse actual = client
+ .countEdges(getTraversalRequestBuilder(fakeSWHID("rel", 10)).setEdges("rel:rev,rev:rev").build());
+ assertEquals(2, actual.getCount());
+ }
+
+ @Test
+ public void backwardFromMiddle() {
+ CountResponse actual = client.countEdges(
+ getTraversalRequestBuilder(fakeSWHID("dir", 12)).setDirection(GraphDirection.BACKWARD).build());
+ assertEquals(3, actual.getCount());
+ }
+
+ @Test
+ public void backwardFromLeaf() {
+ CountResponse actual = client.countEdges(
+ getTraversalRequestBuilder(fakeSWHID("cnt", 4)).setDirection(GraphDirection.BACKWARD).build());
+ assertEquals(12, actual.getCount());
+ }
+
+ @Test
+ public void backwardRevToRevRevToRel() {
+ CountResponse actual = client.countEdges(getTraversalRequestBuilder(fakeSWHID("rev", 3))
+ .setEdges("rev:rev,rev:rel").setDirection(GraphDirection.BACKWARD).build());
+ assertEquals(5, actual.getCount());
+ }
+
+ @Test
+ public void testWithEmptyMask() {
+ CountResponse actual = client.countEdges(
+ getTraversalRequestBuilder(fakeSWHID("dir", 12)).setMask(FieldMask.getDefaultInstance()).build());
+ assertEquals(7, actual.getCount());
+ }
+}
diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/CountNodesTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/CountNodesTest.java
new file mode 100644
index 0000000..a0bebc1
--- /dev/null
+++ b/java/src/test/java/org/softwareheritage/graph/rpc/CountNodesTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2022 The Software Heritage developers
+ * See the AUTHORS file at the top-level directory of this distribution
+ * License: GNU General Public License version 3, or any later version
+ * See top-level LICENSE file for more information
+ */
+
+package org.softwareheritage.graph.rpc;
+
+import com.google.protobuf.FieldMask;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import org.junit.jupiter.api.Test;
+import org.softwareheritage.graph.SWHID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class CountNodesTest extends TraversalServiceTest {
+ private TraversalRequest.Builder getTraversalRequestBuilder(SWHID src) {
+ return TraversalRequest.newBuilder().addSrc(src.toString());
+ }
+
+ @Test
+ public void testSwhidErrors() {
+ StatusRuntimeException thrown;
+ thrown = assertThrows(StatusRuntimeException.class, () -> client
+ .countNodes(TraversalRequest.newBuilder().addSrc(fakeSWHID("cnt", 404).toString()).build()));
+ assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode());
+ thrown = assertThrows(StatusRuntimeException.class, () -> client.countNodes(
+ TraversalRequest.newBuilder().addSrc("swh:1:lol:0000000000000000000000000000000000000001").build()));
+ assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode());
+ thrown = assertThrows(StatusRuntimeException.class, () -> client.countNodes(
+ TraversalRequest.newBuilder().addSrc("swh:1:cnt:000000000000000000000000000000000000000z").build()));
+ assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode());
+ }
+
+ @Test
+ public void forwardFromRoot() {
+ CountResponse actual = client.countNodes(getTraversalRequestBuilder(new SWHID(TEST_ORIGIN_ID)).build());
+ assertEquals(12, actual.getCount());
+ }
+
+ @Test
+ public void forwardFromMiddle() {
+ CountResponse actual = client.countNodes(getTraversalRequestBuilder(fakeSWHID("dir", 12)).build());
+ assertEquals(8, actual.getCount());
+ }
+
+ @Test
+ public void forwardRelRev() {
+ CountResponse actual = client
+ .countNodes(getTraversalRequestBuilder(fakeSWHID("rel", 10)).setEdges("rel:rev,rev:rev").build());
+ assertEquals(3, actual.getCount());
+ }
+
+ @Test
+ public void backwardFromMiddle() {
+ CountResponse actual = client.countNodes(
+ getTraversalRequestBuilder(fakeSWHID("dir", 12)).setDirection(GraphDirection.BACKWARD).build());
+ assertEquals(4, actual.getCount());
+ }
+
+ @Test
+ public void backwardFromLeaf() {
+ CountResponse actual = client.countNodes(
+ getTraversalRequestBuilder(fakeSWHID("cnt", 4)).setDirection(GraphDirection.BACKWARD).build());
+ assertEquals(11, actual.getCount());
+ }
+
+ @Test
+ public void backwardRevToRevRevToRel() {
+ CountResponse actual = client.countNodes(getTraversalRequestBuilder(fakeSWHID("rev", 3))
+ .setEdges("rev:rev,rev:rel").setDirection(GraphDirection.BACKWARD).build());
+ assertEquals(6, actual.getCount());
+ }
+
+ @Test
+ public void testWithEmptyMask() {
+ CountResponse actual = client.countNodes(
+ getTraversalRequestBuilder(fakeSWHID("dir", 12)).setMask(FieldMask.getDefaultInstance()).build());
+ assertEquals(8, actual.getCount());
+ }
+}
diff --git a/proto/swhgraph.proto b/proto/swhgraph.proto
index 87da0cb..192e7ff 100644
--- a/proto/swhgraph.proto
+++ b/proto/swhgraph.proto
@@ -1,145 +1,146 @@
syntax = "proto3";
import "google/protobuf/field_mask.proto";
option java_multiple_files = true;
option java_package = "org.softwareheritage.graph.rpc";
option java_outer_classname = "GraphService";
package swh.graph;
service TraversalService {
rpc Traverse (TraversalRequest) returns (stream Node);
rpc FindPathTo (FindPathToRequest) returns (Path);
rpc FindPathBetween (FindPathBetweenRequest) returns (Path);
rpc CountNodes (TraversalRequest) returns (CountResponse);
rpc CountEdges (TraversalRequest) returns (CountResponse);
rpc Stats (StatsRequest) returns (StatsResponse);
rpc GetNode (GetNodeRequest) returns (Node);
}
enum GraphDirection {
FORWARD = 0;
BACKWARD = 1;
}
message TraversalRequest {
repeated string src = 1;
GraphDirection direction = 2;
optional string edges = 3;
optional int64 max_edges = 4;
optional int64 min_depth = 5;
optional int64 max_depth = 6;
optional NodeFilter return_nodes = 7;
optional google.protobuf.FieldMask mask = 8;
}
message FindPathToRequest {
repeated string src = 1;
optional NodeFilter target = 2;
GraphDirection direction = 3;
optional string edges = 4;
optional int64 max_edges = 5;
optional int64 max_depth = 6;
optional google.protobuf.FieldMask mask = 7;
}
message FindPathBetweenRequest {
repeated string src = 1;
repeated string dst = 2;
GraphDirection direction = 3;
optional GraphDirection direction_reverse = 4;
optional string edges = 5;
optional string edges_reverse = 6;
optional int64 max_edges = 7;
optional int64 max_depth = 8;
optional google.protobuf.FieldMask mask = 9;
}
message NodeFilter {
optional string types = 1;
optional int64 min_traversal_successors = 2;
optional int64 max_traversal_successors = 3;
}
message Node {
string swhid = 1;
repeated Successor successor = 2;
oneof data {
ContentData cnt = 3;
RevisionData rev = 5;
ReleaseData rel = 6;
OriginData ori = 8;
};
+ optional int64 num_successors = 9;
}
message Path {
repeated Node node = 1;
optional int64 middle_node_index = 2;
}
message Successor {
optional string swhid = 1;
repeated EdgeLabel label = 2;
}
message ContentData {
optional int64 length = 1;
optional bool is_skipped = 2;
}
message RevisionData {
optional int64 author = 1;
optional int64 author_date = 2;
optional int32 author_date_offset = 3;
optional int64 committer = 4;
optional int64 committer_date = 5;
optional int32 committer_date_offset = 6;
optional bytes message = 7;
}
message ReleaseData {
optional int64 author = 1;
optional int64 author_date = 2;
optional int32 author_date_offset = 3;
optional bytes name = 4;
optional bytes message = 5;
}
message OriginData {
optional string url = 1;
}
message EdgeLabel {
bytes name = 1;
int32 permission = 2;
}
message CountResponse {
int64 count = 1;
}
message StatsRequest {
}
message StatsResponse {
int64 num_nodes = 1;
int64 num_edges = 2;
double compression = 3;
double bits_per_node = 4;
double bits_per_edge = 5;
double avg_locality = 6;
int64 indegree_min = 7;
int64 indegree_max = 8;
double indegree_avg = 9;
int64 outdegree_min = 10;
int64 outdegree_max = 11;
double outdegree_avg = 12;
}
message GetNodeRequest {
string swhid = 1;
optional google.protobuf.FieldMask mask = 8;
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jul 4, 12:16 PM (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3292622
Attached To
rDGRPH Compressed graph representation
Event Timeline
Log In to Comment