You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flume.apache.org by mp...@apache.org on 2016/07/13 19:25:48 UTC

flume git commit: FLUME-2725. HDFS Sink does not use configured timezone for rounding

Repository: flume
Updated Branches:
  refs/heads/trunk ba64b1267 -> ec28b6624


FLUME-2725. HDFS Sink does not use configured timezone for rounding

(Denes Arvay via Mike Percy)


Project: http://git-wip-us.apache.org/repos/asf/flume/repo
Commit: http://git-wip-us.apache.org/repos/asf/flume/commit/ec28b662
Tree: http://git-wip-us.apache.org/repos/asf/flume/tree/ec28b662
Diff: http://git-wip-us.apache.org/repos/asf/flume/diff/ec28b662

Branch: refs/heads/trunk
Commit: ec28b66246f1f165ccaf01abf7fb27adebc9e4bb
Parents: ba64b12
Author: Denes Arvay <de...@cloudera.com>
Authored: Wed Jul 13 12:07:24 2016 -0700
Committer: Mike Percy <mp...@apache.org>
Committed: Wed Jul 13 12:09:46 2016 -0700

----------------------------------------------------------------------
 .../flume/formatter/output/BucketPath.java      |  10 +-
 .../flume/tools/TimestampRoundDownUtil.java     |  72 ++++++++++-
 .../flume/formatter/output/TestBucketPath.java  | 119 +++++++++++++++---
 .../flume/tools/TestTimestampRoundDownUtil.java | 124 +++++++++++++++----
 4 files changed, 275 insertions(+), 50 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/flume/blob/ec28b662/flume-ng-core/src/main/java/org/apache/flume/formatter/output/BucketPath.java
----------------------------------------------------------------------
diff --git a/flume-ng-core/src/main/java/org/apache/flume/formatter/output/BucketPath.java b/flume-ng-core/src/main/java/org/apache/flume/formatter/output/BucketPath.java
index b2fe3f0..167b542 100644
--- a/flume-ng-core/src/main/java/org/apache/flume/formatter/output/BucketPath.java
+++ b/flume-ng-core/src/main/java/org/apache/flume/formatter/output/BucketPath.java
@@ -236,7 +236,7 @@ public class BucketPath {
     }
 
     if (needRounding) {
-      ts = roundDown(roundDown, unit, ts);
+      ts = roundDown(roundDown, unit, ts, timeZone);
     }
 
     // It's a date
@@ -329,7 +329,7 @@ public class BucketPath {
     return format.format(date);
   }
 
