/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.internal.cache.partitioned; import java.io.ByteArrayInputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.Logger; import org.apache.geode.CancelException; import org.apache.geode.DataSerializer; import org.apache.geode.cache.CacheException; import org.apache.geode.distributed.internal.DM; import org.apache.geode.distributed.internal.DistributionManager; import org.apache.geode.distributed.internal.DistributionStats; import org.apache.geode.distributed.internal.InternalDistributedSystem; import org.apache.geode.distributed.internal.ReplyException; import org.apache.geode.distributed.internal.ReplyMessage; import org.apache.geode.distributed.internal.ReplyProcessor21; import org.apache.geode.distributed.internal.membership.InternalDistributedMember; import org.apache.geode.internal.Assert; import org.apache.geode.internal.HeapDataOutputStream; import org.apache.geode.internal.Version; import org.apache.geode.internal.cache.BucketDump; import org.apache.geode.internal.cache.BucketRegion; import org.apache.geode.internal.cache.EntryEventImpl; import org.apache.geode.internal.cache.ForceReattemptException; import org.apache.geode.internal.cache.InitialImageOperation; import org.apache.geode.internal.cache.PartitionedRegion; import org.apache.geode.internal.cache.PartitionedRegionDataStore; import org.apache.geode.internal.cache.VersionTagHolder; import org.apache.geode.internal.cache.tier.InterestType; import org.apache.geode.internal.cache.versions.VersionSource; import org.apache.geode.internal.cache.versions.VersionTag; import org.apache.geode.internal.i18n.LocalizedStrings; import org.apache.geode.internal.logging.LogService; import org.apache.geode.internal.logging.log4j.LogMarker; /** * * @since GemFire 8.0 */ public final class FetchBulkEntriesMessage extends PartitionMessage { private static final Logger logger = LogService.getLogger(); private HashSet<Integer> bucketIds; private String regex; /** * Map of bucket-id as key and set of keys as value. */ private HashMap<Integer, HashSet> bucketKeys; private static final byte ALL_KEYS = (byte) 0; private static final byte KEY_LIST = (byte) 1; private static final byte REGEX = (byte) 2; private byte keys; private boolean allowTombstones; public FetchBulkEntriesMessage() {} private FetchBulkEntriesMessage(InternalDistributedMember recipient, int regionId, ReplyProcessor21 processor, HashMap<Integer, HashSet> bucketKeys, HashSet<Integer> bucketIds, String regex, boolean allowTombstones) { super(recipient, regionId, processor); this.bucketKeys = bucketKeys; this.bucketIds = bucketIds; this.regex = regex; this.keys = bucketKeys != null ? KEY_LIST : ALL_KEYS; this.allowTombstones = allowTombstones; } /** * Sends a PartitionedRegion message to fetch all the entries for a bucketId * * @param recipient the member that the fetch keys message is sent to * @param r the PartitionedRegion that contains the bucket * @param bucketIds the identity of the buckets that contain the entries to be returned * @param regex the regular expression to be evaluated for selecting keys * @param allowTombstones * @return the processor used to read the returned entries * @throws ForceReattemptException if the peer is no longer available */ public static FetchBulkEntriesResponse send(InternalDistributedMember recipient, PartitionedRegion r, HashMap<Integer, HashSet> bucketKeys, HashSet<Integer> bucketIds, String regex, boolean allowTombstones) throws ForceReattemptException { Assert.assertTrue(recipient != null, "FetchBulkEntriesMessage NULL reply message"); FetchBulkEntriesResponse p = new FetchBulkEntriesResponse(r.getSystem(), r, recipient); FetchBulkEntriesMessage m = new FetchBulkEntriesMessage(recipient, r.getPRId(), p, bucketKeys, bucketIds, regex, allowTombstones); Set failures = r.getDistributionManager().putOutgoing(m); if (failures != null && failures.size() > 0) { throw new ForceReattemptException( LocalizedStrings.FetchEntriesMessage_FAILED_SENDING_0.toLocalizedString(m)); } return p; } @Override protected boolean operateOnPartitionedRegion(DistributionManager dm, PartitionedRegion pr, long startTime) throws CacheException, ForceReattemptException { if (logger.isTraceEnabled(LogMarker.DM)) { logger.debug("FetchBulkEntriesMessage operateOnRegion: {}", pr.getFullPath()); } FetchBulkEntriesReplyMessage.sendReply(pr, getSender(), getProcessorId(), dm, this.bucketKeys, this.bucketIds, regex, this.allowTombstones, startTime); return false; } @Override protected void appendFields(StringBuffer buff) { super.appendFields(buff); buff.append("; bucketId=").append(this.bucketIds); buff.append("; recipient=").append(this.getRecipient()); } public int getDSFID() { return PR_FETCH_BULK_ENTRIES_MESSAGE; } @Override public void fromData(DataInput in) throws IOException, ClassNotFoundException { super.fromData(in); this.keys = DataSerializer.readByte(in); if (this.keys == KEY_LIST) { this.bucketKeys = DataSerializer.readHashMap(in); } else if (this.keys == ALL_KEYS) { this.bucketIds = DataSerializer.readHashSet(in); } this.regex = DataSerializer.readString(in); this.allowTombstones = DataSerializer.readPrimitiveBoolean(in); } @Override public void toData(DataOutput out) throws IOException { super.toData(out); DataSerializer.writeByte(this.keys, out); if (this.keys == KEY_LIST) { DataSerializer.writeHashMap(this.bucketKeys, out); } else if (this.keys == ALL_KEYS) { DataSerializer.writeHashSet(this.bucketIds, out); } DataSerializer.writeString(this.regex, out); DataSerializer.writePrimitiveBoolean(this.allowTombstones, out); } public static final class FetchBulkEntriesReplyMessage extends ReplyMessage { /** Whether this message is the last of a series of chunk responses */ boolean lastInSeries; /** Array holding chunk of entries received */ transient byte[] chunk; /** Stream holding chunk of entries to send */ transient HeapDataOutputStream chunkStream; /** Sequence number of this chunk message */ private int msgNum; private HashSet<Integer> failedBucketIds; @Override public Version[] getSerializationVersions() { return null; } /** * Empty constructor to conform to DataSerializable interface */ public FetchBulkEntriesReplyMessage() {} private FetchBulkEntriesReplyMessage(InternalDistributedMember dest, int processorId, HeapDataOutputStream chunk, int msgNum, boolean lastInSeries) { setRecipient(dest); setProcessorId(processorId); this.lastInSeries = lastInSeries; this.chunkStream = chunk; this.msgNum = msgNum; } public static void sendReply(PartitionedRegion pr, final InternalDistributedMember recipient, final int processorId, final DM dm, final HashMap<Integer, HashSet> bucketKeys, final HashSet<Integer> bucketIds, String regex, boolean allowTombstones, long startTime) throws ForceReattemptException { PartitionedRegionDataStore ds = pr.getDataStore(); if (ds == null) { return; } ArrayList<BucketRegion> maps = new ArrayList<BucketRegion>(); HashSet<Integer> failedBuckets = new HashSet<Integer>(); Set<Integer> bucketIdSet = null; if (bucketKeys != null) { bucketIdSet = bucketKeys.keySet(); } else { // bucketIds != null bucketIdSet = bucketIds; } for (int id : bucketIdSet) { try { maps.add(ds.handleRemoteGetEntries(id)); } catch (ForceReattemptException fre) { failedBuckets.add(id); } } HeapDataOutputStream mos = new HeapDataOutputStream( InitialImageOperation.CHUNK_SIZE_IN_BYTES + 2048, recipient.getVersionObject()); Iterator<BucketRegion> mapsIterator = maps.iterator(); BucketRegion map = null; Iterator it = null; boolean keepGoing = true; boolean lockAcquired = false; boolean writeFooter = false; boolean lastMsgSent = false; boolean needToWriteBucketInfo = true; int msgNum = 0; while (mapsIterator.hasNext()) { if (map != null && lockAcquired) { try { map.releaseDestroyLock(); // instead take a bucketCreationLock.getWriteLock() or pr.BucketLock? } catch (CancelException e) { } finally { lockAcquired = false; } } map = mapsIterator.next(); if (map.isBucketDestroyed()) { failedBuckets.add(map.getId()); continue; } try { map.acquireDestroyLock(); lockAcquired = true; } catch (CancelException e) { if (logger.isDebugEnabled()) { logger.debug("sendReply: acquireDestroyLock failed due to cache closure, region = {}", map.getFullPath()); } } try { if (bucketKeys != null) { it = bucketKeys.get(map.getId()).iterator(); } else { // bucketIds != null if (regex == null) { it = new HashSet(map.keySet(allowTombstones)).iterator(); } else { it = map.getKeysWithInterest(InterestType.REGULAR_EXPRESSION, regex, allowTombstones) .iterator(); } } while (it.hasNext()) { Object key = it.next(); VersionTagHolder clientEvent = new VersionTagHolder(); Object value = map.get(key, null, true, true, true, null, clientEvent, allowTombstones); if (needToWriteBucketInfo) { DataSerializer.writePrimitiveInt(map.getId(), mos); needToWriteBucketInfo = false; writeFooter = true; } int entrySize = mos.size(); DataSerializer.writeObject(key, mos); VersionTag versionTag = clientEvent.getVersionTag(); if (versionTag != null) { versionTag.replaceNullIDs(map.getVersionMember()); } DataSerializer.writeObject(value, mos); DataSerializer.writeObject(versionTag, mos); entrySize = mos.size() - entrySize; // If no more space OR no more entries in bucket, write end-of-bucket marker. if ((mos.size() + entrySize) >= InitialImageOperation.CHUNK_SIZE_IN_BYTES || !it.hasNext()) { DataSerializer.writeObject(null, mos); DataSerializer.writePrimitiveBoolean(it.hasNext(), mos); needToWriteBucketInfo = true; writeFooter = false; // Be safe } // If no more space in chunk, send it. if ((mos.size() + entrySize) >= InitialImageOperation.CHUNK_SIZE_IN_BYTES) { // Send as last message if no more data boolean lastMsg = !(it.hasNext() || mapsIterator.hasNext()); ++msgNum; FetchBulkEntriesReplyMessage reply = new FetchBulkEntriesReplyMessage(recipient, processorId, mos, msgNum, lastMsg); if (lastMsg) { reply.failedBucketIds = failedBuckets; } Set failures = dm.putOutgoing(reply); keepGoing = (failures == null) || (failures.size() == 0); if (lastMsg && keepGoing) { lastMsgSent = true; } mos.reset(); } // else still enough space } // while (for each key) if (!keepGoing) { throw new ForceReattemptException("Failed to send response"); } } catch (IOException ioe) { throw new ForceReattemptException( LocalizedStrings.FetchEntriesMessage_UNABLE_TO_SEND_RESPONSE_TO_FETCHENTRIES_REQUEST .toLocalizedString(), ioe); } finally { if (lockAcquired) { try { map.releaseDestroyLock(); } catch (CancelException e) { } finally { lockAcquired = false; } } } } // while (for each map) if (!lastMsgSent) { if (mos.size() == 0) { try { DataSerializer.writePrimitiveInt(-1, mos); } catch (IOException ioe) { throw new ForceReattemptException( LocalizedStrings.FetchEntriesMessage_UNABLE_TO_SEND_RESPONSE_TO_FETCHENTRIES_REQUEST .toLocalizedString(), ioe); } } else if (writeFooter) { try { DataSerializer.writeObject(null, mos); // end of entries of current bucket in current // response DataSerializer.writePrimitiveBoolean(false, mos); // no more entries of current bucket } catch (IOException ioe) { throw new ForceReattemptException( LocalizedStrings.FetchEntriesMessage_UNABLE_TO_SEND_RESPONSE_TO_FETCHENTRIES_REQUEST .toLocalizedString(), ioe); } } ++msgNum; FetchBulkEntriesReplyMessage reply = new FetchBulkEntriesReplyMessage(recipient, processorId, mos, msgNum, true); reply.failedBucketIds = failedBuckets; Set failures = dm.putOutgoing(reply); if (failures != null && failures.size() > 0) { throw new ForceReattemptException("Failed to send response"); } } if (lockAcquired) { try { map.releaseDestroyLock(); } catch (CancelException e) { // ignore } finally { lockAcquired = false; } } } /** * Processes this message. This method is invoked by the receiver of the message. * * @param dm the distribution manager that is processing the message. */ @Override public void process(final DM dm, final ReplyProcessor21 p) { final long startTime = getTimestamp(); FetchBulkEntriesResponse processor = (FetchBulkEntriesResponse) p; if (processor == null) { if (logger.isTraceEnabled(LogMarker.DM)) { logger.trace(LogMarker.DM, "FetchBulkEntriesReplyMessage processor not found"); } return; } processor.processChunkResponse(this); if (logger.isTraceEnabled(LogMarker.DM)) { logger.trace(LogMarker.DM, "{} processed {}", processor, this); } dm.getStats().incReplyMessageTime(DistributionStats.getStatTime() - startTime); } @Override public void toData(DataOutput out) throws IOException { super.toData(out); out.writeBoolean(this.lastInSeries); DataSerializer.writePrimitiveInt(this.msgNum, out); DataSerializer.writeObjectAsByteArray(this.chunkStream, out); DataSerializer.writeHashSet(this.failedBucketIds, out); } @Override public int getDSFID() { return PR_FETCH_BULK_ENTRIES_REPLY_MESSAGE; } @Override public void fromData(DataInput in) throws IOException, ClassNotFoundException { super.fromData(in); this.lastInSeries = in.readBoolean(); this.msgNum = DataSerializer.readPrimitiveInt(in); this.chunk = DataSerializer.readByteArray(in); this.failedBucketIds = DataSerializer.readHashSet(in); } @Override public String toString() { StringBuffer sb = new StringBuffer(); sb.append("FetchBulkEntriesReplyMessage ").append("processorid=").append(this.processorId); if (getSender() != null) { sb.append(",sender=").append(this.getSender()); } sb.append(",lastInSeries=").append(lastInSeries); if (chunk != null) { sb.append(",size=").append(chunk.length); } else if (chunkStream != null) { sb.append(",size=").append(chunkStream.size()); } if (getException() != null) { sb.append(",exception=").append(getException()); } return sb.toString(); } } /** * A processor to capture the value returned by * {@link org.apache.geode.internal.cache.partitioned.FetchBulkEntriesMessage} * * @since GemFire 8.0 */ public static class FetchBulkEntriesResponse extends ReplyProcessor21 { private final PartitionedRegion pr; private final HashMap<Integer, HashMap<Object, Object>> returnValue; private final HashMap<Integer, HashMap<Object, VersionTag>> returnVersions = new HashMap(); private final Map<VersionSource, VersionSource> canonicalMembers = new ConcurrentHashMap<VersionSource, VersionSource>(); /** lock used to synchronize chunk processing */ private final Object endLock = new Object(); /** number of chunks processed */ private volatile int chunksProcessed; /** whether the last chunk has been processed */ private volatile boolean lastChunkReceived; private HashSet<Integer> failedBucketIds; private ArrayList<Integer> receivedBuckets = new ArrayList<Integer>(); private int expectedChunks; private InternalDistributedMember recipient; public FetchBulkEntriesResponse(InternalDistributedSystem ds, final PartitionedRegion pr, final InternalDistributedMember recipient) { super(ds, Collections.singleton(recipient)); this.pr = pr; this.recipient = recipient; this.returnValue = new HashMap<Integer, HashMap<Object, Object>>(); } void processChunkResponse(FetchBulkEntriesReplyMessage msg) { boolean doneProcessing = false; if (msg.getException() != null) { process(msg); } else { boolean deserializingKey = true; try { ByteArrayInputStream byteStream = new ByteArrayInputStream(msg.chunk); DataInputStream in = new DataInputStream(byteStream); Object key; int currentId; final boolean isDebugEnabled = logger.isTraceEnabled(LogMarker.DM); while (in.available() > 0) { currentId = DataSerializer.readPrimitiveInt(in); if (currentId == -1) { break; } while (in.available() > 0) { deserializingKey = true; key = DataSerializer.readObject(in); if (key != null) { deserializingKey = false; Object value = DataSerializer.readObject(in); VersionTag versionTag = DataSerializer.readObject(in); if (versionTag != null) { // Fix for 47260 - canonicalize the member ids to avoid an OOME if (canonicalMembers.containsKey(versionTag.getMemberID())) { versionTag.setMemberID(canonicalMembers.get(versionTag.getMemberID())); } else { canonicalMembers.put(versionTag.getMemberID(), versionTag.getMemberID()); } } synchronized (returnValue) { HashMap<Object, Object> valueMap = returnValue.get(currentId); HashMap<Object, VersionTag> versionMap = returnVersions.get(currentId); if (valueMap != null) { valueMap.put(key, value); } else { valueMap = new HashMap<Object, Object>(); valueMap.put(key, value); returnValue.put(currentId, valueMap); } if (versionMap != null) { versionMap.put(key, versionTag); } else { versionMap = new HashMap<Object, VersionTag>(); versionMap.put(key, versionTag); returnVersions.put(currentId, versionMap); } } } else { // null should signal the end of the set of keys boolean bucketHasMore = DataSerializer.readPrimitiveBoolean(in); synchronized (this.returnValue) { if (!bucketHasMore && currentId != -1) { this.receivedBuckets.add(currentId); } } break; } } // inner while } // outer while synchronized (this.endLock) { this.chunksProcessed = this.chunksProcessed + 1; if (msg.lastInSeries) { this.expectedChunks = msg.msgNum; this.failedBucketIds = msg.failedBucketIds; } if (this.expectedChunks == this.chunksProcessed) { doneProcessing = true; this.lastChunkReceived = true; } if (isDebugEnabled) { logger.trace(LogMarker.DM, "{} chunksProcessed={}, lastChunkReceived={},done={}", this, this.chunksProcessed, this.lastChunkReceived, doneProcessing); } } } catch (Exception e) { if (deserializingKey) { processException(new ReplyException( LocalizedStrings.FetchEntriesMessage_ERROR_DESERIALIZING_KEYS.toLocalizedString(), e)); } else { processException(new ReplyException( LocalizedStrings.FetchEntriesMessage_ERROR_DESERIALIZING_VALUES.toLocalizedString(), e)); // for bug 41202 } checkIfDone(); // fix for hang in 41202 } // if all chunks have been received, wake up the waiting thread if (doneProcessing) { process(msg); } } // else msg.getException() == null } /** * @return Array of BucketDumps * @throws ForceReattemptException if the peer is no longer available */ public BucketDump[] waitForEntries() throws ForceReattemptException { try { waitForRepliesUninterruptibly(); } catch (ReplyException e) { Throwable t = e.getCause(); if (t instanceof CancelException) { logger.debug("FetchBulkEntriesResponse got remote cancellation; forcing reattempt. {}", t.getMessage(), t); throw new ForceReattemptException( LocalizedStrings.FetchEntriesMessage_FETCHKEYSRESPONSE_GOT_REMOTE_CANCELLATION_FORCING_REATTEMPT .toLocalizedString(), t); } else if (t instanceof ForceReattemptException) { // Not sure this is necessary, but it is possible for // FetchBulkEntriesMessage to marshal a ForceReattemptException, so... throw new ForceReattemptException( LocalizedStrings.FetchEntriesMessage_PEER_REQUESTS_REATTEMPT.toLocalizedString(), t); } e.handleAsUnexpected(); } if (!this.lastChunkReceived) { throw new ForceReattemptException( LocalizedStrings.FetchEntriesMessage_NO_REPLIES_RECEIVED.toLocalizedString()); } BucketDump[] dumps = new BucketDump[this.receivedBuckets.size()]; for (int i = 0; i < this.receivedBuckets.size(); i++) { int id = this.receivedBuckets.get(i); dumps[i] = new BucketDump(id, recipient, null, returnValue.get(id), returnVersions.get(id)); } return dumps; } public HashSet<Integer> getFailedBucketIds() { return this.failedBucketIds; } } public Version[] getSerializationVersions() { return null; } }