package com.hubspot.mesos; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Random; import java.util.Set; import org.apache.mesos.Protos.MasterInfo; import org.apache.mesos.Protos.Offer; import org.apache.mesos.Protos.Resource; import org.apache.mesos.Protos.TaskState; import org.apache.mesos.Protos.Value; import org.apache.mesos.Protos.Value.Range; import org.apache.mesos.Protos.Value.Ranges; import org.apache.mesos.Protos.Value.Type; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.net.InetAddresses; import com.google.common.primitives.Longs; public final class MesosUtils { public static final String CPUS = "cpus"; public static final String MEMORY = "mem"; public static final String PORTS = "ports"; public static final String DISK = "disk"; private MesosUtils() { } private static double getScalar(Resource r) { return r.getScalar().getValue(); } private static double getScalar(List<Resource> resources, String name, Optional<String> requiredRole) { for (Resource r : resources) { if (r.hasName() && r.getName().equals(name) && r.hasScalar() && hasRequiredRole(r, requiredRole)) { return getScalar(r); } } return 0; } private static boolean hasRole(Resource r) { return r.hasRole() && !r.getRole().equals("*"); } private static boolean hasRequiredRole(Resource r, Optional<String> requiredRole) { if (requiredRole.isPresent() && hasRole(r)) { // required role with a resource with role return requiredRole.get().equals(r.getRole()); } else if (requiredRole.isPresent() && !hasRole(r)) { // required role with a resource for any role return false; } else if (!requiredRole.isPresent() && hasRole(r)) { // no required role but resource with role return false; } else if (!requiredRole.isPresent() && !hasRole(r)) { // no required role and resource for any role return true; } else { return false; } } private static Ranges getRanges(List<Resource> resources, String name) { for (Resource r : resources) { if (r.hasName() && r.getName().equals(name) && r.hasRanges()) { return r.getRanges(); } } return Ranges.getDefaultInstance(); } private static int getNumRanges(List<Resource> resources, String name) { int totalRanges = 0; for (Range range : getRanges(resources, name).getRangeList()) { totalRanges += (range.getEnd() - range.getBegin()) + 1; } return totalRanges; } public static Resource getCpuResource(double cpus, Optional<String> role) { return newScalar(CPUS, cpus, role); } public static Resource getMemoryResource(double memory, Optional<String> role) { return newScalar(MEMORY, memory, role); } public static Resource getDiskResource(double disk, Optional<String> role) { return newScalar(DISK, disk, role); } public static long[] getPorts(Resource portsResource, int numPorts) { long[] ports = new long[numPorts]; if (numPorts == 0) { return ports; } int idx = 0; for (Range r : portsResource.getRanges().getRangeList()) { for (long port = r.getBegin(); port <= r.getEnd(); port++) { ports[idx++] = port; if (idx >= numPorts) { return ports; } } } return ports; } public static Resource getPortRangeResource(long begin, long end) { return newRange(PORTS, begin, end); } public static List<Long> getAllPorts(List<Resource> resources) { Ranges ranges = getRanges(resources, PORTS); final List<Long> ports = Lists.newArrayList(); if (ranges != null) { for (Range range : ranges.getRangeList()) { for (long port = range.getBegin(); port <= range.getEnd(); port++) { ports.add(port); } } } return ports; } public static Resource getPortsResource(int numPorts, Offer offer) { return getPortsResource(numPorts, offer.getResourcesList(), Collections.<Long>emptyList()); } public static Resource getPortsResource(int numPorts, List<Resource> resources, List<Long> otherRequestedPorts) { List<Long> requestedPorts = new ArrayList<>(otherRequestedPorts); Ranges ranges = getRanges(resources, PORTS); Preconditions.checkState(ranges.getRangeCount() > 0, "Ports %s should have existed in resources %s", PORTS, formatForLogging(resources)); Ranges.Builder rangesBldr = Ranges.newBuilder(); int portsSoFar = 0; List<Range> offerRangeList = Lists.newArrayList(ranges.getRangeList()); Random random = new Random(); Collections.shuffle(offerRangeList, random); if (numPorts > 0) { for (Range range : offerRangeList) { long rangeStartSelection = Math.max(range.getBegin(), range.getEnd() - (numPorts - portsSoFar + 1)); if (rangeStartSelection != range.getBegin()) { int rangeDelta = (int) (rangeStartSelection - range.getBegin()) + 1; rangeStartSelection = random.nextInt(rangeDelta) + range.getBegin(); } long rangeEndSelection = Math.min(range.getEnd(), rangeStartSelection + (numPorts - portsSoFar - 1)); rangesBldr.addRange(Range.newBuilder() .setBegin(rangeStartSelection) .setEnd(rangeEndSelection)); portsSoFar += (rangeEndSelection - rangeStartSelection) + 1; List<Long> toRemove = new ArrayList<>(); for (long port : requestedPorts) { if (rangeStartSelection >= port && rangeEndSelection <= port) { toRemove.add(port); portsSoFar--; } } requestedPorts.removeAll(toRemove); if (portsSoFar == numPorts) { break; } } } for (long port : requestedPorts) { rangesBldr.addRange(Range.newBuilder() .setBegin(port) .setEnd(port) .build()); } return Resource.newBuilder() .setType(Type.RANGES) .setName(PORTS) .setRanges(rangesBldr) .build(); } private static Resource newScalar(String name, double value, Optional<String> role) { Resource.Builder builder = Resource.newBuilder().setName(name).setType(Value.Type.SCALAR).setScalar(Value.Scalar.newBuilder().setValue(value).build()); if (role.isPresent()) { builder.setRole(role.get()); } return builder.build(); } private static Resource newRange(String name, long begin, long end) { return Resource.newBuilder().setName(name).setType(Type.RANGES).setRanges(Ranges.newBuilder().addRange(Range.newBuilder().setBegin(begin).setEnd(end).build()).build()).build(); } public static Set<String> getRoles(Offer offer) { Set<String> roles = Sets.newHashSet(); for (Resource r : offer.getResourcesList()) { roles.add(r.getRole()); } return roles; } public static double getNumCpus(Offer offer) { return getNumCpus(offer.getResourcesList(), Optional.<String>absent()); } public static double getMemory(Offer offer) { return getMemory(offer.getResourcesList(), Optional.<String>absent()); } public static double getDisk(Offer offer) { return getDisk(offer.getResourcesList(), Optional.<String>absent()); } public static double getNumCpus(List<Resource> resources, Optional<String> requiredRole) { return getScalar(resources, CPUS, requiredRole); } public static double getMemory(List<Resource> resources, Optional<String> requiredRole) { return getScalar(resources, MEMORY, requiredRole); } public static double getDisk(List<Resource> resources, Optional<String> requiredRole) { return getScalar(resources, DISK, requiredRole); } public static int getNumPorts(List<Resource> resources) { return getNumRanges(resources, PORTS); } public static int getNumPorts(Offer offer) { return getNumPorts(offer.getResourcesList()); } public static boolean doesOfferMatchResources(Optional<String> requiredRole, Resources resources, List<Resource> offerResources, List<Long> otherRequestedPorts) { double numCpus = getNumCpus(offerResources, requiredRole); if (numCpus < resources.getCpus()) { return false; } double memory = getMemory(offerResources, requiredRole); if (memory < resources.getMemoryMb()) { return false; } double disk = getDisk(offerResources, requiredRole); if (disk < resources.getDiskMb()) { return false; } int numPorts = getNumPorts(offerResources); if (numPorts < resources.getNumPorts()) { return false; } if (!getAllPorts(offerResources).containsAll(otherRequestedPorts)) { return false; } return true; } public static boolean isTaskDone(TaskState state) { return state == TaskState.TASK_FAILED || state == TaskState.TASK_LOST || state == TaskState.TASK_KILLED || state == TaskState.TASK_FINISHED; } public static String getMasterHostAndPort(MasterInfo masterInfo) { byte[] fromIp = ByteBuffer.allocate(4).putInt(masterInfo.getIp()).array(); try { return String.format("%s:%s", InetAddresses.fromLittleEndianByteArray(fromIp).getHostAddress(), masterInfo.getPort()); } catch (UnknownHostException e) { throw Throwables.propagate(e); } } private static Optional<Resource> getMatchingResource(Resource toMatch, List<Resource> resources) { for (Resource resource : resources) { if (toMatch.getName().equals(resource.getName())) { return Optional.of(resource); } } return Optional.absent(); } private static final Comparator<Range> RANGE_COMPARATOR = new Comparator<Range>() { @Override public int compare(Range o1, Range o2) { return Longs.compare(o1.getBegin(), o2.getBegin()); } }; private static Ranges subtractRanges(Ranges ranges, Ranges toSubtract) { Ranges.Builder newRanges = Ranges.newBuilder(); List<Range> sortedRanges = Lists.newArrayList(ranges.getRangeList()); Collections.sort(sortedRanges, RANGE_COMPARATOR); List<Range> subtractRanges = Lists.newArrayList(toSubtract.getRangeList()); Collections.sort(subtractRanges, RANGE_COMPARATOR); int s = 0; for (Range range : ranges.getRangeList()) { Range.Builder currentRange = range.toBuilder(); for (int i = s; i < subtractRanges.size(); i++) { Range matchedRange = subtractRanges.get(i); if (matchedRange.getBegin() < currentRange.getBegin() || matchedRange.getEnd() > currentRange.getEnd()) { s = i; break; } currentRange.setEnd(matchedRange.getBegin() - 1); if (currentRange.getEnd() >= currentRange.getBegin()) { newRanges.addRange(currentRange.build()); } currentRange = Range.newBuilder(); currentRange.setBegin(matchedRange.getEnd() + 1); currentRange.setEnd(range.getEnd()); } if (currentRange.getEnd() >= currentRange.getBegin()) { newRanges.addRange(currentRange.build()); } } return newRanges.build(); } public static List<Resource> subtractResources(List<Resource> resources, List<Resource> subtract) { List<Resource> remaining = Lists.newArrayListWithCapacity(resources.size()); for (Resource resource : resources) { Optional<Resource> matched = getMatchingResource(resource, subtract); if (!matched.isPresent()) { remaining.add(resource.toBuilder().clone().build()); } else { Resource.Builder resourceBuilder = resource.toBuilder().clone(); if (resource.hasScalar()) { resourceBuilder.setScalar(resource.toBuilder().getScalarBuilder().setValue(resource.getScalar().getValue() - matched.get().getScalar().getValue()).build()); } else if (resource.hasRanges()) { resourceBuilder.setRanges(subtractRanges(resource.getRanges(), matched.get().getRanges())); } else { throw new IllegalStateException(String.format("Can't subtract non-scalar or range resources %s", formatForLogging(resource))); } remaining.add(resourceBuilder.build()); } } return remaining; } public static Resources buildResourcesFromMesosResourceList(List<Resource> resources) { return new Resources(getNumCpus(resources, Optional.<String>absent()), getMemory(resources, Optional.<String>absent()), getNumPorts(resources), getDisk(resources, Optional.<String>absent())); } public static Path getTaskDirectoryPath(String taskId) { return Paths.get(getSafeTaskIdForDirectory(taskId)).toAbsolutePath(); } public static String getSafeTaskIdForDirectory(String taskId) { return taskId.replace(":", "_"); } public static String formatForLogging(Object object) { return object.toString().replace("\n", "").replaceAll("( )+", " "); } }