-  private static long roundDown(int roundDown, int unit, long ts) {
+  private static long roundDown(int roundDown, int unit, long ts, TimeZone timeZone) {
     long timestamp = ts;
     if (roundDown <= 0) {
       roundDown = 1;
@@ -337,15 +337,15 @@ public class BucketPath {
     switch (unit) {
       case Calendar.SECOND:
         timestamp = TimestampRoundDownUtil.roundDownTimeStampSeconds(
-            ts, roundDown);
+            ts, roundDown, timeZone);
         break;
       case Calendar.MINUTE:
         timestamp = TimestampRoundDownUtil.roundDownTimeStampMinutes(
-            ts, roundDown);
+            ts, roundDown, timeZone);
         break;
       case Calendar.HOUR_OF_DAY:
         timestamp = TimestampRoundDownUtil.roundDownTimeStampHours(
-            ts, roundDown);
+            ts, roundDown, timeZone);
         break;
       default:
         timestamp = ts;

http://git-wip-us.apache.org/repos/asf/flume/blob/ec28b662/flume-ng-core/src/main/java/org/apache/flume/tools/TimestampRoundDownUtil.java
----------------------------------------------------------------------
diff --git a/flume-ng-core/src/main/java/org/apache/flume/tools/TimestampRoundDownUtil.java b/flume-ng-core/src/main/java/org/apache/flume/tools/TimestampRoundDownUtil.java
index daa9606..6ce0fb9 100644
--- a/flume-ng-core/src/main/java/org/apache/flume/tools/TimestampRoundDownUtil.java
+++ b/flume-ng-core/src/main/java/org/apache/flume/tools/TimestampRoundDownUtil.java
@@ -19,25 +19,47 @@
 package org.apache.flume.tools;
 
 import java.util.Calendar;
+import java.util.TimeZone;
 
 import com.google.common.base.Preconditions;
+import org.apache.flume.annotations.InterfaceAudience;
+import org.apache.flume.annotations.InterfaceStability;
 
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
 public class TimestampRoundDownUtil {
 
   /**
    *
    * @param timestamp - The time stamp to be rounded down.
+   * For parsing the <tt>timestamp</tt> the system default timezone will be used.
    * @param roundDownSec - The <tt>timestamp</tt> is rounded down to the largest
    * multiple of <tt>roundDownSec</tt> seconds
    * less than or equal to <tt>timestamp.</tt> Should be between 0 and 60.
    * @return - Rounded down timestamp
    * @throws IllegalStateException
+   * @see TimestampRoundDownUtil#roundDownTimeStampSeconds(long, int, TimeZone)
    */
   public static long roundDownTimeStampSeconds(long timestamp,
       int roundDownSec) throws IllegalStateException {
+    return roundDownTimeStampSeconds(timestamp, roundDownSec, null);
+  }
+
+  /**
+   *
+   * @param timestamp - The time stamp to be rounded down.
+   * @param roundDownSec - The <tt>timestamp</tt> is rounded down to the largest
+   * multiple of <tt>roundDownSec</tt> seconds
+   * less than or equal to <tt>timestamp.</tt> Should be between 0 and 60.
+   * @param timeZone - The timezone to use for parsing the <tt>timestamp</tt>.
+   * @return - Rounded down timestamp
+   * @throws IllegalStateException
+   */
+  public static long roundDownTimeStampSeconds(long timestamp, int roundDownSec, TimeZone timeZone)
+      throws IllegalStateException {
     Preconditions.checkArgument(roundDownSec > 0 && roundDownSec <= 60,
         "RoundDownSec must be > 0 and <=60");
-    Calendar cal = roundDownField(timestamp, Calendar.SECOND, roundDownSec);
+    Calendar cal = roundDownField(timestamp, Calendar.SECOND, roundDownSec, timeZone);
     cal.set(Calendar.MILLISECOND, 0);
     return cal.getTimeInMillis();
   }
@@ -45,17 +67,35 @@ public class TimestampRoundDownUtil {
   /**
    *
    * @param timestamp - The time stamp to be rounded down.
+   * For parsing the <tt>timestamp</tt> the system default timezone will be used.
    * @param roundDownMins - The <tt>timestamp</tt> is rounded down to the
    * largest multiple of <tt>roundDownMins</tt> minutes less than
    * or equal to <tt>timestamp.</tt> Should be between 0 and 60.
    * @return - Rounded down timestamp
    * @throws IllegalStateException
+   * @see TimestampRoundDownUtil#roundDownTimeStampMinutes(long, int, TimeZone)
    */
   public static long roundDownTimeStampMinutes(long timestamp,
       int roundDownMins) throws IllegalStateException {
+    return roundDownTimeStampMinutes(timestamp, roundDownMins, null);
+  }
+
+  /**
+   *
+   * @param timestamp - The time stamp to be rounded down.
+   * @param roundDownMins - The <tt>timestamp</tt> is rounded down to the
+   * largest multiple of <tt>roundDownMins</tt> minutes less than
+   * or equal to <tt>timestamp.</tt> Should be between 0 and 60.
+   * @param timeZone - The timezone to use for parsing the <tt>timestamp</tt>.
+   * If <tt>null</tt> the system default will be used.
+   * @return - Rounded down timestamp
+   * @throws IllegalStateException
+   */
+  public static long roundDownTimeStampMinutes(long timestamp, int roundDownMins, TimeZone timeZone)
+      throws IllegalStateException {
     Preconditions.checkArgument(roundDownMins > 0 && roundDownMins <= 60,
         "RoundDown must be > 0 and <=60");
-    Calendar cal = roundDownField(timestamp, Calendar.MINUTE, roundDownMins);
+    Calendar cal = roundDownField(timestamp, Calendar.MINUTE, roundDownMins, timeZone);
     cal.set(Calendar.SECOND, 0);
     cal.set(Calendar.MILLISECOND, 0);
     return cal.getTimeInMillis();
@@ -65,28 +105,48 @@ public class TimestampRoundDownUtil {
   /**
    *
    * @param timestamp - The time stamp to be rounded down.
+   * For parsing the <tt>timestamp</tt> the system default timezone will be used.
    * @param roundDownHours - The <tt>timestamp</tt> is rounded down to the
    * largest multiple of <tt>roundDownHours</tt> hours less than
    * or equal to <tt>timestamp.</tt> Should be between 0 and 24.
    * @return - Rounded down timestamp
    * @throws IllegalStateException
+   * @see TimestampRoundDownUtil#roundDownTimeStampHours(long, int, TimeZone)
    */
   public static long roundDownTimeStampHours(long timestamp,
       int roundDownHours) throws IllegalStateException {
+    return roundDownTimeStampHours(timestamp, roundDownHours, null);
+  }
+
+  /**
+   *
+   * @param timestamp - The time stamp to be rounded down.
+   * @param roundDownHours - The <tt>timestamp</tt> is rounded down to the
+   * largest multiple of <tt>roundDownHours</tt> hours less than
+   * or equal to <tt>timestamp.</tt> Should be between 0 and 24.
+   * @param timeZone - The timezone to use for parsing the <tt>timestamp</tt>.
+   *                 If <tt>null</tt> the system default will be used.
+   * @return - Rounded down timestamp
+   * @throws IllegalStateException
+   */
+  public static long roundDownTimeStampHours(long timestamp, int roundDownHours, TimeZone timeZone)
+      throws IllegalStateException {
     Preconditions.checkArgument(roundDownHours > 0 && roundDownHours <= 24,
         "RoundDown must be > 0 and <=24");
-    Calendar cal = roundDownField(timestamp,
-        Calendar.HOUR_OF_DAY, roundDownHours);
+    Calendar cal = roundDownField(timestamp, Calendar.HOUR_OF_DAY, roundDownHours, timeZone);
     cal.set(Calendar.MINUTE, 0);
     cal.set(Calendar.SECOND, 0);
     cal.set(Calendar.MILLISECOND, 0);
     return cal.getTimeInMillis();
   }
 
-  private static Calendar roundDownField(long timestamp, int field, int roundDown) {
+  private static Calendar roundDownField(long timestamp, int field, int roundDown,
+                                         TimeZone timeZone) {
     Preconditions.checkArgument(timestamp > 0, "Timestamp must be positive");
-    Calendar cal = Calendar.getInstance();
+
+    Calendar cal = (timeZone == null) ? Calendar.getInstance() : Calendar.getInstance(timeZone);
     cal.setTimeInMillis(timestamp);
+
     int fieldVal = cal.get(field);
     int remainder =  (fieldVal % roundDown);
     cal.set(field, fieldVal - remainder);

http://git-wip-us.apache.org/repos/asf/flume/blob/ec28b662/flume-ng-core/src/test/java/org/apache/flume/formatter/output/TestBucketPath.java
----------------------------------------------------------------------
diff --git a/flume-ng-core/src/test/java/org/apache/flume/formatter/output/TestBucketPath.java b/flume-ng-core/src/test/java/org/apache/flume/formatter/output/TestBucketPath.java
index b1b828a..ccc7460 100644
--- a/flume-ng-core/src/test/java/org/apache/flume/formatter/output/TestBucketPath.java
+++ b/flume-ng-core/src/test/java/org/apache/flume/formatter/output/TestBucketPath.java
@@ -25,29 +25,40 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
+import javax.annotation.Nullable;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.SimpleTimeZone;
 import java.util.TimeZone;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 public class TestBucketPath {
-  Calendar cal;
-  Map<String, String> headers;
+  private static final TimeZone CUSTOM_TIMEZONE = new SimpleTimeZone(1, "custom-timezone");
+
+  private Calendar cal;
+  private Map<String, String> headers;
+  private Map<String, String> headersWithTimeZone;
 
   @Before
   public void setUp() {
-    cal = Calendar.getInstance();
-    cal.set(2012, 5, 23, 13, 46, 33);
-    cal.set(Calendar.MILLISECOND, 234);
-    headers = new HashMap<String, String>();
+    cal = createCalendar(2012, 5, 23, 13, 46, 33, 234, null);
+    headers = new HashMap<>();
     headers.put("timestamp", String.valueOf(cal.getTimeInMillis()));
+
+    Calendar calWithTimeZone = createCalendar(2012, 5, 23, 13, 46, 33, 234, CUSTOM_TIMEZONE);
+    headersWithTimeZone = new HashMap<>();
+    headersWithTimeZone.put("timestamp", String.valueOf(calWithTimeZone.getTimeInMillis()));
   }
 
+  /**
+   * Tests if the internally cached SimpleDateFormat instances can be reused with different
+   * TimeZone without interference.
+   */
   @Test
   public void testDateFormatCache() {
     TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
@@ -64,15 +75,19 @@ public class TestBucketPath {
     Assert.assertEquals(expectedString, escapedString);
   }
 
+  /**
+   * Tests if the timestamp with the default timezone is properly rounded down
+   * to 12 hours using "%c" ("EEE MMM d HH:mm:ss yyyy") formatting.
+   */
   @Test
   public void testDateFormatHours() {
     String test = "%c";
     String escapedString = BucketPath.escapeString(
         test, headers, true, Calendar.HOUR_OF_DAY, 12);
     System.out.println("Escaped String: " + escapedString);
-    Calendar cal2 = Calendar.getInstance();
-    cal2.set(2012, 5, 23, 12, 0, 0);
-    cal2.set(Calendar.MILLISECOND, 0);
+
+    Calendar cal2 = createCalendar(2012, 5, 23, 12, 0, 0, 0, null);
+
     SimpleDateFormat format = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy");
     Date d = new Date(cal2.getTimeInMillis());
     String expectedString = format.format(d);
@@ -80,34 +95,99 @@ public class TestBucketPath {
     Assert.assertEquals(expectedString, escapedString);
   }
 
+  /**
+   * Tests if the timestamp with the custom timezone is properly rounded down
+   * to 12 hours using "%c" ("EEE MMM d HH:mm:ss yyyy") formatting.
+   */
+  @Test
+  public void testDateFormatHoursTimeZone() {
+    String test = "%c";
+    String escapedString = BucketPath.escapeString(
+        test, headersWithTimeZone, CUSTOM_TIMEZONE, true, Calendar.HOUR_OF_DAY, 12, false);
+    System.out.println("Escaped String: " + escapedString);
+
+    Calendar cal2 = createCalendar(2012, 5, 23, 12, 0, 0, 0, CUSTOM_TIMEZONE);
+
+    SimpleDateFormat format = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy");
+    format.setTimeZone(CUSTOM_TIMEZONE);
+
+    Date d = new Date(cal2.getTimeInMillis());
+    String expectedString = format.format(d);
+    System.out.println("Expected String: " + expectedString);
+    Assert.assertEquals(expectedString, escapedString);
+  }
+
+  /**
+   * Tests if the timestamp with the default timezone is properly rounded down
+   * to 5 minutes using "%s" (seconds) formatting
+   */
   @Test
   public void testDateFormatMinutes() {
     String test = "%s";
     String escapedString = BucketPath.escapeString(
         test, headers, true, Calendar.MINUTE, 5);
     System.out.println("Escaped String: " + escapedString);
-    Calendar cal2 = Calendar.getInstance();
-    cal2.set(2012, 5, 23, 13, 45, 0);
-    cal2.set(Calendar.MILLISECOND, 0);
+
+    Calendar cal2 = createCalendar(2012, 5, 23, 13, 45, 0, 0, null);
+    String expectedString = String.valueOf(cal2.getTimeInMillis() / 1000);
+    System.out.println("Expected String: " + expectedString);
+    Assert.assertEquals(expectedString, escapedString);
+  }
+
+  /**
+   * Tests if the timestamp with the custom timezone is properly rounded down
+   * to 5 minutes using "%s" (seconds) formatting
+   */
+  @Test
+  public void testDateFormatMinutesTimeZone() {
+    String test = "%s";
+    String escapedString = BucketPath.escapeString(
+        test, headersWithTimeZone, CUSTOM_TIMEZONE, true, Calendar.MINUTE, 5, false);
+    System.out.println("Escaped String: " + escapedString);
+
+    Calendar cal2 = createCalendar(2012, 5, 23, 13, 45, 0, 0, CUSTOM_TIMEZONE);
     String expectedString = String.valueOf(cal2.getTimeInMillis() / 1000);
     System.out.println("Expected String: " + expectedString);
     Assert.assertEquals(expectedString, escapedString);
   }
 
+  /**
+   * Tests if the timestamp with the default timezone is properly rounded down
+   * to 5 seconds using "%s" (seconds) formatting
+   */
   @Test
   public void testDateFormatSeconds() {
     String test = "%s";
     String escapedString = BucketPath.escapeString(
         test, headers, true, Calendar.SECOND, 5);
     System.out.println("Escaped String: " + escapedString);
-    Calendar cal2 = Calendar.getInstance();
-    cal2.set(2012, 5, 23, 13, 46, 30);
-    cal2.set(Calendar.MILLISECOND, 0);
+
+    Calendar cal2 = createCalendar(2012, 5, 23, 13, 46, 30, 0, null);
     String expectedString = String.valueOf(cal2.getTimeInMillis() / 1000);
     System.out.println("Expected String: " + expectedString);
     Assert.assertEquals(expectedString, escapedString);
   }
 
+  /**
+   * Tests if the timestamp with the custom timezone is properly rounded down
+   * to 5 seconds using "%s" (seconds) formatting
+   */
+  @Test
+  public void testDateFormatSecondsTimeZone() {
+    String test = "%s";
+    String escapedString = BucketPath.escapeString(
+        test, headersWithTimeZone, CUSTOM_TIMEZONE, true, Calendar.SECOND, 5, false);
+    System.out.println("Escaped String: " + escapedString);
+
+    Calendar cal2 = createCalendar(2012, 5, 23, 13, 46, 30, 0, CUSTOM_TIMEZONE);
+    String expectedString = String.valueOf(cal2.getTimeInMillis() / 1000);
+    System.out.println("Expected String: " + expectedString);
+    Assert.assertEquals(expectedString, escapedString);
+  }
+
+  /**
+   * Tests if the timestamp is properly formatted without rounding it down.
+   */
   @Test
   public void testNoRounding() {
     String test = "%c";
@@ -189,4 +269,13 @@ public class TestBucketPath {
 
     Assert.assertEquals("Race condition detected", "02:50", escaped);
   }
+
+  private static Calendar createCalendar(int year, int month, int day,
+                                         int hour, int minute, int second, int ms,
+                                         @Nullable TimeZone timeZone) {
+    Calendar cal = (timeZone == null) ? Calendar.getInstance() : Calendar.getInstance(timeZone);
+    cal.set(year, month, day, hour, minute, second);
+    cal.set(Calendar.MILLISECOND, ms);
+    return cal;
+  }
 }

http://git-wip-us.apache.org/repos/asf/flume/blob/ec28b662/flume-ng-core/src/test/java/org/apache/flume/tools/TestTimestampRoundDownUtil.java
----------------------------------------------------------------------
diff --git a/flume-ng-core/src/test/java/org/apache/flume/tools/TestTimestampRoundDownUtil.java b/flume-ng-core/src/test/java/org/apache/flume/tools/TestTimestampRoundDownUtil.java
index 1ac11ab..384a9dc 100644
--- a/flume-ng-core/src/test/java/org/apache/flume/tools/TestTimestampRoundDownUtil.java
+++ b/flume-ng-core/src/test/java/org/apache/flume/tools/TestTimestampRoundDownUtil.java
@@ -19,23 +19,36 @@
 package org.apache.flume.tools;
 
 import java.util.Calendar;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
 
 import junit.framework.Assert;
 
 import org.junit.Test;
 
+import javax.annotation.Nullable;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertThat;
+
 public class TestTimestampRoundDownUtil {
 
+  private static final TimeZone CUSTOM_TIMEZONE = new SimpleTimeZone(1, "custom-timezone");
+  private static final Calendar BASE_CALENDAR_WITH_DEFAULT_TIMEZONE =
+      createCalendar(2012, 5, 15, 15, 12, 54, 0, null);
+  private static final Calendar BASE_CALENDAR_WITH_CUSTOM_TIMEZONE =
+      createCalendar(2012, 5, 15, 15, 12, 54, 0, CUSTOM_TIMEZONE);
+
+  /**
+   * Tests if the timestamp with the default timezone is properly rounded down
+   * to 60 seconds.
+   */
   @Test
   public void testRoundDownTimeStampSeconds() {
-    Calendar cal = Calendar.getInstance();
-    cal.clear();
-    cal.set(2012, 5, 15, 15, 12, 54);
-    cal.set(Calendar.MILLISECOND, 20);
-    Calendar cal2 = Calendar.getInstance();
-    cal2.clear();
-    cal2.set(2012, 5, 15, 15, 12, 0);
-    cal2.set(Calendar.MILLISECOND, 0);
+    Calendar cal = BASE_CALENDAR_WITH_DEFAULT_TIMEZONE;
+    Calendar cal2 = createCalendar(2012, 5, 15, 15, 12, 0, 0, null);
+
     long timeToVerify = cal2.getTimeInMillis();
     long ret = TimestampRoundDownUtil.roundDownTimeStampSeconds(cal.getTimeInMillis(), 60);
     System.out.println("Cal 1: " + cal.toString());
@@ -43,16 +56,34 @@ public class TestTimestampRoundDownUtil {
     Assert.assertEquals(timeToVerify, ret);
   }
 
+  /**
+   * Tests if the timestamp with the custom timezone is properly rounded down
+   * to 60 seconds.
+   */
+  @Test
+  public void testRoundDownTimeStampSecondsWithTimeZone() {
+    Calendar cal = BASE_CALENDAR_WITH_CUSTOM_TIMEZONE;
+    Calendar cal2 = createCalendar(2012, 5, 15, 15, 12, 0, 0, CUSTOM_TIMEZONE);
+
+    long timeToVerify = cal2.getTimeInMillis();
+    long withoutTimeZone = TimestampRoundDownUtil.roundDownTimeStampSeconds(
+        cal.getTimeInMillis(), 60);
+    long withTimeZone = TimestampRoundDownUtil.roundDownTimeStampSeconds(
+        cal.getTimeInMillis(), 60, CUSTOM_TIMEZONE);
+
+    assertThat(withoutTimeZone, not(equalTo(timeToVerify)));
+    Assert.assertEquals(withTimeZone, timeToVerify);
+  }
+
+  /**
+   * Tests if the timestamp with the default timezone is properly rounded down
+   * to 5 minutes.
+   */
   @Test
   public void testRoundDownTimeStampMinutes() {
-    Calendar cal = Calendar.getInstance();
-    cal.clear();
-    cal.set(2012, 5, 15, 15, 12, 54);
-    cal.set(Calendar.MILLISECOND, 20);
-    Calendar cal2 = Calendar.getInstance();
-    cal2.clear();
-    cal2.set(2012, 5, 15, 15, 10, 0);
-    cal2.set(Calendar.MILLISECOND, 0);
+    Calendar cal = BASE_CALENDAR_WITH_DEFAULT_TIMEZONE;
+    Calendar cal2 = createCalendar(2012, 5, 15, 15, 10, 0, 0, null);
+
     long timeToVerify = cal2.getTimeInMillis();
     long ret = TimestampRoundDownUtil.roundDownTimeStampMinutes(cal.getTimeInMillis(), 5);
     System.out.println("Cal 1: " + cal.toString());
@@ -60,16 +91,34 @@ public class TestTimestampRoundDownUtil {
     Assert.assertEquals(timeToVerify, ret);
   }
 
+  /**
+   * Tests if the timestamp with the custom timezone is properly rounded down
+   * to 5 minutes.
+   */
+  @Test
+  public void testRoundDownTimeStampMinutesWithTimeZone() {
+    Calendar cal = BASE_CALENDAR_WITH_CUSTOM_TIMEZONE;
+    Calendar cal2 = createCalendar(2012, 5, 15, 15, 10, 0, 0, CUSTOM_TIMEZONE);
+
+    long timeToVerify = cal2.getTimeInMillis();
+    long withoutTimeZone = TimestampRoundDownUtil.roundDownTimeStampMinutes(
+        cal.getTimeInMillis(), 5);
+    long withTimeZone = TimestampRoundDownUtil.roundDownTimeStampMinutes(
+        cal.getTimeInMillis(), 5, CUSTOM_TIMEZONE);
+
+    assertThat(withoutTimeZone, not(equalTo(timeToVerify)));
+    Assert.assertEquals(withTimeZone, timeToVerify);
+  }
+
+  /**
+   * Tests if the timestamp with the default timezone is properly rounded down
+   * to 2 hours.
+   */
   @Test
   public void testRoundDownTimeStampHours() {
-    Calendar cal = Calendar.getInstance();
-    cal.clear();
-    cal.set(2012, 5, 15, 15, 12, 54);
-    cal.set(Calendar.MILLISECOND, 20);
-    Calendar cal2 = Calendar.getInstance();
-    cal2.clear();
-    cal2.set(2012, 5, 15, 14, 0, 0);
-    cal2.set(Calendar.MILLISECOND, 0);
+    Calendar cal = BASE_CALENDAR_WITH_DEFAULT_TIMEZONE;
+    Calendar cal2 = createCalendar(2012, 5, 15, 14, 0, 0, 0, null);
+
     long timeToVerify = cal2.getTimeInMillis();
     long ret = TimestampRoundDownUtil.roundDownTimeStampHours(cal.getTimeInMillis(), 2);
     System.out.println("Cal 1: " + ret);
@@ -77,4 +126,31 @@ public class TestTimestampRoundDownUtil {
     Assert.assertEquals(timeToVerify, ret);
   }
 
+  /**
+   * Tests if the timestamp with the custom timezone is properly rounded down
+   * to 2 hours.
+   */
+  @Test
+  public void testRoundDownTimeStampHoursWithTimeZone() {
+    Calendar cal = BASE_CALENDAR_WITH_CUSTOM_TIMEZONE;
+    Calendar cal2 = createCalendar(2012, 5, 15, 14, 0, 0, 0, CUSTOM_TIMEZONE);
+
+    long timeToVerify = cal2.getTimeInMillis();
+    long withoutTimeZone = TimestampRoundDownUtil.roundDownTimeStampHours(
+        cal.getTimeInMillis(), 2);
+    long withTimeZone = TimestampRoundDownUtil.roundDownTimeStampHours(
+        cal.getTimeInMillis(), 2, CUSTOM_TIMEZONE);
+
+    assertThat(withoutTimeZone, not(equalTo(timeToVerify)));
+    Assert.assertEquals(withTimeZone, timeToVerify);
+  }
+
+  private static Calendar createCalendar(int year, int month, int day,
+                                         int hour, int minute, int second, int ms,
+                                         @Nullable TimeZone timeZone) {
+    Calendar cal = (timeZone == null) ? Calendar.getInstance() : Calendar.getInstance(timeZone);
+    cal.set(year, month, day, hour, minute, second);
+    cal.set(Calendar.MILLISECOND, ms);
+    return cal;
+  }
 }