LOG4J2-1349 GC-free ThreadContext initial commit - move ArrayContextData and OpenHashMapContextData to spi package in log4j-api module - move BiConsumer/TriConsumer to util package in log4j-api module - added CopyOnWriteSortedArrayThreadContext and SortedArrayThreadContext to support garbage-free and classic thread context strategies
Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/1118b27f Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/1118b27f Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/1118b27f Branch: refs/heads/LOG4J2-1349-gcfree-threadcontext Commit: 1118b27fafe76c4d6e36cd71ffa2eee90991710f Parents: 88d2066 Author: rpopma <[email protected]> Authored: Fri Aug 19 09:10:15 2016 +0900 Committer: rpopma <[email protected]> Committed: Tue Aug 23 00:31:03 2016 +0900 ---------------------------------------------------------------------- .../logging/log4j/spi/ArrayContextData.java | 448 ++++++++++ .../apache/logging/log4j/spi/ContextData.java | 115 +++ .../CopyOnWriteSortedArrayThreadContext.java | 156 ++++ .../logging/log4j/spi/MutableContextData.java | 68 ++ .../log4j/spi/OpenHashMapContextData.java | 883 ++++++++++++++++++ .../log4j/spi/SortedArrayThreadContext.java | 156 ++++ .../apache/logging/log4j/util/BiConsumer.java | 19 + .../apache/logging/log4j/util/TriConsumer.java | 21 + .../logging/log4j/spi/ArrayContextDataTest.java | 569 ++++++++++++ .../log4j/spi/OpenHashMapContextDataTest.java | 507 +++++++++++ .../logging/log4j/core/AbstractLogEvent.java | 1 + .../apache/logging/log4j/core/ContextData.java | 119 --- .../org/apache/logging/log4j/core/LogEvent.java | 1 + .../db/jpa/AbstractLogEventWrapperEntity.java | 2 +- .../ContextDataAttributeConverter.java | 2 +- .../ContextDataJsonAttributeConverter.java | 6 +- .../logging/log4j/core/async/AsyncLogger.java | 2 +- .../log4j/core/async/RingBufferLogEvent.java | 4 +- .../async/RingBufferLogEventTranslator.java | 2 +- .../log4j/core/impl/ArrayContextData.java | 438 --------- .../log4j/core/impl/ContextDataFactory.java | 3 + .../log4j/core/impl/ContextDataInjector.java | 3 + .../core/impl/ContextDataInjectorFactory.java | 2 + .../logging/log4j/core/impl/Log4jLogEvent.java | 4 +- .../log4j/core/impl/MutableContextData.java | 68 -- .../log4j/core/impl/MutableLogEvent.java | 3 +- .../log4j/core/impl/OpenHashMapContextData.java | 887 ------------------- .../core/impl/ReusableLogEventFactory.java | 1 + .../core/impl/ThreadContextDataInjector.java | 4 +- .../ContextDataAsEntryListDeserializer.java | 2 +- .../ContextDataAsEntryListSerializer.java | 4 +- .../core/jackson/ContextDataDeserializer.java | 2 +- .../core/jackson/ContextDataSerializer.java | 4 +- .../log4j/core/jackson/LogEventJsonMixIn.java | 2 +- .../jackson/LogEventWithContextListMixIn.java | 2 +- .../logging/log4j/core/layout/GelfLayout.java | 2 +- .../log4j/core/pattern/MdcPatternConverter.java | 4 +- .../logging/log4j/core/util/BiConsumer.java | 19 - .../logging/log4j/core/util/TriConsumer.java | 21 - .../core/appender/db/jpa/TestBaseEntity.java | 2 +- .../ContextDataAttributeConverterTest.java | 4 +- .../ContextDataJsonAttributeConverterTest.java | 6 +- .../core/async/RingBufferLogEventTest.java | 2 +- .../log4j/core/impl/ArrayContextDataTest.java | 568 ------------ .../log4j/core/impl/Log4jLogEventTest.java | 2 + .../log4j/core/impl/MutableLogEventTest.java | 2 + .../core/impl/OpenHashMapContextDataTest.java | 506 ----------- .../log4j/flume/appender/FlumeEvent.java | 2 +- .../jmh/ArrayContextDataVsHashMapBenchmark.java | 8 +- 49 files changed, 2997 insertions(+), 2661 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/spi/ArrayContextData.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ArrayContextData.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ArrayContextData.java new file mode 100644 index 0000000..d934a3d --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ArrayContextData.java @@ -0,0 +1,448 @@ +/* + * 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.logging.log4j.spi; + +import java.io.IOException; +import java.io.InvalidObjectException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.apache.logging.log4j.util.BiConsumer; +import org.apache.logging.log4j.util.TriConsumer; + +/** + * Array-based implementation of the {@code ContextData} interface. Keys are held in a sorted array. + * <p> + * This is not a generic collection, but makes some trade-offs to optimize for the Log4j ContextData use case: + * </p> + * <ul> + * <li>Garbage-free iteration over key-value pairs with {@code BiConsumer} and {@code TriConsumer}.</li> + * <li>Fast copy. If the ThreadContextMap is also an instance of {@code ArrayContextData}, the full thread context + * data can be transferred with two array copies and two field updates.</li> + * <li>Acceptable performance for small data sets. The current implementation stores keys in a sorted array, values + * are stored in a separate array at the same index. + * Worst-case performance of {@code get} and {@code containsKey} is O(log N), + * worst-case performance of {@code put} and {@code remove} is O(N log N). + * The expectation is that for the small values of {@code N} (less than 100) that are the vast majority of + * ThreadContext use cases, the constants dominate performance more than the asymptotic performance of the + * algorithms used. + * </li> + * <li>Compact representation.</li> + * </ul> + * + * @since 2.7 + */ +public class ArrayContextData implements MutableContextData, ThreadContextMap { + + /** + * The default initial capacity. + */ + private static final int DEFAULT_INITIAL_CAPACITY = 4; + private static final long serialVersionUID = -5748905872274478116L; + private static final int HASHVAL = 31; + + private static final TriConsumer<String, Object, MutableContextData> PUT_ALL = new TriConsumer<String, Object, MutableContextData>() { + @Override + public void accept(final String key, final Object value, final MutableContextData contextData) { + contextData.putValue(key, value); + } + }; + + /** + * An empty array instance to share when the table is not inflated. + */ + private static final String[] EMPTY = {}; + + private transient String[] keys = EMPTY; + private transient Object[] values = EMPTY; + + /** + * The number of key-value mappings contained in this map. + */ + private transient int size; + + /** + * The next size value at which to resize (capacity * load factor). + * @serial + */ + // If table == EMPTY_TABLE then this is the initial capacity at which the + // table will be created when inflated. + private int threshold; + + public ArrayContextData() { + this(DEFAULT_INITIAL_CAPACITY); + } + + public ArrayContextData(final int initialCapacity) { + if (initialCapacity < 1) { + throw new IllegalArgumentException("Initial capacity must be at least one but was " + initialCapacity); + } + threshold = ceilingNextPowerOfTwo(initialCapacity); + } + + public ArrayContextData(final ContextData other) { + if (other instanceof ArrayContextData) { + initFrom0((ArrayContextData) other); + } else if (other != null) { + resize(ceilingNextPowerOfTwo(other.size())); + other.forEach(PUT_ALL, this); + } + } + + @Override + public void clear() { + Arrays.fill(keys, 0, size, null); + Arrays.fill(values, 0, size, null); + size = 0; + } + + @Override + public boolean containsKey(final String key) { + return indexOfKey(key) >= 0; + } + + @Override + public Map<String, String> asMap() { + final Map<String, String> result = new HashMap<>(size()); + for (int i = 0; i < size(); i++) { + final Object value = getValueAt(i); + result.put(getKeyAt(i), value == null ? null : String.valueOf(value)); + } + return result; + } + + @Override + public Map<String, String> getCopy() { + return asMap(); + } + + @Override + public Map<String, String> getImmutableMapOrNull() { + return isEmpty() ? null : Collections.unmodifiableMap(asMap()); + } + + @Override + public String get(final String key) { + final Object result = getValue(key); + return result == null ? null : String.valueOf(result); + } + + @SuppressWarnings("unchecked") + @Override + public <V> V getValue(final String key) { + final int index = indexOfKey(key); + if (index < 0) { + return null; + } + return (V) values[index]; + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + int indexOfKey(final String key) { + if (keys == EMPTY) { + return -1; + } + if (key == null) { // null key is located at the start of the array + return nullKeyIndex(); // insert at index zero + } + final int start = size > 0 && keys[0] == null ? 1 : 0; + return Arrays.binarySearch(keys, start, size, key); + } + + private int nullKeyIndex() { + return size > 0 && keys[0] == null ? 0 : ~0; + } + + @Override + public void put(final String key, final String value) { + putValue(key, value); + } + + @Override + public void putValue(final String key, final Object value) { + if (keys == EMPTY) { + inflateTable(threshold); + } + final int index = indexOfKey(key); + if (index >= 0) { + keys[index] = key; + values[index] = value; + } else { // not found, so insert. + insertAt(~index, key, value); + } + } + + private void insertAt(final int index, final String key, final Object value) { + ensureCapacity(); + System.arraycopy(keys, index, keys, index + 1, size - index); + System.arraycopy(values, index, values, index + 1, size - index); + keys[index] = key; + values[index] = value; + size++; + } + + @Override + public void putAll(final ContextData source) { + if (source instanceof ArrayContextData) { + initFrom0((ArrayContextData) source); + } else if (source != null) { + source.forEach(PUT_ALL, this); + } + } + + public void initFrom(final ArrayContextData other) { + initFrom0(other); + } + + private void initFrom0(final ArrayContextData other) { + if (keys.length < other.size) { + keys = new String[other.threshold]; + values = new Object[other.threshold]; + } + System.arraycopy(other.keys, 0, keys, 0, other.size); + System.arraycopy(other.values, 0, values, 0, other.size); + + size = other.size; + threshold = other.threshold; + } + + private void ensureCapacity() { + if (size >= threshold) { + resize(threshold * 2); + } + } + + private void resize(final int newCapacity) { + final String[] oldKeys = keys; + final Object[] oldValues = values; + + keys = new String[newCapacity]; + values = new Object[newCapacity]; + + System.arraycopy(oldKeys, 0, keys, 0, size); + System.arraycopy(oldValues, 0, values, 0, size); + + threshold = newCapacity; + } + + /** + * Inflates the table. + */ + private void inflateTable(int toSize) { + threshold = toSize; + keys = new String[toSize]; + values = new Object[toSize]; + } + + @Override + public void remove(final String key) { + if (keys == EMPTY) { + return; + } + final int index = indexOfKey(key); + if (index >= 0) { + System.arraycopy(keys, index + 1, keys, index, size - index); + System.arraycopy(values, index + 1, values, index, size - index); + size--; + } + } + + String getKeyAt(final int index) { + if (index < 0 || index >= size) { + return null; + } + return keys[index]; + } + + @SuppressWarnings("unchecked") + <V> V getValueAt(final int index) { + if (index < 0 || index >= size) { + return null; + } + return (V) values[index]; + } + + @Override + public int size() { + return size; + } + + @SuppressWarnings("unchecked") + @Override + public <V> void forEach(BiConsumer<String, ? super V> action) { + for (int i = 0; i < size; i++) { + action.accept(keys[i], (V) values[i]); + } + } + + @SuppressWarnings("unchecked") + @Override + public <V, T> void forEach(TriConsumer<String, ? super V, T> action, T state) { + for (int i = 0; i < size; i++) { + action.accept(keys[i], (V) values[i], state); + } + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ArrayContextData)) { + return false; + } + ArrayContextData other = (ArrayContextData) obj; + if (this.size() != other.size()) { + return false; + } + for (int i = 0; i < size(); i++) { + if (!Objects.equals(keys[i], other.keys[i])) { + return false; + } + if (!Objects.equals(values[i], other.values[i])) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int result = 37; + result = HASHVAL * result + size; + result = HASHVAL * result + hashCode(keys, size); + result = HASHVAL * result + hashCode(values, size); + return result; + } + + private static int hashCode(Object[] values, int length) { + int result = 1; + for (int i = 0; i < length; i++) { + result = HASHVAL * result + (values[i] == null ? 0 : values[i].hashCode()); + } + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(256); + sb.append('{'); + for (int i = 0; i < size; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(keys[i]).append('='); + sb.append(values[i] == this ? "(this map)" : values[i]); + } + sb.append('}'); + return sb.toString(); + } + + /** + * Save the state of the {@code ArrayContextData} instance to a stream (i.e., + * serialize it). + * + * @serialData The <i>capacity</i> of the ArrayContextData (the length of the + * bucket array) is emitted (int), followed by the + * <i>size</i> (an int, the number of key-value + * mappings), followed by the key (Object) and value (Object) + * for each key-value mapping. The key-value mappings are + * emitted in no particular order. + */ + private void writeObject(java.io.ObjectOutputStream s) throws IOException { + // Write out the threshold, and any hidden stuff + s.defaultWriteObject(); + + // Write out number of buckets + if (keys == EMPTY) { + s.writeInt(ceilingNextPowerOfTwo(threshold)); + } else { + s.writeInt(keys.length); + } + + // Write out size (number of Mappings) + s.writeInt(size); + + // Write out keys and values (alternating) + if (size > 0) { + for (int i = 0; i < size; i++) { + s.writeObject(keys[i]); + s.writeObject(values[i]); + } + } + } + + + /** + * Calculate the next power of 2, greater than or equal to x. + * <p> + * From Hacker's Delight, Chapter 3, Harry S. Warren Jr. + * + * @param x Value to round up + * @return The next power of 2 from x inclusive + */ + private static int ceilingNextPowerOfTwo(final int x) { + final int BITS_PER_INT = 32; + return 1 << (BITS_PER_INT - Integer.numberOfLeadingZeros(x - 1)); + } + + /** + * Reconstitute the {@code ArrayContextData} instance from a stream (i.e., + * deserialize it). + */ + private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + // Read in the threshold (ignored), and any hidden stuff + s.defaultReadObject(); + + // set other fields that need values + keys = EMPTY; + values = EMPTY; + + // Read in number of buckets + int capacity = s.readInt(); + if (capacity < 0) { + throw new InvalidObjectException("Illegal capacity: " + capacity); + } + + // Read number of mappings + int mappings = s.readInt(); + if (mappings < 0) { + throw new InvalidObjectException("Illegal mappings count: " + mappings); + } + + // allocate the bucket array; + if (mappings > 0) { + inflateTable(capacity); + } else { + threshold = capacity; + } + + // Read the keys and values, and put the mappings in the arrays + for (int i = 0; i < mappings; i++) { + keys[i] = (String) s.readObject(); + values[i] = s.readObject(); + } + size = mappings; + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextData.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextData.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextData.java new file mode 100644 index 0000000..d62b3f4 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextData.java @@ -0,0 +1,115 @@ +/* + * 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.logging.log4j.spi; + +import java.io.Serializable; +import java.util.Map; + +import org.apache.logging.log4j.util.BiConsumer; +import org.apache.logging.log4j.util.TriConsumer; + +/** + * A read-only collection of context data. Context data items are String keys and values of arbitrary type that are + * set by the application to be included in all subsequent log events. A typical source of context data is the + * {@code ThreadContextMap} and the {@code Properties} defined in the configuration. + * <p> + * Applications can put custom data in this collection by installing a custom {@code ContextDataInjector}. + * </p> + * + * @see org.apache.logging.log4j.spi.ThreadContextMap + * @since 2.7 + */ +public interface ContextData extends Serializable { + /** + * Returns a {@code Map<String, String>} view of this context data. + * + * @return a map view of this context data + */ + Map<String, String> asMap(); + + /** + * Returns {@code true} if this context data contains the specified key, {@code false} otherwise. + * + * @param key the key whose presence to check. May be {@code null}. + * @return {@code true} if this context data contains the specified key, {@code false} otherwise + */ + boolean containsKey(String key); + + /** + * Performs the given action for each key-value pair in this data structure + * until all entries have been processed or the action throws an exception. + * <p> + * Some implementations may not support structural modifications (adding new elements or removing elements) while + * iterating over the contents. In such implementations, attempts to add or remove elements from the + * {@code BiConsumer}'s {@link BiConsumer#accept(Object, Object)} accept} method may cause a + * {@code ConcurrentModificationException} to be thrown. + * </p> + * + * @param action The action to be performed for each key-value pair in this collection + * @param <V> type of the value + * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications + * to this context data while iterating over the contents with {@link #forEach(BiConsumer)} or + * {@link #forEach(TriConsumer, Object)}. + */ + <V> void forEach(final BiConsumer<String, ? super V> action); + + /** + * Performs the given action for each key-value pair in this data structure + * until all entries have been processed or the action throws an exception. + * <p> + * The third parameter lets callers pass in a stateful object to be modified with the key-value pairs, + * so the TriConsumer implementation itself can be stateless and potentially reusable. + * </p> + * <p> + * Some implementations may not support structural modifications (adding new elements or removing elements) while + * iterating over the contents. In such implementations, attempts to add or remove elements from the + * {@code TriConsumer}'s {@link TriConsumer#accept(Object, Object, Object) accept} method may cause a + * {@code ConcurrentModificationException} to be thrown. + * </p> + * + * @param action The action to be performed for each key-value pair in this collection + * @param state the object to be passed as the third parameter to each invocation on the specified + * triconsumer + * @param <V> type of the value + * @param <S> type of the third parameter + * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications + * to this context data while iterating over the contents with {@link #forEach(BiConsumer)} or + * {@link #forEach(TriConsumer, Object)}. + */ + <V, S> void forEach(final TriConsumer<String, ? super V, S> action, final S state); + + /** + * Returns the value for the specified key, or {@code null} if the specified key does not exist in this collection. + * + * @param key the key whose value to return + * @return the value for the specified key or {@code null} + */ + <V> V getValue(final String key); + + /** + * Returns {@code true} if this collection is empty (size is zero), {@code false} otherwise. + * @return {@code true} if this collection is empty (size is zero) + */ + boolean isEmpty(); + + /** + * Returns the number of key-value pairs in this collection. + * + * @return the number of key-value pairs in this collection + */ + int size(); +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContext.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContext.java new file mode 100644 index 0000000..be172b4 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContext.java @@ -0,0 +1,156 @@ +/* + * 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.logging.log4j.spi; + +import java.util.Collections; +import java.util.Map; + +import org.apache.logging.log4j.util.PropertiesUtil; + +/** + * ThreadContextMap implementation backed by {@code SortedArrayContextData}. + * A new ThreadContext Map is created each time it is updated and the Map stored is always + * immutable. This means the Map can be passed to other threads without concern that it will be updated. Since it is + * expected that the Map will be passed to many more log events than the number of keys it contains the performance + * should be much better than if the Map was copied for each event. + */ +public class CopyOnWriteSortedArrayThreadContext implements ThreadContextMap { + /** + * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain + * {@code ThreadLocal} (value is not "true") in the implementation. + */ + public static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; + + private final ThreadLocal<ArrayContextData> localMap; + + public CopyOnWriteSortedArrayThreadContext() { + this.localMap = createThreadLocalMap(); + } + + // LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured. + // (This method is package protected for JUnit tests.) + static ThreadLocal<ArrayContextData> createThreadLocalMap() { + final PropertiesUtil managerProps = PropertiesUtil.getProperties(); + final boolean inheritable = managerProps.getBooleanProperty(INHERITABLE_MAP); + if (inheritable) { + return new InheritableThreadLocal<ArrayContextData>() { + @Override + protected ArrayContextData childValue(final ArrayContextData parentValue) { + return parentValue != null ? new ArrayContextData(parentValue) : null; + } + }; + } + // if not inheritable, return plain ThreadLocal with null as initial value + return new ThreadLocal<>(); + } + + @Override + public void put(final String key, final String value) { + ArrayContextData map = localMap.get(); + map = map == null ? new ArrayContextData() : new ArrayContextData(map); + map.put(key, value); + localMap.set(map); + } + + @Override + public String get(final String key) { + final ArrayContextData map = localMap.get(); + return map == null ? null : map.get(key); + } + + @Override + public void remove(final String key) { + final ArrayContextData map = localMap.get(); + if (map != null) { + final ArrayContextData copy = new ArrayContextData(map); + copy.remove(key); + localMap.set(copy); + } + } + + @Override + public void clear() { + localMap.remove(); + } + + @Override + public boolean containsKey(final String key) { + final ArrayContextData map = localMap.get(); + return map != null && map.containsKey(key); + } + + @Override + public Map<String, String> getCopy() { + final ArrayContextData map = localMap.get(); + return map == null ? Collections.<String, String>emptyMap() : map.asMap(); + } + + public ContextData getContextData() { + return localMap.get(); + } + + @Override + public Map<String, String> getImmutableMapOrNull() { + final ArrayContextData map = localMap.get(); + return map == null ? null : Collections.unmodifiableMap(map.asMap()); + } + + @Override + public boolean isEmpty() { + final ArrayContextData map = localMap.get(); + return map == null || map.size() == 0; + } + + @Override + public String toString() { + final ArrayContextData map = localMap.get(); + return map == null ? "{}" : map.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + final ArrayContextData map = this.localMap.get(); + result = prime * result + ((map == null) ? 0 : map.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ThreadContextMap)) { + return false; + } + final ThreadContextMap other = (ThreadContextMap) obj; + final Map<String, String> map = this.getImmutableMapOrNull(); + final Map<String, String> otherMap = other.getImmutableMapOrNull(); + if (map == null) { + if (otherMap != null) { + return false; + } + } else if (!map.equals(otherMap)) { + return false; + } + return true; + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/spi/MutableContextData.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/MutableContextData.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/MutableContextData.java new file mode 100644 index 0000000..8bc64e8 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/MutableContextData.java @@ -0,0 +1,68 @@ +/* + * 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.logging.log4j.spi; + +import org.apache.logging.log4j.spi.ContextData; +import org.apache.logging.log4j.util.BiConsumer; +import org.apache.logging.log4j.util.TriConsumer; + +/** + * Exposes methods to add and remove key-value pairs to and from {@code ContextData}. + * + * @see ContextData + * @since 2.7 + */ +public interface MutableContextData extends ContextData { + + /** + * Removes all key-value pairs from this collection. + * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications + * to this context data while iterating over the contents with {@link #forEach(BiConsumer)} or + * {@link #forEach(TriConsumer, Object)}. + */ + void clear(); + + /** + * Puts the specified key-value pair into the collection. + * + * @param key the key to add or remove. Keys may be {@code null}. + * @param value the value to add. Values may be {@code null}. + * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications + * to this context data while iterating over the contents with {@link #forEach(BiConsumer)} or + * {@link #forEach(TriConsumer, Object)}. + */ + void putValue(final String key, final Object value); + + /** + * Copy all key-value pairs from the specified {@code ContextData} into this {@code MutableContextData}. + * @param source the {@code ContextData} to copy key-value pairs from + * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications + * to this context data while iterating over the contents with {@link #forEach(BiConsumer)} or + * {@link #forEach(TriConsumer, Object)}. + */ + void putAll(final ContextData source); + + /** + * Removes the key-value pair for the specified key from this context data collection. + * + * @param key the key to remove. May be {@code null}. + * @throws java.util.ConcurrentModificationException some implementations may not support structural modifications + * to this context data while iterating over the contents with {@link #forEach(BiConsumer)} or + * {@link #forEach(TriConsumer, Object)}. + */ + void remove(final String key); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/spi/OpenHashMapContextData.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/OpenHashMapContextData.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/OpenHashMapContextData.java new file mode 100644 index 0000000..9f27a6c --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/OpenHashMapContextData.java @@ -0,0 +1,883 @@ +/* + * 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.logging.log4j.spi; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.apache.logging.log4j.util.BiConsumer; +import org.apache.logging.log4j.util.TriConsumer; + +/** + * Open hash map-based implementation of the {@code ContextData} interface. + * Implementation based on <a href="http://fastutil.di.unimi.it/">fastutil</a>'s + * <a href="http://fastutil.di.unimi.it/docs/it/unimi/dsi/fastutil/objects/Object2ObjectOpenHashMap.html">Object2ObjectOpenHashMap</a>. + * <p> + * A type-specific hash map with a fast, small-footprint implementation. + * + * <P> + * Instances of this class use a hash table to represent a map. The table is + * filled up to a specified <em>load factor</em>, and then doubled in size to + * accommodate new entries. If the table is emptied below <em>one fourth</em> of + * the load factor, it is halved in size. However, halving is not performed when + * deleting entries from an iterator, as it would interfere with the iteration + * process. + * + * <p> + * Note that {@link #clear()} does not modify the hash table size. Rather, the + * {@link #trim(int)} method lets you control the size of + * the table; this is particularly useful if you reuse instances of this class. + * <p> + * <ul> + * <li>Garbage-free iteration over key-value pairs with {@code BiConsumer} and {@code TriConsumer}.</li> + * <li>Fast copy. If the ThreadContextMap is also an instance of {@code OpenHashMapContextData}, + * the full thread context data can be transferred with two array copies and five field updates.</li> + * </ul> + * + * @since 2.7 + */ +public class OpenHashMapContextData<K, V> implements MutableContextData, ThreadContextMap { + /** The initial default size of a hash table. */ + public static final int DEFAULT_INITIAL_SIZE = 16; + + /** The default load factor of a hash table. */ + public static final float DEFAULT_LOAD_FACTOR = .75f; + + private static final long serialVersionUID = -1486744623338827187L; + + /** The array of keys. */ + protected transient K[] keys; + /** The array of values. */ + protected transient V[] values; + /** The mask for wrapping a position counter. */ + protected transient int mask; + /** Whether this set contains the key zero. */ + protected transient boolean containsNullKey; + /** The current table size. */ + protected transient int arraySize; + /** + * Threshold after which we rehash. It must be the table size times {@link #loadFactor}. + */ + protected transient int maxFill; + /** Number of entries in the set (including the key zero, if present). */ + protected int size; + /** The acceptable load factor. */ + protected final float loadFactor; + + private V defRetValue = null; + + /** + * Creates a new hash map with initial expected + * {@link #DEFAULT_INITIAL_SIZE} entries and + * {@link #DEFAULT_LOAD_FACTOR} as load factor. + */ + public OpenHashMapContextData() { + this(DEFAULT_INITIAL_SIZE, DEFAULT_LOAD_FACTOR); + } + /** + * Creates a new hash map with {@link #DEFAULT_LOAD_FACTOR} as load factor. + * + * @param expected + * the expected number of elements in the hash map. + */ + public OpenHashMapContextData(final int expected) { + this(expected, DEFAULT_LOAD_FACTOR); + } + /** + * Creates a new hash map. + * + * <p> + * The actual table size will be the least power of two greater than + * <code>expected</code>/<code>f</code>. + * + * @param expected + * the expected number of elements in the hash set. + * @param f + * the load factor. + */ + @SuppressWarnings("unchecked") + public OpenHashMapContextData(final int expected, final float f) { + if (f <= 0 || f > 1) { + throw new IllegalArgumentException( + "Load factor must be greater than 0 and smaller than or equal to 1"); + } + if (expected < 0){ + throw new IllegalArgumentException( + "The expected number of elements must be nonnegative"); + } + this.loadFactor = f; + arraySize = HashCommon.arraySize(expected, f); + mask = arraySize - 1; + maxFill = HashCommon.maxFill(arraySize, f); + keys = (K[]) new Object[arraySize + 1]; + values = (V[]) new Object[arraySize + 1]; + } + /** + * Creates a new hash map with {@link #DEFAULT_LOAD_FACTOR} as load + * factor copying a given one. + * + * @param map + * a {@link Map} to be copied into the new hash map. + */ + public OpenHashMapContextData(final Map<? extends K, ? extends V> map) { + this(map, DEFAULT_LOAD_FACTOR); + } + /** + * Creates a new hash map copying a given one. + * + * @param map + * a {@link Map} to be copied into the new hash map. + * @param f + * the load factor. + */ + public OpenHashMapContextData(final Map<? extends K, ? extends V> map, final float f) { + this(map.size(), f); + putAll(map); + } + + /** + * Creates a new hash map with {@link #DEFAULT_LOAD_FACTOR} as load + * factor copying a given type-specific one. + * + * @param contextData + * a type-specific map to be copied into the new hash map. + */ + public OpenHashMapContextData(final ContextData contextData) { + this(contextData, DEFAULT_LOAD_FACTOR); + } + /** + * Creates a new hash map copying a given type-specific one. + * + * @param contextData + * a type-specific map to be copied into the new hash map. + * @param f + * the load factor. + */ + public OpenHashMapContextData(final ContextData contextData, final float f) { + this(contextData.size(), f); + if (contextData instanceof OpenHashMapContextData) { + initFrom0((OpenHashMapContextData) contextData); + } else { + contextData.forEach(PUT_ALL, this); + } + } + private static final TriConsumer<String, Object, MutableContextData> PUT_ALL = + new TriConsumer<String, Object, MutableContextData>() { + @Override + public void accept(final String key, final Object value, final MutableContextData contextData) { + contextData.putValue(key, value); + } + }; + + @SuppressWarnings("unchecked") + private void initFrom0(final OpenHashMapContextData other) { + // this.loadFactor = other.loadFactor; // final field + this.arraySize = other.arraySize; + this.size = other.size; + this.containsNullKey = other.containsNullKey; + this.mask = other.mask; + this.maxFill = other.maxFill; + keys = (K[]) Arrays.copyOf(other.keys, arraySize + 1); + values = (V[]) Arrays.copyOf(other.values, arraySize + 1); + } + + private int realSize() { + return containsNullKey ? size - 1 : size; + } + + private void ensureCapacity(final int capacity) { + final int needed = HashCommon.arraySize(capacity, loadFactor); + if (needed > arraySize) { + rehash(needed); + } + } + + private void tryCapacity(final long capacity) { + final int needed = (int) Math.min( + 1 << 30, Math.max(2, HashCommon.nextPowerOfTwo((int) Math.ceil(capacity / loadFactor)))); + if (needed > arraySize) { + rehash(needed); + } + } + + /** {@inheritDoc} */ + public void putAll(Map<? extends K, ? extends V> map) { + if (loadFactor <= .5) { + // The resulting map will be sized for m.size() elements + ensureCapacity(map.size()); + } else { + // The resulting map will be tentatively sized for size() + m.size() elements + tryCapacity(size() + map.size()); + } + for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) { + putObjectValue(entry.getKey(), entry.getValue()); + } + } + + @Override + public Map<String, String> asMap() { + final Map<String, String> result = new HashMap<>(size); + forEach(COPY_INTO_MAP, result); + return result; + } + + private static final TriConsumer<String, Object, Map<String, String>> COPY_INTO_MAP = + new TriConsumer<String, Object, Map<String, String>>() { + @Override + public void accept(final String k, final Object v, final Map<String, String> map) { + map.put(k, v == null ? null : v.toString()); + } + }; + + /* + * Removes all elements from this map. + * + * <P>To increase object reuse, this method does not change the table size. + * If you want to reduce the table size, you must use {@link #trim()}. + */ + @Override + public void clear() { + if (size == 0) { + return; + } + size = 0; + containsNullKey = false; + Arrays.fill(keys, (null)); + Arrays.fill(values, null); + } + + @Override + public boolean containsKey(final String key) { + return containsObjectKey((Object) key); + } + + @SuppressWarnings("unchecked") + private boolean containsObjectKey(final Object k) { + if (k == null) { + return containsNullKey; + } + K curr; + final K[] key = this.keys; + int pos; + // The starting point. + if ((curr = key[pos = HashCommon.mix(k.hashCode()) & mask]) == null) { + return false; + } + if (k.equals(curr)) { + return true; + } + // There's always an unused entry. + while (true) { + if ((curr = key[pos = (pos + 1) & mask]) == null) { + return false; + } + if (k.equals(curr)) { + return true; + } + } + } + + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ContextData)) { + return false; + } + final ContextData other = (ContextData) obj; + if (other.size() != size()) { + return false; + } + int pos = arraySize; + if (containsNullKey) { + if (!Objects.equals(getObjectValue(null), other.getValue(null))) { + return false; + } + } + --pos; + final K myKeys[] = this.keys; + for (; pos >= 0; pos--) { + K k; + if ((k = myKeys[pos]) != null) { + if (!Objects.equals(values[pos], other.getValue((String) k))) { + return false; + } + } + } + return true; + } + + @Override + @SuppressWarnings("unchecked") + public <VAL> void forEach(final BiConsumer<String, ? super VAL> action) { + final int startSize = size; + final K myKeys[] = this.keys; + int pos = arraySize; + if (containsNullKey) { + action.accept((String) myKeys[pos], (VAL) values[pos]); + if (size != startSize) { + throw new ConcurrentModificationException(); + } + } + --pos; + for (; pos >= 0; pos--) { + if (myKeys[pos] != null) { + action.accept((String) myKeys[pos], (VAL) values[pos]); + if (size != startSize) { + throw new ConcurrentModificationException(); + } + } + } + } + + @Override + @SuppressWarnings("unchecked") + public <VAL, STATE> void forEach(final TriConsumer<String, ? super VAL, STATE> action, final STATE state) { + final int startSize = size; + final K myKeys[] = this.keys; + int pos = arraySize; + if (containsNullKey) { + action.accept((String) myKeys[pos], (VAL) values[pos], state); + if (size != startSize) { + throw new ConcurrentModificationException(); + } + } + --pos; + for (; pos >= 0; pos--) { + if (myKeys[pos] != null) { + action.accept((String) myKeys[pos], (VAL) values[pos], state); + if (size != startSize) { + throw new ConcurrentModificationException(); + } + } + } + } + + @Override + public String get(final String key) { + return (String) getObjectValue(key); + } + + @SuppressWarnings("unchecked") + private V getObjectValue(final Object k) { + if (k == null) { + return containsNullKey ? values[arraySize] : defRetValue; + } + K curr; + final K[] key = this.keys; + int pos; + // The starting point. + if ((curr = key[pos = HashCommon.mix(k.hashCode()) & mask]) == null) { + return defRetValue; + } + if (k.equals(curr)) { + return values[pos]; + } + // There's always an unused entry. + while (true) { + if (((curr = key[pos = (pos + 1) & mask]) == null)) { + return defRetValue; + } + if (k.equals(curr)) { + return values[pos]; + } + } + } + + @Override + public Map<String, String> getCopy() { + return asMap(); + } + + @Override + public Map<String, String> getImmutableMapOrNull() { + return isEmpty() ? null : Collections.unmodifiableMap(asMap()); + } + + @Override + public <VAL> VAL getValue(final String key) { + return (VAL) getObjectValue((Object) key); + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + @SuppressWarnings("unchecked") + public void put(final String key, final String value) { + putObjectValue((K) key, (V) value); + } + + private int insert(final K k, final V v) { + int pos; + if (k == null) { + if (containsNullKey) { + return arraySize; + } + containsNullKey = true; + pos = arraySize; + } else { + K curr; + final K[] key = this.keys; + // The starting point. + if (!((curr = key[pos = HashCommon.mix(k.hashCode()) & mask]) == null)) { + if (curr.equals(k)) { + return pos; + } + while (!((curr = key[pos = (pos + 1) & mask]) == null)) { + if (curr.equals(k)) { + return pos; + } + } + } + } + keys[pos] = k; + values[pos] = v; + if (size++ >= maxFill) { + rehash(HashCommon.arraySize(size + 1, loadFactor)); + } + return -1; + } + + @Override + public void putAll(final ContextData source) { + if (size() == 0 && source instanceof OpenHashMapContextData) { + initFrom0((OpenHashMapContextData) source); + } else if (source != null) { + source.forEach(PUT_ALL, this); + } + } + + private V putObjectValue(final K k, final V v) { + final int pos = insert(k, v); + if (pos < 0) { + return defRetValue; + } + final V oldValue = values[pos]; + values[pos] = v; + return oldValue; + } + + @Override + @SuppressWarnings("unchecked") + public void putValue(final String key, final Object value) { + putObjectValue((K) key, (V) value); + } + + @Override + public void remove(final String key) { + removeObjectKey((Object) key); + } + + @SuppressWarnings("unchecked") + private V removeObjectKey(final Object k) { + if (k == null) { + if (containsNullKey) { + return removeNullEntry(); + } + return defRetValue; + } + final K[] key = this.keys; + int pos = HashCommon.mix(k.hashCode()) & mask; + K curr = key[pos & mask]; + // The starting point. + if (curr == null) { + return defRetValue; + } + if (k.equals(curr)) { + return removeEntry(pos); + } + while (true) { + if ((curr = key[pos = (pos + 1) & mask]) == null) { + return defRetValue; + } + if (k.equals(curr)) { + return removeEntry(pos); + } + } + } + private V removeEntry(final int pos) { + final V oldValue = values[pos]; + values[pos] = null; + size--; + shiftKeys(pos); + if (size < maxFill / 4 && arraySize > DEFAULT_INITIAL_SIZE) { + rehash(arraySize / 2); + } + return oldValue; + } + private V removeNullEntry() { + containsNullKey = false; + keys[arraySize] = null; + final V oldValue = values[arraySize]; + values[arraySize] = null; + size--; + if (size < maxFill / 4 && arraySize > DEFAULT_INITIAL_SIZE) { + rehash(arraySize / 2); + } + return oldValue; + } + /** + * Shifts left entries with the specified hash code, starting at the + * specified position, and empties the resulting free entry. + * + * @param pos + * a starting position. + */ + private void shiftKeys(int pos) { + // Shift entries with the same hash. + int last, slot; + K curr; + final K[] myKeys = this.keys; + for (;;) { + pos = ((last = pos) + 1) & mask; + for (;;) { + if (((curr = myKeys[pos]) == null)) { + myKeys[last] = (null); + values[last] = null; + return; + } + slot = HashCommon.mix(curr.hashCode()) & mask; + if (last <= pos ? (last >= slot || slot > pos) : (last >= slot && slot > pos)) { + break; + } + pos = (pos + 1) & mask; + } + myKeys[last] = curr; + values[last] = values[pos]; + } + } + + @Override + public int size() { + return size; + } + + /** + * Rehashes this map if the table is too large. + * + * <P> + * Let <var>N</var> be the smallest table size that can hold + * <code>max(n,{@link #size()})</code> entries, still satisfying the load + * factor. If the current table size is smaller than or equal to + * <var>N</var>, this method does nothing. Otherwise, it rehashes this map + * in a table of size <var>N</var>. + * + * <P> + * This method is useful when reusing maps. {@linkplain #clear() Clearing a + * map} leaves the table size untouched. If you are reusing a map many times, + * you can call this method with a typical size to avoid keeping around a + * very large table just because of a few large transient maps. + * + * @param n + * the threshold for the trimming. + * @return true if there was enough memory to trim the map. + */ + public boolean trim(final int n) { + final int l = HashCommon.nextPowerOfTwo((int) Math.ceil(n / loadFactor)); + if (l >= n || size > HashCommon.maxFill(l, loadFactor)) + return true; + try { + rehash(l); + } catch (OutOfMemoryError cantDoIt) { // unusual to catch OOME but in this case appropriate + return false; + } + return true; + } + /** + * Rehashes the map. + * + * <P> + * This method implements the basic rehashing strategy, and may be overriden + * by subclasses implementing different rehashing strategies (e.g., + * disk-based rehashing). However, you should not override this method + * unless you understand the internal workings of this class. + * + * @param newN + * the new size + */ + @SuppressWarnings("unchecked") + protected void rehash(final int newN) { + final K myKeys[] = this.keys; + final V myValues[] = this.values; + final int mask = newN - 1; // Note that this is used by the hashing + // macro + final K newKey[] = (K[]) new Object[newN + 1]; + final V newValue[] = (V[]) new Object[newN + 1]; + int i = arraySize, pos; + for (int j = realSize(); j-- != 0;) { + while (myKeys[--i] == null) { + // advance i until we find an existing key + } + if (newKey[pos = HashCommon.mix(myKeys[i].hashCode()) & mask] != null) { // rehash & check slot availability + while (newKey[pos = (pos + 1) & mask] != null) { + // find available slot at (or immediately following) pos + } + } + newKey[pos] = myKeys[i]; + newValue[pos] = myValues[i]; + } + newValue[newN] = myValues[arraySize]; + arraySize = newN; + this.mask = mask; + maxFill = HashCommon.maxFill(arraySize, loadFactor); + this.keys = newKey; + this.values = newValue; + } + + /** + * Returns a hash code for this map. + * + * @return a hash code for this map. + */ + public int hashCode() { + int result = 0; + for (int j = realSize(), i = 0, t = 0; j-- != 0;) { + while (keys[i] == null) { + i++; + } + if (this != keys[i]) { + t = keys[i].hashCode(); + } + if (this != values[i]) { + t ^= (values[i] == null ? 0 : values[i].hashCode()); + } + result += t; + i++; + } + // Zero / null keys have hash zero. + if (containsNullKey) { + result += (values[arraySize] == null ? 0 : values[arraySize].hashCode()); + } + return result; + } + + @SuppressWarnings("unchecked") + private void readObject(final ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + arraySize = HashCommon.arraySize(size, loadFactor); + maxFill = HashCommon.maxFill(arraySize, loadFactor); + mask = arraySize - 1; + final K key[] = this.keys = (K[]) new Object[arraySize + 1]; + final V value[] = this.values = (V[]) new Object[arraySize + 1]; + K k; + V v; + for (int i = size, pos; i-- != 0;) { + k = (K) s.readObject(); + v = (V) s.readObject(); + if (k == null) { + pos = arraySize; + containsNullKey = true; + } else { + pos = HashCommon.mix(k.hashCode()) & mask; + while (key[pos] != null) { + pos = (pos + 1) & mask; + } + } + key[pos] = k; + value[pos] = v; + } + } + + private void writeObject(final ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + try { + forEach(SERIALIZER, s); + } catch (final RuntimeException runex) { + if (runex.getCause() instanceof IOException) { + throw (IOException) runex.getCause(); + } + throw runex; + } + } + + private static final TriConsumer<String, Object, ObjectOutputStream> SERIALIZER = + new TriConsumer<String, Object, ObjectOutputStream>() { + @Override + public void accept(final String k, final Object v, final ObjectOutputStream objectOutputStream) { + try { + objectOutputStream.writeObject(k); + objectOutputStream.writeObject(v); + } catch (final IOException ioex) { + throw new IllegalStateException(ioex); + } + } + }; + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(256); + sb.append('{'); + final K myKeys[] = this.keys; + int pos = arraySize; + boolean first = true; + if (containsNullKey) { + sb.append(myKeys[pos] == this ? "(this map)" : myKeys[pos]); + sb.append('='); + sb.append(values[pos] == this ? "(this map)" : values[pos]); + first = false; + } + --pos; + for (; pos >= 0; pos--) { + if (myKeys[pos] != null) { + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append(myKeys[pos] == this ? "(this map)" : myKeys[pos]); + sb.append('='); + sb.append(values[pos] == this ? "(this map)" : values[pos]); + } + } + sb.append('}'); + return sb.toString(); + } + + private static class HashCommon { + private HashCommon() {} + + /** 2<sup>32</sup> · φ, φ = (√5 − 1)/2. */ + private static final int INT_PHI = 0x9E3779B9; + + /** The reciprocal of {@link #INT_PHI} modulo 2<sup>32</sup>. */ + private static final int INV_INT_PHI = 0x144cbc89; + + /** Avalanches the bits of an integer by applying the finalisation step of MurmurHash3. + * + * <p>This method implements the finalisation step of Austin Appleby's + * <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>. + * Its purpose is to avalanche the bits of the argument to within 0.25% bias. + * + * @param x an integer. + * @return a hash value with good avalanching properties. + */ + public static int murmurHash3(int x) { + x ^= x >>> 16; + x *= 0x85ebca6b; + x ^= x >>> 13; + x *= 0xc2b2ae35; + x ^= x >>> 16; + return x; + } + + /** + * Quickly mixes the bits of an integer. + * + * <p>This method mixes the bits of the argument by multiplying by the golden ratio and + * xorshifting the result. It is borrowed from <a href="https://github.com/OpenHFT/Koloboke">Koloboke</a>, and + * it has slightly worse behaviour than {@link #murmurHash3(int)} (in open-addressing hash tables the average + * number of probes is slightly larger), but it's much faster. + * + * @param x an integer. + * @return a hash value obtained by mixing the bits of {@code x}. + * @see #invMix(int) + */ + public static int mix(final int x) { + final int h = x * INT_PHI; + return h ^ (h >>> 16); + } + + /** The inverse of {@link #mix(int)}. This method is mainly useful to create unit tests. + * + * @param x an integer. + * @return a value that passed through {@link #mix(int)} would give {@code x}. + */ + public static int invMix(final int x) { + return (x ^ x >>> 16) * INV_INT_PHI; + } + + /** Return the least power of two greater than or equal to the specified value. + * + * <p>Note that this function will return 1 when the argument is 0. + * + * @param x an integer smaller than or equal to 2<sup>30</sup>. + * @return the least power of two greater than or equal to the specified value. + */ + public static int nextPowerOfTwo(int x) { + if (x == 0) { + return 1; + } + x--; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + return (x | x >> 16) + 1; + } + + /** Return the least power of two greater than or equal to the specified value. + * + * <p>Note that this function will return 1 when the argument is 0. + * + * @param x a long integer smaller than or equal to 2<sup>62</sup>. + * @return the least power of two greater than or equal to the specified value. + */ + public static long nextPowerOfTwo(long x) { + if (x == 0) { + return 1; + } + x--; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + return (x | x >> 32) + 1; + } + + + /** Returns the maximum number of entries that can be filled before rehashing. + * + * @param n the size of the backing array. + * @param f the load factor. + * @return the maximum number of entries before rehashing. + */ + public static int maxFill(final int n, final float f) { + /* We must guarantee that there is always at least + * one free entry (even with pathological load factors). */ + return Math.min((int) Math.ceil(n * f), n - 1); + } + + /** + * Returns the least power of two smaller than or equal to 2<sup>30</sup> and larger than or equal to + * <code>Math.ceil( expected / f )</code>. + * + * @param expected the expected number of elements in a hash table. + * @param f the load factor. + * @return the minimum possible size for a backing array. + * @throws IllegalArgumentException if the necessary size is larger than 2<sup>30</sup>. + */ + public static int arraySize(final int expected, final float f) { + final long result = Math.max(2, nextPowerOfTwo((long) Math.ceil(expected / f))); + if (result > (1 << 30)) { + throw new IllegalArgumentException("Too large (" + expected + + " expected elements with load factor " + f + ")"); + } + return (int) result; + } + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/spi/SortedArrayThreadContext.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/SortedArrayThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/SortedArrayThreadContext.java new file mode 100644 index 0000000..f3438c5 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/SortedArrayThreadContext.java @@ -0,0 +1,156 @@ +/* + * 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.logging.log4j.spi; + +import java.util.Collections; +import java.util.Map; + +import org.apache.logging.log4j.util.PropertiesUtil; + +/** + * Garbage-free ThreadContextMap implementation backed by {@code SortedArrayContextData}. + */ +public class SortedArrayThreadContext implements ThreadContextMap { + /** + * Property name ({@value} ) for selecting {@code InheritableThreadLocal} (value "true") or plain + * {@code ThreadLocal} (value is not "true") in the implementation. + */ + public static final String INHERITABLE_MAP = "isThreadContextMapInheritable"; + + private final ThreadLocal<ArrayContextData> localMap; + + public SortedArrayThreadContext() { + this.localMap = createThreadLocalMap(); + } + + // LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured. + // (This method is package protected for JUnit tests.) + static ThreadLocal<ArrayContextData> createThreadLocalMap() { + final PropertiesUtil managerProps = PropertiesUtil.getProperties(); + final boolean inheritable = managerProps.getBooleanProperty(INHERITABLE_MAP); + if (inheritable) { + return new InheritableThreadLocal<ArrayContextData>() { + @Override + protected ArrayContextData childValue(final ArrayContextData parentValue) { + return parentValue != null ? new ArrayContextData(parentValue) : null; + } + }; + } + // if not inheritable, return plain ThreadLocal with null as initial value + return new ThreadLocal<>(); + } + + private ArrayContextData getThreadLocalMap() { + ArrayContextData map = localMap.get(); + if (map == null) { + map = new ArrayContextData(); + localMap.set(map); + } + return map; + } + + @Override + public void put(final String key, final String value) { + getThreadLocalMap().put(key, value); + } + + @Override + public String get(final String key) { + final ArrayContextData map = localMap.get(); + return map == null ? null : map.get(key); + } + + @Override + public void remove(final String key) { + final ArrayContextData map = localMap.get(); + if (map != null) { + map.remove(key); + } + } + + @Override + public void clear() { + localMap.remove(); + } + + @Override + public boolean containsKey(final String key) { + final ArrayContextData map = localMap.get(); + return map != null && map.containsKey(key); + } + + @Override + public Map<String, String> getCopy() { + final ArrayContextData map = localMap.get(); + return map == null ? Collections.<String, String>emptyMap() : map.asMap(); + } + + public ContextData getContextData() { + return localMap.get(); + } + + @Override + public Map<String, String> getImmutableMapOrNull() { + final ArrayContextData map = localMap.get(); + return map == null ? null : Collections.unmodifiableMap(map.asMap()); + } + + @Override + public boolean isEmpty() { + final ArrayContextData map = localMap.get(); + return map == null || map.size() == 0; + } + + @Override + public String toString() { + final ArrayContextData map = localMap.get(); + return map == null ? "{}" : map.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + final ArrayContextData map = this.localMap.get(); + result = prime * result + ((map == null) ? 0 : map.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ThreadContextMap)) { + return false; + } + final ThreadContextMap other = (ThreadContextMap) obj; + final Map<String, String> map = this.getImmutableMapOrNull(); + final Map<String, String> otherMap = other.getImmutableMapOrNull(); + if (map == null) { + if (otherMap != null) { + return false; + } + } else if (!map.equals(otherMap)) { + return false; + } + return true; + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/util/BiConsumer.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/BiConsumer.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/BiConsumer.java new file mode 100644 index 0000000..3dad0e4 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/BiConsumer.java @@ -0,0 +1,19 @@ +package org.apache.logging.log4j.util; + +/** + * An operation that accepts two input arguments and returns no result. + * + * @param <K> type of the first argument + * @param <V> type of the second argument + * @see org.apache.logging.log4j.core.ContextData + * @since 2.7 + */ +public interface BiConsumer<K, V> { + + /** + * Performs the operation given the specified arguments. + * @param k the first input argument + * @param v the second input argument + */ + void accept(K k, V v); +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/1118b27f/log4j-api/src/main/java/org/apache/logging/log4j/util/TriConsumer.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/TriConsumer.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/TriConsumer.java new file mode 100644 index 0000000..63960be --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/TriConsumer.java @@ -0,0 +1,21 @@ +package org.apache.logging.log4j.util; + +/** + * An operation that accepts three input arguments and returns no result. + * + * @param <K> type of the first argument + * @param <V> type of the second argument + * @param <S> type of the third argument + * @see org.apache.logging.log4j.core.ContextData + * @since 2.7 + */ +public interface TriConsumer<K, V, S> { + + /** + * Performs the operation given the specified arguments. + * @param k the first input argument + * @param v the second input argument + * @param s the third input argument + */ + void accept(K k, V v, S s); +}
