/********************************************************************************************************************* # Copyright 2019-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # # # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # # with the License. A copy of the License is located at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # # and limitations under the License. # *********************************************************************************************************************/ package com.demo.producer; import java.nio.ByteBuffer; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.Random; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.sql.Timestamp; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.kinesis.producer.Attempt; import com.amazonaws.services.kinesis.producer.KinesisProducerConfiguration; import com.amazonaws.services.kinesis.producer.KinesisProducer; import com.amazonaws.services.kinesis.producer.UserRecordFailedException; import com.amazonaws.services.kinesis.producer.UserRecordResult; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; public class SampleProducer { private static final Logger LOGGER = LoggerFactory.getLogger(SampleProducer.class); // setting default value. can be changed through commandline arguments private static final String STREAM_NAME = "default-data-stream"; // setting default value. can be changed through commandline arguments private static final String REGION = "us-east-1"; private static final Random RANDOM = new Random(); private static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); private static final int RECORDS_PER_SECOND = 100; private static final int SECONDS_TO_RUN_DEFAULT = 5; private static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(1); private static final String DELIMITER = ","; /** * Here'll walk through some of the config options and create an instance of * KinesisProducer, which will be used to put records. * * @param region The region of the Kinesis stream being used. * * @return KinesisProducer instance used to put records. */ private static KinesisProducer getKinesisProducer(final String region) { KinesisProducerConfiguration config = new KinesisProducerConfiguration(); config.setRegion(region); config.setCredentialsProvider(new DefaultAWSCredentialsProviderChain()); config.setMaxConnections(1); config.setRequestTimeout(60000); config.setRecordMaxBufferedTime(2000); config.setAggregationEnabled(false); KinesisProducer producer = new KinesisProducer(config); return producer; } private static String getArgIfPresent(final String[] args, final int index, final String defaultValue) { return args.length > index ? args[index] : defaultValue; } /** The main method. * @param args The command line args for the Sample Producer. It takes 3 optional position parameters: * 1. The stream name to use (default-data-stream is default) * 2. The region name to use (us-east-1 is default) * 3. The duration of the test in seconds, 5 is the default. */ public static void main(String[] args) throws Exception { final String streamName = getArgIfPresent(args, 0, STREAM_NAME); final String region = getArgIfPresent(args, 1, REGION); final String secondsToRunString = getArgIfPresent(args, 2, String.valueOf(SECONDS_TO_RUN_DEFAULT)); final int secondsToRun = Integer.parseInt(secondsToRunString); if (secondsToRun <= 0) { LOGGER.error("Seconds to Run should be a positive integer"); System.exit(1); } final KinesisProducer producer = getKinesisProducer(region); final AtomicLong sequenceNumber = new AtomicLong(0); final AtomicLong completed = new AtomicLong(0); FutureCallback<UserRecordResult> callback = new FutureCallback<UserRecordResult>() { @Override public void onFailure(Throwable t) { // If we see any failures, we will log them. if (t instanceof UserRecordFailedException) { Attempt last = Iterables.getLast(((UserRecordFailedException) t).getResult().getAttempts()); LOGGER.error(String.format("Record failed to put - %s : %s", last.getErrorCode(), last.getErrorMessage())); } LOGGER.error("Exception during put", t); }; @Override public void onSuccess(UserRecordResult result) { completed.getAndIncrement(); }; }; final ExecutorService callbackThreadPool = Executors.newCachedThreadPool(); // The lines within run() are the essence of the KPL API. final Runnable putOneRecord = new Runnable() { @Override public void run() { ByteBuffer data = generateData(); // TIMESTAMP is our partition key ListenableFuture<UserRecordResult> f = producer.addUserRecord(streamName, TIMESTAMP, randomExplicitHashKey(), data); Futures.addCallback(f, callback, callbackThreadPool); } }; EXECUTOR.scheduleAtFixedRate(new Runnable() { @Override public void run() { long put = sequenceNumber.get(); long total = RECORDS_PER_SECOND * secondsToRun; double putPercent = 100.0 * put / total; long done = completed.get(); double donePercent = 100.0 * done / total; LOGGER.info(String.format( "Put %d of %d so far (%.2f %%), %d have completed (%.2f %%)", put, total, putPercent, done, donePercent )); } }, 1, 1, TimeUnit.SECONDS); executeAtTargetRate(EXECUTOR, putOneRecord, sequenceNumber, secondsToRun, RECORDS_PER_SECOND); EXECUTOR.awaitTermination(secondsToRun + 1, TimeUnit.SECONDS); LOGGER.info("Waiting for remaining puts to finish..."); producer.flushSync(); LOGGER.info("All records complete."); producer.destroy(); LOGGER.info("Finished."); } private static String randomExplicitHashKey() { return new BigInteger(128, RANDOM).toString(10); } private static void executeAtTargetRate( final ScheduledExecutorService exec, final Runnable task, final AtomicLong counter, final int durationSeconds, final int ratePerSecond) { exec.scheduleWithFixedDelay(new Runnable() { final long startTime = System.nanoTime(); @Override public void run() { double secondsRun = (System.nanoTime() - startTime) / 1e9; double targetCount = Math.min(durationSeconds, secondsRun) * ratePerSecond; while (counter.get() < targetCount) { counter.getAndIncrement(); try { task.run(); } catch (Exception e) { LOGGER.error("Error running task", e); System.exit(1); } } if (secondsRun >= durationSeconds) { exec.shutdown(); } } }, 0, 1, TimeUnit.MILLISECONDS); } private static int getRandomNumber(int min, int max) { return (int) (Math.random() * ((max - min) + 1)) + min; } public static ByteBuffer generateData() { int zipcode = getRandomNumber(91000, 99999); String productCode = "product".concat(Integer.toString(getRandomNumber(2, 50))); int price = getRandomNumber(1, 100); StringBuffer record = new StringBuffer(); record .append(zipcode).append(DELIMITER) .append(productCode).append(DELIMITER) .append(price).append(DELIMITER) .append(new Timestamp(System.currentTimeMillis()).toString()); // For debugging purposes, the following two lines can be uncommented: // String recordString = record.toString(); // LOGGER.info(recordString); byte[] sendData = null; try { sendData = record.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { LOGGER.error("Error converting string to byte array " + e); } return ByteBuffer.wrap(sendData); } }