diff --git a/core/src/main/java/com/yahoo/ycsb/generator/RandomDiscreteTimestampGenerator.java b/core/src/main/java/com/yahoo/ycsb/generator/RandomDiscreteTimestampGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..bb8bb07fe963e4e4af4e09f0d8b7f8e418e0584f --- /dev/null +++ b/core/src/main/java/com/yahoo/ycsb/generator/RandomDiscreteTimestampGenerator.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2017 YCSB contributors. All rights reserved. + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ +package com.yahoo.ycsb.generator; + +import java.util.concurrent.TimeUnit; + +import com.yahoo.ycsb.Utils; + +/** + * A generator that picks from a discrete set of offsets from a base Unix Epoch + * timestamp that returns timestamps in a random order with the guarantee that + * each timestamp is only returned once. + * <p> + * TODO - It would be best to implement some kind of psuedo non-repeating random + * generator for this as it's likely OK that some small percentage of values are + * repeated. For now we just generate all of the offsets in an array, shuffle + * it and then iterate over the array. + * <p> + * Note that {@link #MAX_INTERVALS} defines a hard limit on the size of the + * offset array so that we don't completely blow out the heap. + * <p> + * The constructor parameter {@code intervals} determines how many values will be + * returned by the generator. For example, if the {@code interval} is 60 and the + * {@code timeUnits} are set to {@link TimeUnit#SECONDS} and {@code intervals} + * is set to 60, then the consumer can call {@link #nextValue()} 60 times for + * timestamps within an hour. + */ +public class RandomDiscreteTimestampGenerator extends UnixEpochTimestampGenerator { + + /** A hard limit on the size of the offsets array to a void using too much heap. */ + public static final int MAX_INTERVALS = 16777216; + + /** The total number of intervals for this generator. */ + private final int intervals; + + // can't be primitives due to the generic params on the sort function :( + /** The array of generated offsets from the base time. */ + private final Integer[] offsets; + + /** The current index into the offsets array. */ + private int offsetIndex; + + /** + * Ctor that uses the current system time as current. + * @param interval The interval between timestamps. + * @param timeUnits The time units of the returned Unix Epoch timestamp (as well + * as the units for the interval). + * @param intervals The total number of intervals for the generator. + * @throws IllegalArgumentException if the intervals is larger than {@link #MAX_INTERVALS} + */ + public RandomDiscreteTimestampGenerator(final long interval, final TimeUnit timeUnits, + final int intervals) { + super(interval, timeUnits); + this.intervals = intervals; + offsets = new Integer[intervals]; + setup(); + } + + /** + * Ctor for supplying a starting timestamp. + * The interval between timestamps. + * @param timeUnits The time units of the returned Unix Epoch timestamp (as well + * as the units for the interval). + * @param startTimestamp The start timestamp to use. + * NOTE that this must match the time units used for the interval. + * If the units are in nanoseconds, provide a nanosecond timestamp {@code System.nanoTime()} + * or in microseconds, {@code System.nanoTime() / 1000} + * or in millis, {@code System.currentTimeMillis()} + * @param intervals The total number of intervals for the generator. + * @throws IllegalArgumentException if the intervals is larger than {@link #MAX_INTERVALS} + */ + public RandomDiscreteTimestampGenerator(final long interval, final TimeUnit timeUnits, + final long startTimestamp, final int intervals) { + super(interval, timeUnits, startTimestamp); + this.intervals = intervals; + offsets = new Integer[intervals]; + setup(); + } + + /** + * Generates the offsets and shuffles the array. + */ + private void setup() { + if (intervals > MAX_INTERVALS) { + throw new IllegalArgumentException("Too many intervals for the in-memory " + + "array. The limit is " + MAX_INTERVALS + "."); + } + offsetIndex = 0; + for (int i = 0; i < intervals; i++) { + offsets[i] = i; + } + Utils.shuffleArray(offsets); + } + + @Override + public Long nextValue() { + if (offsetIndex >= offsets.length) { + throw new IllegalStateException("Reached the end of the random timestamp " + + "intervals: " + offsetIndex); + } + lastTimestamp = currentTimestamp; + currentTimestamp = startTimestamp + (offsets[offsetIndex++] * getOffset(1)); + return currentTimestamp; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/yahoo/ycsb/generator/UnixEpochTimestampGenerator.java b/core/src/main/java/com/yahoo/ycsb/generator/UnixEpochTimestampGenerator.java index ce3c9e3d142b623a5dc9591cb26d5761a090ebe0..cb2e83f3599bccb9f2a725fec9b495b1438a50dd 100644 --- a/core/src/main/java/com/yahoo/ycsb/generator/UnixEpochTimestampGenerator.java +++ b/core/src/main/java/com/yahoo/ycsb/generator/UnixEpochTimestampGenerator.java @@ -43,17 +43,20 @@ import java.util.concurrent.TimeUnit; */ public class UnixEpochTimestampGenerator extends Generator<Long> { + /** The base timestamp used as a starting reference. */ + protected long startTimestamp; + /** The current timestamp that will be incremented. */ - private long currentTimestamp; + protected long currentTimestamp; /** The last used timestamp. Should always be one interval behind current. */ - private long lastTimestamp; + protected long lastTimestamp; /** The interval to increment by. Multiplied by {@link #timeUnits}. */ - private long interval; + protected long interval; /** The units of time the interval represents. */ - private TimeUnit timeUnits; + protected TimeUnit timeUnits; /** * Default ctor with the current system time and a 60 second interval. @@ -94,7 +97,8 @@ public class UnixEpochTimestampGenerator extends Generator<Long> { this.timeUnits = timeUnits; // move the first timestamp by 1 interval so that the first call to nextValue // returns this timestamp - this.currentTimestamp = startTimestamp - getOffset(1); + currentTimestamp = startTimestamp - getOffset(1); + this.startTimestamp = currentTimestamp; lastTimestamp = currentTimestamp - getOffset(1); } @@ -133,6 +137,7 @@ public class UnixEpochTimestampGenerator extends Generator<Long> { default: throw new IllegalArgumentException("Unhandled time unit type: " + timeUnits); } + startTimestamp = currentTimestamp; } @Override @@ -175,4 +180,4 @@ public class UnixEpochTimestampGenerator extends Generator<Long> { return currentTimestamp; } -} +} \ No newline at end of file diff --git a/core/src/test/java/com/yahoo/ycsb/generator/TestRandomDiscreteTimestampGenerator.java b/core/src/test/java/com/yahoo/ycsb/generator/TestRandomDiscreteTimestampGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..075005c693db2313b3920c781c977b77e3489712 --- /dev/null +++ b/core/src/test/java/com/yahoo/ycsb/generator/TestRandomDiscreteTimestampGenerator.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2017 YCSB contributors. All rights reserved. + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ +package com.yahoo.ycsb.generator; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.testng.annotations.Test; +import org.testng.collections.Lists; + +public class TestRandomDiscreteTimestampGenerator { + + @Test + public void systemTime() throws Exception { + final RandomDiscreteTimestampGenerator generator = + new RandomDiscreteTimestampGenerator(60, TimeUnit.SECONDS, 60); + List<Long> generated = Lists.newArrayList(); + for (int i = 0; i < 60; i++) { + generated.add(generator.nextValue()); + } + assertEquals(generated.size(), 60); + try { + generator.nextValue(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { } + } + + @Test + public void withStartTime() throws Exception { + final RandomDiscreteTimestampGenerator generator = + new RandomDiscreteTimestampGenerator(60, TimeUnit.SECONDS, 1072915200L, 60); + List<Long> generated = Lists.newArrayList(); + for (int i = 0; i < 60; i++) { + generated.add(generator.nextValue()); + } + assertEquals(generated.size(), 60); + Collections.sort(generated); + long ts = 1072915200L - 60; // starts 1 interval in the past + for (final long t : generated) { + assertEquals(t, ts); + ts += 60; + } + try { + generator.nextValue(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { } + } + + @Test (expectedExceptions = IllegalArgumentException.class) + public void tooLarge() throws Exception { + new RandomDiscreteTimestampGenerator(60, TimeUnit.SECONDS, + RandomDiscreteTimestampGenerator.MAX_INTERVALS + 1); + } + + //TODO - With PowerMockito we could UT the initializeTimestamp(long) call. + // Otherwise it would involve creating more functions and that would get ugly. +}