1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *    http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package org.apache.spark.network.util;
19 
20 import java.io.Closeable;
21 import java.io.File;
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.nio.charset.StandardCharsets;
25 import java.util.concurrent.TimeUnit;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 
29 import com.google.common.base.Preconditions;
30 import com.google.common.collect.ImmutableMap;
31 import io.netty.buffer.Unpooled;
32 import org.apache.commons.lang3.SystemUtils;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 
36 /**
37  * General utilities available in the network package. Many of these are sourced from Spark's
38  * own Utils, just accessible within this package.
39  */
40 public class JavaUtils {
41   private static final Logger logger = LoggerFactory.getLogger(JavaUtils.class);
42 
43   /**
44    * Define a default value for driver memory here since this value is referenced across the code
45    * base and nearly all files already use Utils.scala
46    */
47   public static final long DEFAULT_DRIVER_MEM_MB = 1024;
48 
49   /** Closes the given object, ignoring IOExceptions. */
closeQuietly(Closeable closeable)50   public static void closeQuietly(Closeable closeable) {
51     try {
52       if (closeable != null) {
53         closeable.close();
54       }
55     } catch (IOException e) {
56       logger.error("IOException should not have been thrown.", e);
57     }
58   }
59 
60   /** Returns a hash consistent with Spark's Utils.nonNegativeHash(). */
nonNegativeHash(Object obj)61   public static int nonNegativeHash(Object obj) {
62     if (obj == null) { return 0; }
63     int hash = obj.hashCode();
64     return hash != Integer.MIN_VALUE ? Math.abs(hash) : 0;
65   }
66 
67   /**
68    * Convert the given string to a byte buffer. The resulting buffer can be
69    * converted back to the same string through {@link #bytesToString(ByteBuffer)}.
70    */
stringToBytes(String s)71   public static ByteBuffer stringToBytes(String s) {
72     return Unpooled.wrappedBuffer(s.getBytes(StandardCharsets.UTF_8)).nioBuffer();
73   }
74 
75   /**
76    * Convert the given byte buffer to a string. The resulting string can be
77    * converted back to the same byte buffer through {@link #stringToBytes(String)}.
78    */
bytesToString(ByteBuffer b)79   public static String bytesToString(ByteBuffer b) {
80     return Unpooled.wrappedBuffer(b).toString(StandardCharsets.UTF_8);
81   }
82 
83   /**
84    * Delete a file or directory and its contents recursively.
85    * Don't follow directories if they are symlinks.
86    *
87    * @param file Input file / dir to be deleted
88    * @throws IOException if deletion is unsuccessful
89    */
deleteRecursively(File file)90   public static void deleteRecursively(File file) throws IOException {
91     if (file == null) { return; }
92 
93     // On Unix systems, use operating system command to run faster
94     // If that does not work out, fallback to the Java IO way
95     if (SystemUtils.IS_OS_UNIX) {
96       try {
97         deleteRecursivelyUsingUnixNative(file);
98         return;
99       } catch (IOException e) {
100         logger.warn("Attempt to delete using native Unix OS command failed for path = {}. " +
101                         "Falling back to Java IO way", file.getAbsolutePath(), e);
102       }
103     }
104 
105     deleteRecursivelyUsingJavaIO(file);
106   }
107 
deleteRecursivelyUsingJavaIO(File file)108   private static void deleteRecursivelyUsingJavaIO(File file) throws IOException {
109     if (file.isDirectory() && !isSymlink(file)) {
110       IOException savedIOException = null;
111       for (File child : listFilesSafely(file)) {
112         try {
113           deleteRecursively(child);
114         } catch (IOException e) {
115           // In case of multiple exceptions, only last one will be thrown
116           savedIOException = e;
117         }
118       }
119       if (savedIOException != null) {
120         throw savedIOException;
121       }
122     }
123 
124     boolean deleted = file.delete();
125     // Delete can also fail if the file simply did not exist.
126     if (!deleted && file.exists()) {
127       throw new IOException("Failed to delete: " + file.getAbsolutePath());
128     }
129   }
130 
deleteRecursivelyUsingUnixNative(File file)131   private static void deleteRecursivelyUsingUnixNative(File file) throws IOException {
132     ProcessBuilder builder = new ProcessBuilder("rm", "-rf", file.getAbsolutePath());
133     Process process = null;
134     int exitCode = -1;
135 
136     try {
137       // In order to avoid deadlocks, consume the stdout (and stderr) of the process
138       builder.redirectErrorStream(true);
139       builder.redirectOutput(new File("/dev/null"));
140 
141       process = builder.start();
142 
143       exitCode = process.waitFor();
144     } catch (Exception e) {
145       throw new IOException("Failed to delete: " + file.getAbsolutePath(), e);
146     } finally {
147       if (process != null) {
148         process.destroy();
149       }
150     }
151 
152     if (exitCode != 0 || file.exists()) {
153       throw new IOException("Failed to delete: " + file.getAbsolutePath());
154     }
155   }
156 
listFilesSafely(File file)157   private static File[] listFilesSafely(File file) throws IOException {
158     if (file.exists()) {
159       File[] files = file.listFiles();
160       if (files == null) {
161         throw new IOException("Failed to list files for dir: " + file);
162       }
163       return files;
164     } else {
165       return new File[0];
166     }
167   }
168 
isSymlink(File file)169   private static boolean isSymlink(File file) throws IOException {
170     Preconditions.checkNotNull(file);
171     File fileInCanonicalDir = null;
172     if (file.getParent() == null) {
173       fileInCanonicalDir = file;
174     } else {
175       fileInCanonicalDir = new File(file.getParentFile().getCanonicalFile(), file.getName());
176     }
177     return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
178   }
179 
180   private static final ImmutableMap<String, TimeUnit> timeSuffixes =
181     ImmutableMap.<String, TimeUnit>builder()
182       .put("us", TimeUnit.MICROSECONDS)
183       .put("ms", TimeUnit.MILLISECONDS)
184       .put("s", TimeUnit.SECONDS)
185       .put("m", TimeUnit.MINUTES)
186       .put("min", TimeUnit.MINUTES)
187       .put("h", TimeUnit.HOURS)
188       .put("d", TimeUnit.DAYS)
189       .build();
190 
191   private static final ImmutableMap<String, ByteUnit> byteSuffixes =
192     ImmutableMap.<String, ByteUnit>builder()
193       .put("b", ByteUnit.BYTE)
194       .put("k", ByteUnit.KiB)
195       .put("kb", ByteUnit.KiB)
196       .put("m", ByteUnit.MiB)
197       .put("mb", ByteUnit.MiB)
198       .put("g", ByteUnit.GiB)
199       .put("gb", ByteUnit.GiB)
200       .put("t", ByteUnit.TiB)
201       .put("tb", ByteUnit.TiB)
202       .put("p", ByteUnit.PiB)
203       .put("pb", ByteUnit.PiB)
204       .build();
205 
206   /**
207    * Convert a passed time string (e.g. 50s, 100ms, or 250us) to a time count in the given unit.
208    * The unit is also considered the default if the given string does not specify a unit.
209    */
timeStringAs(String str, TimeUnit unit)210   public static long timeStringAs(String str, TimeUnit unit) {
211     String lower = str.toLowerCase().trim();
212 
213     try {
214       Matcher m = Pattern.compile("(-?[0-9]+)([a-z]+)?").matcher(lower);
215       if (!m.matches()) {
216         throw new NumberFormatException("Failed to parse time string: " + str);
217       }
218 
219       long val = Long.parseLong(m.group(1));
220       String suffix = m.group(2);
221 
222       // Check for invalid suffixes
223       if (suffix != null && !timeSuffixes.containsKey(suffix)) {
224         throw new NumberFormatException("Invalid suffix: \"" + suffix + "\"");
225       }
226 
227       // If suffix is valid use that, otherwise none was provided and use the default passed
228       return unit.convert(val, suffix != null ? timeSuffixes.get(suffix) : unit);
229     } catch (NumberFormatException e) {
230       String timeError = "Time must be specified as seconds (s), " +
231               "milliseconds (ms), microseconds (us), minutes (m or min), hour (h), or day (d). " +
232               "E.g. 50s, 100ms, or 250us.";
233 
234       throw new NumberFormatException(timeError + "\n" + e.getMessage());
235     }
236   }
237 
238   /**
239    * Convert a time parameter such as (50s, 100ms, or 250us) to milliseconds for internal use. If
240    * no suffix is provided, the passed number is assumed to be in ms.
241    */
timeStringAsMs(String str)242   public static long timeStringAsMs(String str) {
243     return timeStringAs(str, TimeUnit.MILLISECONDS);
244   }
245 
246   /**
247    * Convert a time parameter such as (50s, 100ms, or 250us) to seconds for internal use. If
248    * no suffix is provided, the passed number is assumed to be in seconds.
249    */
timeStringAsSec(String str)250   public static long timeStringAsSec(String str) {
251     return timeStringAs(str, TimeUnit.SECONDS);
252   }
253 
254   /**
255    * Convert a passed byte string (e.g. 50b, 100kb, or 250mb) to the given. If no suffix is
256    * provided, a direct conversion to the provided unit is attempted.
257    */
byteStringAs(String str, ByteUnit unit)258   public static long byteStringAs(String str, ByteUnit unit) {
259     String lower = str.toLowerCase().trim();
260 
261     try {
262       Matcher m = Pattern.compile("([0-9]+)([a-z]+)?").matcher(lower);
263       Matcher fractionMatcher = Pattern.compile("([0-9]+\\.[0-9]+)([a-z]+)?").matcher(lower);
264 
265       if (m.matches()) {
266         long val = Long.parseLong(m.group(1));
267         String suffix = m.group(2);
268 
269         // Check for invalid suffixes
270         if (suffix != null && !byteSuffixes.containsKey(suffix)) {
271           throw new NumberFormatException("Invalid suffix: \"" + suffix + "\"");
272         }
273 
274         // If suffix is valid use that, otherwise none was provided and use the default passed
275         return unit.convertFrom(val, suffix != null ? byteSuffixes.get(suffix) : unit);
276       } else if (fractionMatcher.matches()) {
277         throw new NumberFormatException("Fractional values are not supported. Input was: "
278           + fractionMatcher.group(1));
279       } else {
280         throw new NumberFormatException("Failed to parse byte string: " + str);
281       }
282 
283     } catch (NumberFormatException e) {
284       String byteError = "Size must be specified as bytes (b), " +
285         "kibibytes (k), mebibytes (m), gibibytes (g), tebibytes (t), or pebibytes(p). " +
286         "E.g. 50b, 100k, or 250m.";
287 
288       throw new NumberFormatException(byteError + "\n" + e.getMessage());
289     }
290   }
291 
292   /**
293    * Convert a passed byte string (e.g. 50b, 100k, or 250m) to bytes for
294    * internal use.
295    *
296    * If no suffix is provided, the passed number is assumed to be in bytes.
297    */
byteStringAsBytes(String str)298   public static long byteStringAsBytes(String str) {
299     return byteStringAs(str, ByteUnit.BYTE);
300   }
301 
302   /**
303    * Convert a passed byte string (e.g. 50b, 100k, or 250m) to kibibytes for
304    * internal use.
305    *
306    * If no suffix is provided, the passed number is assumed to be in kibibytes.
307    */
byteStringAsKb(String str)308   public static long byteStringAsKb(String str) {
309     return byteStringAs(str, ByteUnit.KiB);
310   }
311 
312   /**
313    * Convert a passed byte string (e.g. 50b, 100k, or 250m) to mebibytes for
314    * internal use.
315    *
316    * If no suffix is provided, the passed number is assumed to be in mebibytes.
317    */
byteStringAsMb(String str)318   public static long byteStringAsMb(String str) {
319     return byteStringAs(str, ByteUnit.MiB);
320   }
321 
322   /**
323    * Convert a passed byte string (e.g. 50b, 100k, or 250m) to gibibytes for
324    * internal use.
325    *
326    * If no suffix is provided, the passed number is assumed to be in gibibytes.
327    */
byteStringAsGb(String str)328   public static long byteStringAsGb(String str) {
329     return byteStringAs(str, ByteUnit.GiB);
330   }
331 
332   /**
333    * Returns a byte array with the buffer's contents, trying to avoid copying the data if
334    * possible.
335    */
bufferToArray(ByteBuffer buffer)336   public static byte[] bufferToArray(ByteBuffer buffer) {
337     if (buffer.hasArray() && buffer.arrayOffset() == 0 &&
338         buffer.array().length == buffer.remaining()) {
339       return buffer.array();
340     } else {
341       byte[] bytes = new byte[buffer.remaining()];
342       buffer.get(bytes);
343       return bytes;
344     }
345   }
346 
347 }
348