/*******************************************************************************
* Copyright 2011 Netflix
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
******************************************************************************/
package com.netflix.dyno.recipes.counter;
import com.netflix.dyno.connectionpool.TokenPoolTopology;
import com.netflix.dyno.connectionpool.TopologyView;
import com.netflix.dyno.jedis.DynoJedisClient;
import org.slf4j.LoggerFactory;
import javax.annotation.concurrent.ThreadSafe;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Synchronous implementation of a {@link DynoCounter}. This class is the base
* class for other implementations as it contains the logic to shard the counter
* key.
* <p>
* All DynoJedis*Counter implementations are predicated upon Dynomite's features in conjunction with
* Redis's atomic increment functionality.
* </p>
*
* @see {@INCR http://redis.io/commands/INCR}
*
* @author jcacciatore
*/
@ThreadSafe
public class DynoJedisCounter implements DynoCounter {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(DynoJedisCounter.class);
private static final int MAX_ITERATIONS = 1000;
protected final String key;
protected final DynoJedisClient client;
protected final List<String> generatedKeys;
public DynoJedisCounter(String key, DynoJedisClient client) {
this.key = key;
this.client = client;
this.generatedKeys = generateKeys();
}
@Override
public void initialize() {
// set Lifecycle state
}
public void incr() {
client.incr(generatedKeys.get(randomIntFrom0toN()));
}
public void incrBy(long value) {
client.incrBy(generatedKeys.get(randomIntFrom0toN()), value);
}
public Long get() {
Long result = 0L;
ArrayList<String> values = new ArrayList<String>(generatedKeys.size());
for (String key: generatedKeys) {
String val = client.get(key);
if (val != null) {
result += Long.valueOf(val);
values.add(val);
}
}
logger.debug("result=>" + result + ", key: " + key + ", values: " + values.toString());
return result;
}
public String getKey() {
return key;
}
public List<String> getGeneratedKeys() {
return Collections.unmodifiableList(generatedKeys);
}
List<String> generateKeys() {
final TopologyView view = client.getTopologyView();
final Map<String, List<TokenPoolTopology.TokenStatus>> topology = view.getTopologySnapshot();
if (topology.keySet().isEmpty()) {
throw new RuntimeException("Unable to determine dynomite topology");
}
// Retrieve the tokens for the cluster
final List<String> racks = new ArrayList<String>(topology.keySet());
final Set<Long> tokens = new HashSet<Long>();
for (TokenPoolTopology.TokenStatus status : topology.get(racks.get(0))) {
tokens.add(status.getToken());
}
final List<String> generatedKeys = new ArrayList<String>(tokens.size());
// Find a key corresponding to each token
int i = 0;
while (tokens.size() > 0 && i++ < MAX_ITERATIONS) {
Long token = view.getTokenForKey(key + "_" + i);
if (tokens.contains(token)) {
if (tokens.remove(token)) {
String generated = key + "_" + i;
logger.debug(String.format("Found key=>%s for token=>%s", generated, token));
generatedKeys.add(generated);
}
}
}
return generatedKeys;
}
int randomIntFrom0toN() {
// XORShift instead of Math.random http://javamex.com/tutorials/random_numbers/xorshift.shtml
long x = System.nanoTime();
x ^= (x << 21);
x ^= (x >>> 35);
x ^= (x << 4);
return Math.abs((int) x % generatedKeys.size());
}
@Override
public void close() throws Exception {
// nothing to do for this implementation
}
}