/** * Logic that decides how to proceed after a failed Thrift RPC. If it appears to be a stale * connection, this method will not throw as a signal to retry the RPC. Otherwise, it rethrows the * exception that was passed in. */ private void investigateAndPossiblyRethrowException(TException originalException) throws TException { // Because we got a TException when making a Thrift call, we will dereference the current Thrift // client such that getConnectedClient() will create a new one the next time it is called. thriftClient = null; Throwable e = originalException; while (e != null && !(e instanceof LastErrorException)) { e = e.getCause(); } if (e != null) { // e is a LastErrorException, so it's likely that it was a "Broken pipe" exception, which // happens when the Eden server decides the Thrift client has been idle for too long and // closes the connection. LOG.info(e, "Suspected closed Thrift connection: will create a new one."); } else { throw originalException; } }
@Test public void computeSha1ForOrdinaryFileUnderMount() throws IOException, EdenError, TException { FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path root = fs.getPath(JIMFS_WORKING_DIRECTORY); ProjectFilesystemDelegate delegate = new DefaultProjectFilesystemDelegate(root); EdenMount mount = createMock(EdenMount.class); Path path = fs.getPath("foo/bar"); expect(mount.getBindMounts()).andReturn(ImmutableList.of()); expect(mount.getPathRelativeToProjectRoot(root.resolve(path))).andReturn(Optional.of(path)); expect(mount.getSha1(path)).andReturn(DUMMY_SHA1); replay(mount); EdenProjectFilesystemDelegate edenDelegate = new EdenProjectFilesystemDelegate(mount, delegate); assertEquals(DUMMY_SHA1, edenDelegate.computeSha1(path)); verify(mount); }
@Override public int run(EdenClientPool pool) throws EdenError, IOException, TException { EdenClient client = pool.getClient(); List<MountInfo> mountInfos = client.listMounts(); System.out.printf("Number of mounts: %d\n", mountInfos.size()); for (MountInfo info : mountInfos) { System.out.println(info.mountPoint); EdenMount mount = EdenMount.createEdenMountForProjectRoot(Paths.get(info.mountPoint), pool).get(); List<Path> bindMounts = mount.getBindMounts(); System.out.printf(" Number of bind mounts: %d\n", bindMounts.size()); for (Path bindMount : bindMounts) { System.out.printf(" %s\n", bindMount); } } return 0; }
@Test public void getSha1DelegatesToThriftClient() throws EdenError, IOException, TException { final EdenClient thriftClient = createMock(EdenClient.class); FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path entry = fs.getPath("LICENSE"); HashCode hash = HashCode.fromString("2b8b815229aa8a61e483fb4ba0588b8b6c491890"); SHA1Result sha1Result = new SHA1Result(); sha1Result.setSha1(hash.asBytes()); expect(thriftClient.getSHA1("/home/mbolin/src/buck", ImmutableList.of("LICENSE"))) .andReturn(ImmutableList.of(sha1Result)); replay(thriftClient); EdenClientPool pool = new EdenClientPool(thriftClient); Path pathToBuck = fs.getPath("/home/mbolin/src/buck"); Files.createDirectories(pathToBuck.resolve(".eden")); Files.createSymbolicLink(pathToBuck.resolve(".eden").resolve("root"), pathToBuck); Optional<EdenMount> mount = EdenMount.createEdenMountForProjectRoot(pathToBuck, pool); assertTrue("Should find mount for path.", mount.isPresent()); assertEquals(Sha1HashCode.fromHashCode(hash), mount.get().getSha1(entry)); verify(thriftClient); }
@Override public List<SHA1Result> getSHA1(String mountPoint, List<String> paths) throws EdenError, IOException, TException { try { return attemptGetSHA1(mountPoint, paths); } catch (TException e) { investigateAndPossiblyRethrowException(e); } return attemptGetSHA1(mountPoint, paths); }
@Override public List<String> getBindMounts(String mountPoint) throws EdenError, IOException, TException { try { return attemptGetBindMounts(mountPoint); } catch (TException e) { investigateAndPossiblyRethrowException(e); } return attemptGetBindMounts(mountPoint); }
@Override public List<MountInfo> listMounts() throws EdenError, IOException, TException { try { return attemptListMounts(); } catch (TException e) { investigateAndPossiblyRethrowException(e); } return attemptListMounts(); }
/** @param entry is a path that is relative to {@link #getProjectRoot()}. */ public Sha1HashCode getSha1(Path entry) throws EdenError, IOException, TException { List<SHA1Result> results = pool.getClient().getSHA1(mountPoint, ImmutableList.of(normalizePathArg(entry))); SHA1Result result = Iterables.getOnlyElement(results); if (result.getSetField() == SHA1Result.SHA1) { return Sha1HashCode.fromBytes(result.getSha1()); } else { throw result.getError(); } }
public ImmutableList<Path> getBindMounts() { List<String> bindMounts; try { bindMounts = pool.getClient().getBindMounts(mountPoint); } catch (EdenError | IOException | TException e) { throw new RuntimeException(e); } return bindMounts.stream().map(Paths::get).collect(ImmutableList.toImmutableList()); }
public static Optional<EdenClientPool> newInstanceFromSocket(final Path socketFile) { // We forcibly try to create an EdenClient as a way of verifying that `socketFile` is a // valid UNIX domain socket for talking to Eden. If this is not the case, then we should not // return a new EdenClientPool. ReconnectingEdenClient edenClient = new ReconnectingEdenClient(socketFile, clock); try { edenClient.listMounts(); } catch (EdenError | IOException | TException e) { return Optional.empty(); } return Optional.of(new EdenClientPool(socketFile)); }
@Test public void getMountInfosDelegatesToThriftClient() throws EdenError, IOException, TException { List<MountInfo> mountInfos = ImmutableList.of( new MountInfo("/home/mbolin/src/buck", /* edenClientPath */ ""), new MountInfo("/home/mbolin/src/eden", /* edenClientPath */ "")); expect(thriftClient.listMounts()).andReturn(mountInfos); replay(thriftClient); assertEquals(mountInfos, pool.getClient().listMounts()); verify(thriftClient); }
@Test public void getMountForMatchesProjectRootEqualToMount() throws EdenError, IOException, TException { Path projectRoot = fs.getPath("/home/mbolin/src/eden"); Files.createDirectories(projectRoot.resolve(".eden")); Files.createSymbolicLink(projectRoot.resolve(".eden").resolve("root"), projectRoot); Optional<EdenMount> mount = EdenMount.createEdenMountForProjectRoot(projectRoot, pool); assertTrue("Should find mount for path.", mount.isPresent()); assertEquals(fs.getPath("/home/mbolin/src/eden"), mount.get().getProjectRoot()); assertEquals(fs.getPath(""), mount.get().getPrefix()); }
@Test public void getMountForMatchesProjectRootUnderMount() throws EdenError, IOException, TException { Path edenMountRoot = fs.getPath("/home/mbolin/src/eden"); Path projectRoot = fs.getPath("/home/mbolin/src/eden/deep/project"); Files.createDirectories(projectRoot.resolve(".eden")); Files.createSymbolicLink(projectRoot.resolve(".eden").resolve("root"), edenMountRoot); Optional<EdenMount> mount = EdenMount.createEdenMountForProjectRoot(projectRoot, pool); assertTrue("Should find mount for path.", mount.isPresent()); assertEquals(projectRoot, mount.get().getProjectRoot()); assertEquals(fs.getPath("deep/project"), mount.get().getPrefix()); }
@Test public void getMountForReturnsNullWhenMissingMountPoint() throws EdenError, IOException, TException { Path projectRoot = Paths.get("/home/mbolin/src/other_project"); Optional<EdenMount> mount = EdenMount.createEdenMountForProjectRoot(projectRoot, pool); assertFalse(mount.isPresent()); }
@Test public void computeSha1ForOrdinaryFileUnderMountButBehindBindMount() throws IOException, EdenError, TException { FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path root = fs.getPath(JIMFS_WORKING_DIRECTORY); ProjectFilesystemDelegate delegate = new DefaultProjectFilesystemDelegate(root); EdenMount mount = createMock(EdenMount.class); Path path = fs.getPath("buck-out/gen/some-output"); Files.createDirectories(path.getParent()); Files.createFile(path); byte[] bytes = new byte[] {66, 85, 67, 75}; Files.write(path, bytes); expect(mount.getBindMounts()).andReturn(ImmutableList.of(fs.getPath("buck-out"))); expect(mount.getPathRelativeToProjectRoot(root.resolve(path))).andReturn(Optional.of(path)); replay(mount); EdenProjectFilesystemDelegate edenDelegate = new EdenProjectFilesystemDelegate(mount, delegate); assertEquals( "EdenProjectFilesystemDelegate.computeSha1() should compute the SHA-1 directly via " + "DefaultProjectFilesystemDelegate because the path is behind a bind mount.", Sha1HashCode.fromHashCode(Hashing.sha1().hashBytes(bytes)), edenDelegate.computeSha1(path)); verify(mount); }
@Test public void computeSha1ForSymlinkUnderMountThatPointsToFileUnderMount() throws EdenError, TException, IOException { FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path root = fs.getPath(JIMFS_WORKING_DIRECTORY); ProjectFilesystemDelegate delegate = new DefaultProjectFilesystemDelegate(root); // Create a symlink within the project root. Path link = fs.getPath("/work/link"); Path target = fs.getPath("/work/target"); Files.createFile(target); Files.createSymbolicLink(link, target); // Eden will throw when the SHA-1 for the link is requested, but return a SHA-1 when the target // is requested. EdenMount mount = createMock(EdenMount.class); expect(mount.getBindMounts()).andReturn(ImmutableList.of()); expect(mount.getPathRelativeToProjectRoot(link)).andReturn(Optional.of(fs.getPath("link"))); expect(mount.getPathRelativeToProjectRoot(target)).andReturn(Optional.of(fs.getPath("target"))); expect(mount.getSha1(fs.getPath("link"))).andThrow(new EdenError()); expect(mount.getSha1(fs.getPath("target"))).andReturn(DUMMY_SHA1); replay(mount); EdenProjectFilesystemDelegate edenDelegate = new EdenProjectFilesystemDelegate(mount, delegate); assertEquals(DUMMY_SHA1, edenDelegate.computeSha1(link)); verify(mount); }
@Test public void computeSha1ForSymlinkUnderMountThatPointsToFileOutsideMount() throws IOException, EdenError, TException { FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); Path root = fs.getPath(JIMFS_WORKING_DIRECTORY); ProjectFilesystemDelegate delegate = new DefaultProjectFilesystemDelegate(root); // Create a symlink within the project root. Path link = fs.getPath("/work/link"); Path target = fs.getPath("/example"); Files.createFile(target); byte[] bytes = new byte[] {66, 85, 67, 75}; Files.write(target, bytes); Files.createSymbolicLink(link, target); // Eden will throw when the SHA-1 for the link is requested, but return a SHA-1 when the target // is requested. EdenMount mount = createMock(EdenMount.class); expect(mount.getBindMounts()).andReturn(ImmutableList.of()); expect(mount.getPathRelativeToProjectRoot(link)).andReturn(Optional.of(fs.getPath("link"))); expect(mount.getPathRelativeToProjectRoot(target)).andReturn(Optional.empty()); expect(mount.getSha1(fs.getPath("link"))).andThrow(new EdenError()); replay(mount); EdenProjectFilesystemDelegate edenDelegate = new EdenProjectFilesystemDelegate(mount, delegate); assertEquals( "EdenProjectFilesystemDelegate.computeSha1() should return the SHA-1 of the target of " + "the symlink even though the target is outside of the EdenFS root.", Sha1HashCode.fromHashCode(Hashing.sha1().hashBytes(bytes)), edenDelegate.computeSha1(link)); verify(mount); }
@Override public int run(EdenClientPool pool) throws EdenError, IOException, TException { Path mountPoint = Paths.get(this.mountPoint); EdenMount mount = EdenMount.createEdenMountForProjectRoot(mountPoint, pool).get(); for (String path : paths) { Path entry = mountPoint.relativize(Paths.get(path)); Sha1HashCode sha1 = mount.getSha1(entry); System.out.printf("%s %s\n", entry, sha1); } return 0; }
private List<SHA1Result> attemptGetSHA1(String mountPoint, List<String> paths) throws EdenError, IOException, TException { List<SHA1Result> sha1s = getConnectedClient().getSHA1(mountPoint, paths); lastSuccessfulRequest = clock.currentTimeMillis(); return sha1s; }
private List<String> attemptGetBindMounts(String mountPoint) throws EdenError, IOException, TException { List<String> bindMounts = getConnectedClient().getBindMounts(mountPoint); lastSuccessfulRequest = clock.currentTimeMillis(); return bindMounts; }
private List<MountInfo> attemptListMounts() throws EdenError, IOException, TException { List<MountInfo> mountInfos = getConnectedClient().listMounts(); lastSuccessfulRequest = clock.currentTimeMillis(); return mountInfos; }
List<SHA1Result> getSHA1(String mountPoint, List<String> paths) throws EdenError, IOException, TException;
@Test public void requestToAStaleClientShouldBeRetriedWithAFreshClient() throws EdenError, IOException, TException, TTransportException { String mountPoint = "/some/mountPoint"; List<String> paths = ImmutableList.of(".buckconfig"); HashCode hash = HashCode.fromString("2b8b815229aa8a61e483fb4ba0588b8b6c491890"); SHA1Result sha1Result = new SHA1Result(); sha1Result.setSha1(hash.asBytes()); long currentTime = 1000L; SettableFakeClock clock = new SettableFakeClock(currentTime, 0); // Stale client that throws a TException that has a LastErrorException as a cause. EdenService.Client staleClient = createMock(EdenService.Client.class); TException exceptionBackedByLastErrorException = new TException(new LastErrorException("Broken pipe")); expect(staleClient.getSHA1(mountPoint, paths)).andThrow(exceptionBackedByLastErrorException); // A connected client that should succeed. EdenService.Client connectedClient = createMock(EdenService.Client.class); expect(connectedClient.getSHA1(mountPoint, paths)) .andReturn(ImmutableList.of(sha1Result)) .times(2); // A connected client that should succeed. EdenService.Client secondConnectedClient = createMock(EdenService.Client.class); expect(secondConnectedClient.getSHA1(mountPoint, paths)) .andReturn(ImmutableList.of(sha1Result)); // The calls to client.getSHA1() should trigger the following behavior: // - First call to client.getSHA1() should call createNewThriftClient() twice: // - First, it should return a client that is demonstrably stale because of the TException it // throws. // - Next, it should return a fresh client to replace the stale one. // - Second call to client.getSHA1() should reuse the existing Thrift client. // - Third call to client.getSHA1() should request a new Thrift client because its existing // Thrift client has not been used within the idle time threshold. ReconnectingEdenClient.ThriftClientFactory thriftClientFactory = createMock(ReconnectingEdenClient.ThriftClientFactory.class); expect(thriftClientFactory.createNewThriftClient()).andReturn(staleClient); expect(thriftClientFactory.createNewThriftClient()).andReturn(connectedClient); expect(thriftClientFactory.createNewThriftClient()).andReturn(secondConnectedClient); replayAll(); ReconnectingEdenClient client = new ReconnectingEdenClient(thriftClientFactory, clock); List<SHA1Result> sha1s_1 = client.getSHA1(mountPoint, paths); assertEquals(ImmutableList.of(sha1Result), sha1s_1); // Use the client 1 millisecond before the idle time threshold kicks in. currentTime += ReconnectingEdenClient.IDLE_TIME_THRESHOLD_IN_MILLIS - 1; clock.setCurrentTimeMillis(currentTime); List<SHA1Result> sha1s_2 = client.getSHA1(mountPoint, paths); assertEquals(ImmutableList.of(sha1Result), sha1s_2); // Use the client when the idle time threshold kicks in. currentTime += ReconnectingEdenClient.IDLE_TIME_THRESHOLD_IN_MILLIS; clock.setCurrentTimeMillis(currentTime); List<SHA1Result> sha1s_3 = client.getSHA1(mountPoint, paths); assertEquals(ImmutableList.of(sha1Result), sha1s_3); verifyAll(); }
@Before public void setUp() throws EdenError, TException { thriftClient = createMock(EdenClient.class); fs = Jimfs.newFileSystem(Configuration.unix()); pool = new EdenClientPool(thriftClient); }
/** Runs the command and returns the exit code that reflects the termination status. */ int run(EdenClientPool pool) throws EdenError, IOException, TException;
public int run(EdenClientPool pool) throws EdenError, IOException, TException { Preconditions.checkNotNull(command, "command must be set by args4j"); return command.run(pool); }
List<String> getBindMounts(String mountPoint) throws EdenError, IOException, TException;
List<MountInfo> listMounts() throws EdenError, IOException, TException;