/*
* Copyright 2015 JBoss Inc
*
* 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 io.apiman.gateway.engine.es;
import io.apiman.gateway.engine.async.AsyncResultImpl;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.components.IRateLimiterComponent;
import io.apiman.gateway.engine.components.rate.RateLimitResponse;
import io.apiman.gateway.engine.rates.RateBucketPeriod;
import io.apiman.gateway.engine.rates.RateLimiterBucket;
import io.searchbox.client.JestResult;
import io.searchbox.core.Get;
import io.searchbox.core.Index;
import io.searchbox.params.Parameters;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
/**
* An elasticsearch implementation of the rate limiter component.
*
* @author eric.wittmann@redhat.com
*/
public class ESRateLimiterComponent extends AbstractESComponent implements IRateLimiterComponent {
/**
* Constructor.
* @param config the configuration
*/
public ESRateLimiterComponent(Map<String, String> config) {
super(config);
}
/**
* @see io.apiman.gateway.engine.components.IRateLimiterComponent#accept(java.lang.String, io.apiman.gateway.engine.rates.RateBucketPeriod, long, long, io.apiman.gateway.engine.async.IAsyncResultHandler)
*/
@Override
public void accept(final String bucketId, final RateBucketPeriod period, final long limit,
final long increment, final IAsyncResultHandler<RateLimitResponse> handler) {
final String id = id(bucketId);
try {
Get get = new Get.Builder(getIndexName(), id).type("rateBucket").build(); //$NON-NLS-1$
JestResult result = getClient().execute(get);
RateLimiterBucket bucket;
long version;
if (result.isSucceeded()) {
// use the existing bucket
version = result.getJsonObject().get("_version").getAsLong(); //$NON-NLS-1$
bucket = result.getSourceAsObject(RateLimiterBucket.class);
} else {
// make a new bucket
version = 0;
bucket = new RateLimiterBucket();
}
bucket.resetIfNecessary(period);
final RateLimitResponse rlr = new RateLimitResponse();
if (bucket.getCount() > limit) {
rlr.setAccepted(false);
} else {
rlr.setAccepted(bucket.getCount() < limit);
bucket.setCount(bucket.getCount() + increment);
bucket.setLast(System.currentTimeMillis());
}
int reset = (int) (bucket.getResetMillis(period) / 1000L);
rlr.setReset(reset);
rlr.setRemaining(limit - bucket.getCount());
updateBucketAndReturn(id, bucket, rlr, version, bucketId, period, limit, increment, handler);
} catch (Throwable e) {
handler.handle(AsyncResultImpl.create(e, RateLimitResponse.class));
}
}
/**
* Update the bucket in ES and then return the rate limit response to the
* original handler. If the update fails because we have a stale version,
* then try the whole thing again (because we conflicted with another
* request).
* @param id
* @param bucket
* @param rlr
* @param version
* @param limit
* @param period
* @param bucketId
* @param increment
* @param handler
*/
protected void updateBucketAndReturn(final String id, final RateLimiterBucket bucket,
final RateLimitResponse rlr, final long version, final String bucketId,
final RateBucketPeriod period, final long limit, final long increment,
final IAsyncResultHandler<RateLimitResponse> handler) {
Index.Builder builder = new Index.Builder(bucket).refresh(false).index(getIndexName());
if (version>0) {
builder.setParameter(Parameters.VERSION, String.valueOf(version));
}
Index index = builder.setParameter(Parameters.OP_TYPE, "index") //$NON-NLS-1$
.type("rateBucket").id(id).build(); //$NON-NLS-1$
try {
getClient().execute(index);
handler.handle(AsyncResultImpl.create(rlr));
} catch (Throwable e) {
// FIXME need to fix this now that we've switched to jest!
// if (ESUtils.rootCause(e) instanceof VersionConflictEngineException) {
// // If we got a version conflict, then it means some other request
// // managed to update the ES document since we retrieved it. Therefore
// // everything we've done is out of date, so we should do it all
// // over again.
// accept(bucketId, period, limit, increment, handler);
// } else {
handler.handle(AsyncResultImpl.<RateLimitResponse>create(e));
// }
}
}
/**
* Base64 encode the bucket ID to make an ES-compatible ID.
* @param bucketId
*/
private String id(String bucketId) {
return Base64.encodeBase64String(bucketId.getBytes());
}
/**
* @see io.apiman.gateway.engine.es.AbstractESComponent#getDefaultIndexName()
*/
@Override
protected String getDefaultIndexName() {
return ESConstants.GATEWAY_INDEX_NAME;
}
}