/* * 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.map.impl.event; import com.hazelcast.core.EntryEventType; import com.hazelcast.internal.serialization.InternalSerializationService; import com.hazelcast.map.impl.querycache.QueryCacheContext; import com.hazelcast.map.impl.querycache.accumulator.Accumulator; import com.hazelcast.map.impl.querycache.event.DefaultQueryCacheEventData; import com.hazelcast.map.impl.querycache.event.QueryCacheEventData; import com.hazelcast.map.impl.querycache.publisher.MapPublisherRegistry; import com.hazelcast.map.impl.querycache.publisher.PartitionAccumulatorRegistry; import com.hazelcast.map.impl.querycache.publisher.PublisherContext; import com.hazelcast.map.impl.querycache.publisher.PublisherRegistry; import com.hazelcast.nio.Address; import com.hazelcast.nio.serialization.Data; import com.hazelcast.spi.EventFilter; import java.util.Collection; import java.util.Collections; import static com.hazelcast.core.EntryEventType.ADDED; import static com.hazelcast.core.EntryEventType.EXPIRED; import static com.hazelcast.core.EntryEventType.REMOVED; import static com.hazelcast.core.EntryEventType.UPDATED; import static com.hazelcast.map.impl.event.MapEventPublisherImpl.isIncludeValue; import static com.hazelcast.map.impl.querycache.event.QueryCacheEventDataBuilder.newQueryCacheEventDataBuilder; import static com.hazelcast.util.CollectionUtil.isEmpty; import static com.hazelcast.util.Preconditions.checkInstanceOf; /** * Handles publishing of map events to continuous query caches * * @see com.hazelcast.map.QueryCache */ public class QueryCacheEventPublisher { private final FilteringStrategy filteringStrategy; private final QueryCacheContext queryCacheContext; private final InternalSerializationService serializationService; public QueryCacheEventPublisher(FilteringStrategy filteringStrategy, QueryCacheContext queryCacheContext, InternalSerializationService serializationService) { this.filteringStrategy = filteringStrategy; this.queryCacheContext = queryCacheContext; this.serializationService = serializationService; } public void addEventToQueryCache(Object eventData) { checkInstanceOf(EventData.class, eventData, "eventData"); String mapName = ((EventData) eventData).getMapName(); int eventType = ((EventData) eventData).getEventType(); // in case of expiration, IMap publishes both EVICTED and EXPIRED events for a key // only handling EVICTED event for that key is sufficient if (EXPIRED.getType() == eventType) { return; } // this collection contains all defined query-caches on an IMap Collection<PartitionAccumulatorRegistry> partitionAccumulatorRegistries = getPartitionAccumulatorRegistries(mapName); if (isEmpty(partitionAccumulatorRegistries)) { return; } if (!(eventData instanceof EntryEventData)) { return; } EntryEventData entryEvenData = (EntryEventData) eventData; Data dataKey = entryEvenData.getDataKey(); Data dataNewValue = entryEvenData.getDataNewValue(); Data dataOldValue = entryEvenData.getDataOldValue(); int partitionId = queryCacheContext.getPartitionId(entryEvenData.dataKey); for (PartitionAccumulatorRegistry registry : partitionAccumulatorRegistries) { DefaultQueryCacheEventData singleEventData = (DefaultQueryCacheEventData) convertQueryCacheEventDataOrNull(registry, dataKey, dataNewValue, dataOldValue, eventType, partitionId); if (singleEventData == null) { continue; } Accumulator accumulator = registry.getOrCreate(partitionId); accumulator.accumulate(singleEventData); } } // TODO known issue: Locked keys will also be cleared from the query-cache after calling a map-wide event like clear/evictAll public void hintMapEvent(Address caller, String mapName, EntryEventType eventType, int numberOfEntriesAffected, int partitionId) { // this collection contains all defined query-caches on this map. Collection<PartitionAccumulatorRegistry> partitionAccumulatorRegistries = getPartitionAccumulatorRegistries(mapName); for (PartitionAccumulatorRegistry accumulatorRegistry : partitionAccumulatorRegistries) { Accumulator accumulator = accumulatorRegistry.getOrCreate(partitionId); QueryCacheEventData singleEventData = newQueryCacheEventDataBuilder(false).withPartitionId(partitionId) .withEventType(eventType.getType()).build(); accumulator.accumulate(singleEventData); } } private QueryCacheEventData convertQueryCacheEventDataOrNull(PartitionAccumulatorRegistry registry, Data dataKey, Data dataNewValue, Data dataOldValue, int eventTypeId, int partitionId) { EventFilter eventFilter = registry.getEventFilter(); EntryEventType eventType = EntryEventType.getByType(eventTypeId); // when using Hazelcast default event filtering strategy, then let the CQC workaround kick-in // otherwise, just deliver the event if it matches the registry's predicate according to the configured // filtering strategy if (filteringStrategy instanceof DefaultEntryEventFilteringStrategy) { eventType = getCQCEventTypeOrNull(eventType, eventFilter, dataKey, dataNewValue, dataOldValue); } else { int producedEventTypeId = filteringStrategy.doFilter(eventFilter, dataKey, dataOldValue, dataNewValue, eventType, null); if (producedEventTypeId == FilteringStrategy.FILTER_DOES_NOT_MATCH) { eventType = null; } else { eventType = EntryEventType.getByType(producedEventTypeId); } } if (eventType == null) { return null; } boolean includeValue = isIncludeValue(eventFilter); return newQueryCacheEventDataBuilder(includeValue) .withPartitionId(partitionId) .withDataKey(dataKey) .withDataNewValue(dataNewValue) .withEventType(eventType.getType()) .withDataOldValue(dataOldValue) .withSerializationService((serializationService)).build(); } // this method processes UPDATED events and may morph them into ADDED/REMOVED events // depending on old/new value matching the EventFilter. Fixes an issue that prevents proper CQC // implementation when DefaultEntryEventFilteringStrategy is in use. It is not used when any // other filtering strategy is in place private EntryEventType getCQCEventTypeOrNull(EntryEventType eventType, EventFilter eventFilter, Data dataKey, Data dataNewValue, Data dataOldValue) { boolean newValueMatching = filteringStrategy.doFilter(eventFilter, dataKey, dataOldValue, dataNewValue, eventType, null) != FilteringStrategy.FILTER_DOES_NOT_MATCH; if (eventType == UPDATED) { // UPDATED event has a special handling as it might result in either ADDING or REMOVING an entry to/from CQC // depending on a predicate boolean oldValueMatching = filteringStrategy.doFilter(eventFilter, dataKey, null, dataOldValue, EntryEventType.ADDED, null) != FilteringStrategy.FILTER_DOES_NOT_MATCH; if (oldValueMatching) { if (!newValueMatching) { eventType = REMOVED; } } else { if (newValueMatching) { eventType = ADDED; } else { //neither old value nor new value is matching -> it's a non-event for the CQC return null; } } } else if (!newValueMatching) { return null; } return eventType; } private Collection<PartitionAccumulatorRegistry> getPartitionAccumulatorRegistries(String mapName) { PublisherContext publisherContext = queryCacheContext.getPublisherContext(); MapPublisherRegistry mapPublisherRegistry = publisherContext.getMapPublisherRegistry(); PublisherRegistry publisherRegistry = mapPublisherRegistry.getOrNull(mapName); if (publisherRegistry == null) { return Collections.emptySet(); } // this collection contains all query-caches for this map return publisherRegistry.getAll().values(); } }