/*
* Copyright (c) 2008-2017, Hazelcast, Inc. 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.
* 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.hazelcast.internal.nearcache.impl.invalidation;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IFunction;
import com.hazelcast.core.LifecycleEvent;
import com.hazelcast.core.LifecycleListener;
import com.hazelcast.core.LifecycleService;
import com.hazelcast.nio.serialization.SerializableByConvention;
import com.hazelcast.spi.EventRegistration;
import com.hazelcast.spi.ExecutionService;
import com.hazelcast.spi.NodeEngine;
import com.hazelcast.util.ConstructorFunction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static com.hazelcast.core.LifecycleEvent.LifecycleState.SHUTTING_DOWN;
import static com.hazelcast.util.ConcurrencyUtil.getOrPutIfAbsent;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Sends invalidations to Near Cache in batches.
*/
public class BatchInvalidator extends Invalidator {
private final String invalidationExecutorName;
/**
* Creates an invalidation-queue per data-structure-name.
*/
private final ConstructorFunction<String, InvalidationQueue> invalidationQueueConstructor
= new ConstructorFunction<String, InvalidationQueue>() {
@Override
public InvalidationQueue createNew(String dataStructureName) {
return new InvalidationQueue();
}
};
/**
* data-structure-name to invalidation-queue mappings.
*/
private final ConcurrentMap<String, InvalidationQueue> invalidationQueues
= new ConcurrentHashMap<String, InvalidationQueue>();
private final int batchSize;
private final int batchFrequencySeconds;
private final String nodeShutdownListenerId;
public BatchInvalidator(String serviceName, int batchSize, int batchFrequencySeconds,
IFunction<EventRegistration, Boolean> eventFilter, NodeEngine nodeEngine) {
super(serviceName, eventFilter, nodeEngine);
this.batchSize = batchSize;
this.batchFrequencySeconds = batchFrequencySeconds;
this.nodeShutdownListenerId = registerNodeShutdownListener();
this.invalidationExecutorName = serviceName + getClass();
startBackgroundBatchProcessor();
}
@Override
protected void invalidateInternal(Invalidation invalidation, int orderKey) {
String dataStructureName = invalidation.getName();
InvalidationQueue invalidationQueue = invalidationQueueOf(dataStructureName);
invalidationQueue.offer(invalidation);
if (invalidationQueue.size() >= batchSize) {
pollAndSendInvalidations(dataStructureName, invalidationQueue);
}
}
private InvalidationQueue invalidationQueueOf(String dataStructureName) {
return getOrPutIfAbsent(invalidationQueues, dataStructureName, invalidationQueueConstructor);
}
private void pollAndSendInvalidations(String dataStructureName, InvalidationQueue invalidationQueue) {
assert invalidationQueue != null;
if (!invalidationQueue.tryAcquire()) {
return;
}
List<Invalidation> invalidations;
try {
invalidations = pollInvalidations(invalidationQueue);
} finally {
invalidationQueue.release();
}
sendInvalidations(dataStructureName, invalidations);
}
private List<Invalidation> pollInvalidations(InvalidationQueue invalidationQueue) {
final int size = invalidationQueue.size();
List<Invalidation> invalidations = new ArrayList<Invalidation>(size);
for (int i = 0; i < size; i++) {
Invalidation invalidation = invalidationQueue.poll();
if (invalidation == null) {
break;
}
invalidations.add(invalidation);
}
return invalidations;
}
private void sendInvalidations(String dataStructureName, List<Invalidation> invalidations) {
// There will always be at least one listener which listens invalidations. This is the reason behind eager creation
// of BatchNearCacheInvalidation instance here. There is a causality between listener and invalidation. Only if we have
// a listener, we can have an invalidation, otherwise invalidations are not generated.
Invalidation invalidation = new BatchNearCacheInvalidation(dataStructureName, invalidations);
Collection<EventRegistration> registrations = eventService.getRegistrations(serviceName, dataStructureName);
for (EventRegistration registration : registrations) {
if (eventFilter.apply(registration)) {
// find worker queue of striped executor by using subscribers' address.
// we want to send all batch invalidations belonging to same subscriber go into
// the same workers queue.
int orderKey = registration.getSubscriber().hashCode();
eventService.publishEvent(serviceName, registration, invalidation, orderKey);
}
}
}
/**
* Sends remaining invalidation events in this invalidator's queues to the recipients.
*/
private String registerNodeShutdownListener() {
HazelcastInstance node = nodeEngine.getHazelcastInstance();
LifecycleService lifecycleService = node.getLifecycleService();
return lifecycleService.addLifecycleListener(new LifecycleListener() {
@Override
public void stateChanged(LifecycleEvent event) {
if (event.getState() == SHUTTING_DOWN) {
Set<Map.Entry<String, InvalidationQueue>> entries = invalidationQueues.entrySet();
for (Map.Entry<String, InvalidationQueue> entry : entries) {
pollAndSendInvalidations(entry.getKey(), entry.getValue());
}
}
}
});
}
private void startBackgroundBatchProcessor() {
ExecutionService executionService = nodeEngine.getExecutionService();
executionService.scheduleWithRepetition(invalidationExecutorName,
new BatchInvalidationEventSender(), batchFrequencySeconds, batchFrequencySeconds, SECONDS);
}
/**
* A background runner which runs periodically and consumes invalidation queues.
*/
private class BatchInvalidationEventSender implements Runnable {
@Override
public void run() {
for (Map.Entry<String, InvalidationQueue> entry : invalidationQueues.entrySet()) {
if (currentThread().isInterrupted()) {
break;
}
String name = entry.getKey();
InvalidationQueue invalidationQueue = entry.getValue();
if (invalidationQueue.size() > 0) {
pollAndSendInvalidations(name, invalidationQueue);
}
}
}
}
@Override
public void destroy(String dataStructureName, String sourceUuid) {
InvalidationQueue invalidationQueue = invalidationQueues.remove(dataStructureName);
if (invalidationQueue != null) {
invalidateInternal(newClearInvalidation(dataStructureName, sourceUuid), dataStructureName.hashCode());
}
super.destroy(dataStructureName, sourceUuid);
}
@Override
public void shutdown() {
ExecutionService executionService = nodeEngine.getExecutionService();
executionService.shutdownExecutor(invalidationExecutorName);
HazelcastInstance node = nodeEngine.getHazelcastInstance();
LifecycleService lifecycleService = node.getLifecycleService();
lifecycleService.removeLifecycleListener(nodeShutdownListenerId);
invalidationQueues.clear();
super.shutdown();
}
@Override
public void reset() {
invalidationQueues.clear();
super.reset();
}
@SerializableByConvention
public static class InvalidationQueue extends ConcurrentLinkedQueue<Invalidation> {
private final AtomicInteger elementCount = new AtomicInteger(0);
private final AtomicBoolean flushingInProgress = new AtomicBoolean(false);
@Override
public int size() {
return elementCount.get();
}
@Override
public boolean offer(Invalidation invalidation) {
boolean offered = super.offer(invalidation);
if (offered) {
elementCount.incrementAndGet();
}
return offered;
}
@Override
public Invalidation poll() {
Invalidation invalidation = super.poll();
if (invalidation != null) {
elementCount.decrementAndGet();
}
return invalidation;
}
public boolean tryAcquire() {
return flushingInProgress.compareAndSet(false, true);
}
public void release() {
flushingInProgress.set(false);
}
@Override
public boolean add(Invalidation invalidation) {
throw new UnsupportedOperationException();
}
@Override
public Invalidation remove() {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(Collection<? extends Invalidation> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
}
}