From b3910b78ad5f44eaef6ddc17371112c41fb49a8c Mon Sep 17 00:00:00 2001 From: John Engebretson Date: Wed, 28 Feb 2024 14:14:55 +0000 Subject: [PATCH 1/5] Creating faster ThreadContextMap based on a String[] in the ThreadLocal --- .../spi/StringArrayThreadContextMapTest.java | 265 ++++++++++ .../spi/UnmodifiableArrayBackedMapTest.java | 308 +++++++++++ .../apache/logging/log4j/ThreadContext.java | 9 +- .../spi/StringArrayThreadContextMap.java | 253 +++++++++ .../log4j/spi/ThreadContextMapFactory.java | 1 + .../log4j/spi/UnmodifiableArrayBackedMap.java | 479 ++++++++++++++++++ .../logging/log4j/spi/package-info.java | 2 +- pom.xml | 2 +- 8 files changed, 1315 insertions(+), 4 deletions(-) create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java new file mode 100644 index 00000000000..601927bf6c9 --- /dev/null +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java @@ -0,0 +1,265 @@ +/* + * 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 static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import org.apache.logging.log4j.test.junit.InitializesThreadContext; +import org.apache.logging.log4j.test.junit.SetTestProperty; +import org.apache.logging.log4j.test.junit.UsingThreadContextMap; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearSystemProperty; + +/** + * Tests the {@code StringArrayThreadContextMap} class. + */ +@UsingThreadContextMap +public class StringArrayThreadContextMapTest { + + @Test + public void testEqualsVsSameKind() { + final StringArrayThreadContextMap map1 = createMap(); + final StringArrayThreadContextMap map2 = createMap(); + assertEquals(map1, map1); + assertEquals(map2, map2); + assertEquals(map1, map2); + assertEquals(map2, map1); + } + + @Test + public void testHashCodeVsSameKind() { + final StringArrayThreadContextMap map1 = createMap(); + final StringArrayThreadContextMap map2 = createMap(); + assertEquals(map1.hashCode(), map2.hashCode()); + } + + @Test + public void testGet() { + final StringArrayThreadContextMap map1 = createMap(); + assertEquals(null, map1.get("test")); + map1.put("test", "test"); + assertEquals("test", map1.get("test")); + assertEquals(null, map1.get("not_present")); + } + + @Test + public void testDoesNothingIfConstructedWithUseMapIsFalse() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(false); + assertTrue(map.isEmpty()); + assertFalse(map.containsKey("key")); + map.put("key", "value"); + + assertTrue(map.isEmpty()); + assertFalse(map.containsKey("key")); + assertNull(map.get("key")); + } + + @Test + public void testPut() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + assertTrue(map.isEmpty()); + assertFalse(map.containsKey("key")); + map.put("key", "value"); + + assertFalse(map.isEmpty()); + assertTrue(map.containsKey("key")); + assertEquals("value", map.get("key")); + } + + @Test + public void testPutAll() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + assertTrue(map.isEmpty()); + assertFalse(map.containsKey("key")); + final int mapSize = 10; + final Map newMap = new HashMap<>(mapSize); + for (int i = 1; i <= mapSize; i++) { + newMap.put("key" + i, "value" + i); + } + map.putAll(newMap); + assertFalse(map.isEmpty()); + for (int i = 1; i <= mapSize; i++) { + assertTrue(map.containsKey("key" + i)); + assertEquals("value" + i, map.get("key" + i)); + } + } + + /** + * Test method for + * {@link org.apache.logging.log4j.spi.StringArrayThreadContextMap#remove(java.lang.String)} + * . + */ + @Test + public void testRemove() { + final StringArrayThreadContextMap map = createMap(); + assertEquals("value", map.get("key")); + assertEquals("value2", map.get("key2")); + + map.remove("key"); + assertFalse(map.containsKey("key")); + assertEquals("value2", map.get("key2")); + } + + @Test + public void testRemoveAll() { + final StringArrayThreadContextMap map = createMap(); + + Map newValues = new HashMap<>(); + newValues.put("1", "value1"); + newValues.put("2", "value2"); + + map.putAll(newValues); + map.removeAll(newValues.keySet()); + + map.put("3", "value3"); + } + + @Test + public void testClear() { + final StringArrayThreadContextMap map = createMap(); + + map.clear(); + assertTrue(map.isEmpty()); + assertFalse(map.containsKey("key")); + assertFalse(map.containsKey("key2")); + } + + /** + * @return + */ + private StringArrayThreadContextMap createMap() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + assertTrue(map.isEmpty()); + map.put("key", "value"); + map.put("key2", "value2"); + assertEquals("value", map.get("key")); + assertEquals("value2", map.get("key2")); + return map; + } + + @Test + public void testGetCopyReturnsMutableMap() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + assertTrue(map.isEmpty()); + final Map copy = map.getCopy(); + assertTrue(copy.isEmpty()); + + copy.put("key", "value"); // mutable + assertEquals("value", copy.get("key")); + + // thread context map not affected + assertTrue(map.isEmpty()); + } + + @Test + public void testGetCopyReturnsMutableCopy() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + map.put("key1", "value1"); + assertFalse(map.isEmpty()); + final Map copy = map.getCopy(); + assertEquals("value1", copy.get("key1")); // copy has values too + + copy.put("key", "value"); // copy is mutable + assertEquals("value", copy.get("key")); + + // thread context map not affected + assertFalse(map.containsKey("key")); + + // clearing context map does not affect copy + map.clear(); + assertTrue(map.isEmpty()); + + assertFalse(copy.isEmpty()); + } + + @Test + public void testGetImmutableMapReturnsNullIfEmpty() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + assertTrue(map.isEmpty()); + assertNull(map.getImmutableMapOrNull()); + } + + @Test + public void testGetImmutableMapReturnsImmutableMapIfNonEmpty() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + map.put("key1", "value1"); + assertFalse(map.isEmpty()); + + final Map immutable = map.getImmutableMapOrNull(); + assertEquals("value1", immutable.get("key1")); // copy has values too + + // immutable + assertThrows(UnsupportedOperationException.class, () -> immutable.put("key", "value")); + } + + @Test + public void testGetImmutableMapCopyNotAffectdByContextMapChanges() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + map.put("key1", "value1"); + assertFalse(map.isEmpty()); + + final Map immutable = map.getImmutableMapOrNull(); + assertEquals("value1", immutable.get("key1")); // copy has values too + + // clearing context map does not affect copy + map.clear(); + assertTrue(map.isEmpty()); + + assertFalse(immutable.isEmpty()); + } + + @Test + public void testToStringShowsMapContext() { + final StringArrayThreadContextMap map = new StringArrayThreadContextMap(true); + assertEquals("{}", map.toString()); + + map.put("key1", "value1"); + assertEquals("{key1=value1}", map.toString()); + + map.remove("key1"); + map.put("key2", "value2"); + assertEquals("{key2=value2}", map.toString()); + } + + @Test + @ClearSystemProperty(key = StringArrayThreadContextMap.INHERITABLE_MAP) + @InitializesThreadContext + public void testThreadLocalNotInheritableByDefault() { + ThreadContextMapFactory.init(); + final ThreadLocal threadLocal = StringArrayThreadContextMap.createThreadLocalMap(true); + assertFalse(threadLocal instanceof InheritableThreadLocal); + } + + @Test + @SetTestProperty(key = StringArrayThreadContextMap.INHERITABLE_MAP, value = "true") + @InitializesThreadContext + public void testThreadLocalInheritableIfConfigured() { + ThreadContextMapFactory.init(); + try { + final ThreadLocal threadLocal = StringArrayThreadContextMap.createThreadLocalMap(true); + assertTrue(threadLocal instanceof InheritableThreadLocal); + } finally { + System.clearProperty(StringArrayThreadContextMap.INHERITABLE_MAP); + } + } +} diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java new file mode 100644 index 00000000000..af8f2f85be6 --- /dev/null +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java @@ -0,0 +1,308 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; + +public class UnmodifiableArrayBackedMapTest { + private static final int TEST_DATA_SIZE = 5; + + private HashMap getTestParameters() { + return getTestParameters(TEST_DATA_SIZE); + } + + private HashMap getTestParameters(int numParams) { + HashMap params = new LinkedHashMap<>(); + for (int i = 0; i < numParams; i++) { + params.put("" + i, "value" + i); + } + + return params; + } + + @Test + public void testReads() { + assertEquals(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test"), null); + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + assertTrue(testMap.containsKey(key)); + assertTrue(testMap.containsValue(value)); + assertEquals(testMap.get(key), params.get(key)); + } + assertFalse(testMap.containsKey("not_present")); + assertFalse(testMap.containsValue("not_present")); + assertEquals(null, testMap.get("not_present")); + } + + @Test + public void testCopyAndRemoveAll() { + HashMap initialMapContents = getTestParameters(15); + initialMapContents.put("extra_key", "extra_value"); + + HashSet keysToRemove = new LinkedHashSet<>(); + keysToRemove.add("3"); + keysToRemove.add("11"); + keysToRemove.add("definitely_not_found"); + + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(initialMapContents); + testMap = testMap.copyAndRemoveAll(keysToRemove); + assertEquals(14, testMap.size()); + + assertFalse(testMap.containsKey("3")); + assertFalse(testMap.containsValue("value3")); + assertFalse(testMap.containsKey("11")); + assertFalse(testMap.containsValue("value11")); + + assertTrue(testMap.containsKey("extra_key")); + assertTrue(testMap.containsValue("extra_value")); + assertTrue(testMap.containsKey("1")); + assertTrue(testMap.containsValue("value1")); + assertTrue(testMap.containsKey("0")); + assertTrue(testMap.containsValue("value0")); + assertTrue(testMap.containsKey("14")); + assertTrue(testMap.containsValue("value14")); + + testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(initialMapContents); + UnmodifiableArrayBackedMap testMapWithArrayListRemoval = + testMap.copyAndRemoveAll(new ArrayList<>(keysToRemove)); + UnmodifiableArrayBackedMap testMapWithSetRemoval = testMap.copyAndRemoveAll(keysToRemove); + assertEquals(testMapWithSetRemoval, testMapWithArrayListRemoval); + + testMap = UnmodifiableArrayBackedMap.EMPTY_MAP; + assertEquals(testMap.copyAndRemoveAll(initialMapContents.keySet()).size(), 0); + + testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPut("test", "test"); + assertEquals(testMap.copyAndRemoveAll(initialMapContents.keySet()).size(), 1); + testMap = testMap.copyAndRemoveAll(Collections.singleton("not found")); + assertEquals(testMap.copyAndRemoveAll(testMap.keySet()).size(), 0); + testMap = testMap.copyAndRemoveAll(Collections.singleton("test")); + assertEquals(testMap.copyAndRemoveAll(testMap.keySet()).size(), 0); + } + + @Test + public void testCopyAndPut() { + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP; + testMap = testMap.copyAndPut("1", "value1"); + assertTrue(testMap.containsKey("1")); + assertEquals(testMap.get("1"), "value1"); + + testMap = testMap.copyAndPut("1", "another value"); + assertTrue(testMap.containsKey("1")); + assertEquals(testMap.get("1"), "another value"); + + HashMap newValues = getTestParameters(); + testMap = testMap.copyAndPutAll(newValues); + assertEquals(testMap.get("1"), "value1"); + assertEquals(testMap.get("4"), "value4"); + } + + @Test + public void testInstanceCopy() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + + UnmodifiableArrayBackedMap testMap2 = new UnmodifiableArrayBackedMap(testMap); + assertEquals(testMap, testMap2); + } + + @Test + public void testEntrySetIteratorAndSize() { + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + Set> entrySet = testMap.entrySet(); + int numEntriesFound = 0; + for (@SuppressWarnings("unused") Map.Entry entry : entrySet) { + numEntriesFound++; + } + + assertEquals(testMap.size(), numEntriesFound); + assertEquals(testMap.size(), entrySet.size()); + } + + @Test + public void testEntrySetMutatorsBlocked() { + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + Set> entrySet = testMap.entrySet(); + for (Map.Entry entry : entrySet) { + try { + entry.setValue("test"); + fail("Entry.setValue() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + } + for (@SuppressWarnings("unused") Map.Entry entry : entrySet) { + try { + entrySet.add(null); + fail("EntrySet.add() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + } + for (@SuppressWarnings("unused") Map.Entry entry : entrySet) { + try { + entrySet.addAll(new HashSet<>()); + fail("EntrySet.addAll() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + } + } + + @Test + public void testNullValue() { + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP; + testMap = testMap.copyAndPut("key", null); + assertTrue(testMap.containsKey("key")); + assertTrue(testMap.containsValue(null)); + assertTrue(testMap.size() == 1); + assertEquals(testMap.get("key"), null); + } + + @Test + public void testMutatorsBlocked() { + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + try { + testMap.put("a", "a"); + fail("put() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + + try { + testMap.putAll(new HashMap<>()); + fail("putAll() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + + try { + testMap.remove("1"); + fail("remove() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + + try { + testMap.clear(); + fail("clear() wasn't blocked"); + } catch (UnsupportedOperationException e) { + } + } + + @Test + public void testCopyAndRemove() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + testMap = testMap.copyAndRemove("2"); + testMap = testMap.copyAndRemove("not_present"); + assertEquals(4, testMap.size()); + assertFalse(testMap.containsKey("2")); + assertTrue(testMap.containsKey("1")); + assertFalse(testMap.containsValue("value2")); + } + + /** + * Tests various situations with .equals(). Test tries comparisons in both + * directions, to make sure that HashMap.equals(UnmodifiableArrayBackedMap) work + * as well as UnmodifiableArrayBackedMap.equals(HashMap). + */ + @Test + public void testEqualsHashCodeWithIdenticalContent() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + assertEquals(params, testMap); + assertEquals(testMap, params); + assertEquals(params.hashCode(), testMap.hashCode()); + } + + @Test + public void testEqualsWhenOneValueDiffers() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + assertNotEquals(params, testMap.copyAndPut("1", "different value")); + assertNotEquals(testMap.copyAndPut("1", "different value"), params); + } + + @Test + public void testEqualsHashCodeWithOneKeyRemoved() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + + params.remove("1"); + assertNotEquals(params, testMap); + assertNotEquals(testMap, params); + + testMap = testMap.copyAndRemove("1").copyAndRemove("2"); + assertNotEquals(params, testMap); + assertNotEquals(testMap, params); + } + + @Test + public void testEqualsHashCodeWithOneEmptyMap() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + // verify empty maps are not equal to non-empty maps + assertNotEquals(params, UnmodifiableArrayBackedMap.EMPTY_MAP); + assertNotEquals(new HashMap<>(), testMap); + assertNotEquals(UnmodifiableArrayBackedMap.EMPTY_MAP, params); + assertNotEquals(testMap, new HashMap<>()); + } + + @Test + public void testImmutability() { + HashMap params = getTestParameters(); + UnmodifiableArrayBackedMap originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params); + UnmodifiableArrayBackedMap modifiedMap = originalMap.copyAndPutAll(getTestParameters()); + assertEquals(originalMap, params); + + modifiedMap = modifiedMap.copyAndRemoveAll(modifiedMap.keySet()); + assertTrue(modifiedMap.isEmpty()); + + assertEquals(originalMap, params); + } + + @Test + public void testState() { + UnmodifiableArrayBackedMap originalMap; + UnmodifiableArrayBackedMap newMap; + + originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP; + newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray()); + assertEquals(originalMap, newMap); + + originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters()); + newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray()); + assertEquals(originalMap, newMap); + + originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP + .copyAndPutAll(getTestParameters()) + .copyAndRemove("1"); + newMap = UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray()); + assertEquals(originalMap, newMap); + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java index 37a1c0d1fc2..b8857572873 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java @@ -30,6 +30,7 @@ import org.apache.logging.log4j.spi.DefaultThreadContextStack; import org.apache.logging.log4j.spi.NoOpThreadContextMap; import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap; +import org.apache.logging.log4j.spi.StringArrayThreadContextMap; import org.apache.logging.log4j.spi.ThreadContextMap; import org.apache.logging.log4j.spi.ThreadContextMap2; import org.apache.logging.log4j.spi.ThreadContextMapFactory; @@ -211,9 +212,9 @@ public static void init() { ThreadContextMapFactory.init(); contextMap = null; final PropertiesUtil managerProps = PropertiesUtil.getProperties(); - final boolean disableAll = managerProps.getBooleanProperty(DISABLE_ALL); + boolean disableAll = managerProps.getBooleanProperty(DISABLE_ALL); useStack = !(managerProps.getBooleanProperty(DISABLE_STACK) || disableAll); - final boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) || disableAll); + boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) || disableAll); contextStack = new DefaultThreadContextStack(useStack); if (!useMap) { @@ -275,6 +276,8 @@ public static void putAll(final Map m) { ((ThreadContextMap2) contextMap).putAll(m); } else if (contextMap instanceof DefaultThreadContextMap) { ((DefaultThreadContextMap) contextMap).putAll(m); + } else if (contextMap instanceof StringArrayThreadContextMap) { + ((StringArrayThreadContextMap) contextMap).putAll(m); } else { for (final Map.Entry entry : m.entrySet()) { contextMap.put(entry.getKey(), entry.getValue()); @@ -317,6 +320,8 @@ public static void removeAll(final Iterable keys) { ((CleanableThreadContextMap) contextMap).removeAll(keys); } else if (contextMap instanceof DefaultThreadContextMap) { ((DefaultThreadContextMap) contextMap).removeAll(keys); + } else if (contextMap instanceof StringArrayThreadContextMap) { + ((StringArrayThreadContextMap) contextMap).removeAll(keys); } else { for (final String key : keys) { contextMap.remove(key); diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java new file mode 100644 index 00000000000..025a247aac0 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java @@ -0,0 +1,253 @@ +/* + * 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.HashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.logging.log4j.util.BiConsumer; +import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.apache.logging.log4j.util.TriConsumer; + +/** + * An equivalent for DefaultThreadContxtMap, except that it's backed by + * UnmodifiableArrayBackedMap. An instance of UnmodifiableArrayBackedMap can be + * represented as a single Object[], which can safely be stored on the + * ThreadLocal<> with no fear of classloader-related memory leaks. Performance + * of the underlying UnmodifiableArrayBackedMap exceeds HashMap<> in all + * supported operations other than get(). Note that get() performance scales + * linearly with the current map size, and callers are advised to minimize this + * work. + */ +public class StringArrayThreadContextMap implements ThreadContextMap, ReadOnlyStringMap { + private static final long serialVersionUID = -2635197170958057849L; + + /** + * 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 boolean useMap; + private final ThreadLocal threadLocalMapState; + + private static boolean inheritableMap; + + static { + init(); + } + + // LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured. + // (This method is package protected for JUnit tests.) + static ThreadLocal createThreadLocalMap(final boolean isMapEnabled) { + if (inheritableMap) { + return new InheritableThreadLocal() { + @Override + protected Object[] childValue(final Object[] parentValue) { + return parentValue; + } + }; + } + // if not inheritable, return plain ThreadLocal with null as initial value + return new ThreadLocal<>(); + } + + static void init() { + inheritableMap = PropertiesUtil.getProperties().getBooleanProperty(INHERITABLE_MAP); + } + + public StringArrayThreadContextMap() { + this(true); + } + + public StringArrayThreadContextMap(final boolean useMap) { + this.useMap = useMap; + this.threadLocalMapState = createThreadLocalMap(useMap); + } + + @Override + public void put(final String key, final String value) { + if (!useMap) { + return; + } + final Object[] state = threadLocalMapState.get(); + final UnmodifiableArrayBackedMap modifiedMap = + UnmodifiableArrayBackedMap.getInstance(state).copyAndPut(key, value); + threadLocalMapState.set(modifiedMap.getBackingArray()); + } + + public void putAll(final Map m) { + if (!useMap) { + return; + } + final Object[] state = threadLocalMapState.get(); + final UnmodifiableArrayBackedMap modifiedMap = + UnmodifiableArrayBackedMap.getInstance(state).copyAndPutAll(m); + threadLocalMapState.set(modifiedMap.getBackingArray()); + } + + @Override + public String get(final String key) { + final Object[] state = threadLocalMapState.get(); + if (state == null) { + return null; + } + return UnmodifiableArrayBackedMap.getInstance(state).get(key); + } + + @Override + public void remove(final String key) { + final Object[] state = threadLocalMapState.get(); + if (state != null) { + final UnmodifiableArrayBackedMap modifiedMap = + UnmodifiableArrayBackedMap.getInstance(state).copyAndRemove(key); + threadLocalMapState.set(modifiedMap.getBackingArray()); + } + } + + public void removeAll(final Iterable keys) { + final Object[] state = threadLocalMapState.get(); + if (state != null) { + final UnmodifiableArrayBackedMap modifiedMap = + UnmodifiableArrayBackedMap.getInstance(state).copyAndRemoveAll(keys); + threadLocalMapState.set(modifiedMap.getBackingArray()); + } + } + + @Override + public void clear() { + threadLocalMapState.remove(); + } + + @Override + public Map toMap() { + return getCopy(); + } + + @Override + public boolean containsKey(final String key) { + final Object[] state = threadLocalMapState.get(); + return (state == null ? false : (UnmodifiableArrayBackedMap.getInstance(state)).containsKey(key)); + } + + @Override + public void forEach(final BiConsumer action) { + final Object[] state = threadLocalMapState.get(); + if (state == null) { + return; + } + final UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(state); + for (final Map.Entry entry : map.entrySet()) { + // BiConsumer should be able to handle values of any type V. In our case the values are of type String. + @SuppressWarnings("unchecked") + final V value = (V) entry.getValue(); + action.accept(entry.getKey(), value); + } + } + + @Override + public void forEach(final TriConsumer action, final S state) { + Object[] localState = threadLocalMapState.get(); + if (localState == null) { + return; + } + UnmodifiableArrayBackedMap map = UnmodifiableArrayBackedMap.getInstance(localState); + for (final Map.Entry entry : map.entrySet()) { + // TriConsumer should be able to handle values of any type V. In our case the values are of type String. + @SuppressWarnings("unchecked") + final V value = (V) entry.getValue(); + action.accept(entry.getKey(), value, state); + } + } + + @SuppressWarnings("unchecked") + @Override + public V getValue(final String key) { + return (V) get(key); + } + + @Override + public Map getCopy() { + final Object[] state = threadLocalMapState.get(); + if (state == null) { + return new HashMap<>(0); + } + return new HashMap<>(UnmodifiableArrayBackedMap.getInstance(state)); + } + + @Override + public Map getImmutableMapOrNull() { + final Object[] state = threadLocalMapState.get(); + return (state == null ? null : UnmodifiableArrayBackedMap.getInstance(state)); + } + + @Override + public boolean isEmpty() { + return (size() == 0); + } + + @Override + public int size() { + final Object[] state = threadLocalMapState.get(); + return UnmodifiableArrayBackedMap.getInstance(state).size(); + } + + @Override + public String toString() { + final Object[] state = threadLocalMapState.get(); + return state == null + ? "{}" + : UnmodifiableArrayBackedMap.getInstance(state).toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + final Object[] state = threadLocalMapState.get(); + result = prime * result + + ((state == null) + ? 0 + : UnmodifiableArrayBackedMap.getInstance(state).hashCode()); + result = prime * result + Boolean.valueOf(this.useMap).hashCode(); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof StringArrayThreadContextMap) { + final StringArrayThreadContextMap other = (StringArrayThreadContextMap) obj; + if (this.useMap != other.useMap) { + return false; + } + } + if (!(obj instanceof ThreadContextMap)) { + return false; + } + final ThreadContextMap other = (ThreadContextMap) obj; + final Map map = UnmodifiableArrayBackedMap.getInstance(this.threadLocalMapState.get()); + final Map otherMap = other.getImmutableMapOrNull(); + return Objects.equals(map, otherMap); + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java index 566ae1463cb..14c0fe632a9 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java @@ -70,6 +70,7 @@ public static void init() { CopyOnWriteSortedArrayThreadContextMap.init(); GarbageFreeSortedArrayThreadContextMap.init(); DefaultThreadContextMap.init(); + StringArrayThreadContextMap.init(); initPrivate(); } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java new file mode 100644 index 00000000000..b5d583cc680 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java @@ -0,0 +1,479 @@ +/* + * 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.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * This class represents an immutable map, which stores its state inside a single Object[]: + *
    + *
  1. [0] contains the number of entries
  2. + *
  3. Others contain alternating key-value pairs, for example [1]="1" and [2]="value_for_1"
  4. + *
+ * + * Keys are calculated using (index * 2 + 1) and values are (index * 2 + 2). + * + * Performance: + *
    + *
  • Implements very low-cost copies: shallow-copy the array.
  • + *
  • Doesn't matter for mutable operations, since we don't allow them.
  • + *
  • Iterates very quickly, since it iterates directly across the array. This + * contrasts with HashMap's requirement to scan each bucket in the table and + * chase each pointer.
  • + *
  • Is linear on gets, puts, and removes, since the table must be scanned to + * find a matching key.
  • + *
+ * + * Allocation: + *
    + *
  • Zero on reads.
  • + *
  • Copy-and-modify operations allocate exactly two objects: the new array + * and the new Map instance. This is substantially better than HashMap, which + * requires a new Node for each entry.
  • + *
+ * + */ +class UnmodifiableArrayBackedMap extends AbstractMap implements Serializable { + /** + * Implementation of Map.Entry. The implementation is simple since each instance + * contains an index in the array, then getKey() and getValue() retrieve from + * the array. Blocks modifications. + */ + private class UnmodifiableEntry implements Map.Entry { + private final int index; + + public UnmodifiableEntry(int index) { + this.index = index; + } + + @Override + public String getKey() { + return (String) backingArray[getArrayIndexForKey(index)]; + } + + @Override + public String getValue() { + return (String) backingArray[getArrayIndexForValue(index)]; + } + + /** + * Per spec, the hashcode is a function of the key and value. Calculation + * exactly matches HashMap. + */ + public int hashCode() { + String key = (String) backingArray[getArrayIndexForKey(index)]; + String value = (String) backingArray[getArrayIndexForValue(index)]; + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + @Override + public String setValue(String value) { + throw new UnsupportedOperationException("Cannot update Entry instances in UnmodifiableArrayBackedMap"); + } + } + + /** + * Simple Entry iterator, tracking solely the index in the array. Blocks + * modifications. + */ + private class UnmodifiableEntryIterator implements Iterator> { + private int index; + + @Override + public boolean hasNext() { + return index < numEntries; + } + + @Override + public Entry next() { + return new UnmodifiableEntry(index++); + } + } + + /** + * Simple Entry set, providing a reference to UnmodifiableEntryIterator and + * blocking modifications. + */ + private class UnmodifiableEntrySet extends AbstractSet> { + + @Override + public boolean add(Entry e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> c) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator> iterator() { + return new UnmodifiableEntryIterator(); + } + + @Override + public int size() { + return numEntries; + } + } + + private static final long serialVersionUID = 6849423432534211514L; + + public static final UnmodifiableArrayBackedMap EMPTY_MAP = new UnmodifiableArrayBackedMap(0); + + private final Object[] backingArray; + private int numEntries; + + private static final int NUM_FIXED_ARRAY_ENTRIES = 1; + + private static int getArrayIndexForKey(int entryIndex) { + return 2 * entryIndex + NUM_FIXED_ARRAY_ENTRIES; + } + + private static int getArrayIndexForValue(int entryIndex) { + return 2 * entryIndex + 1 + NUM_FIXED_ARRAY_ENTRIES; + } + + private UnmodifiableArrayBackedMap(int capacity) { + this.backingArray = new Object[capacity * 2 + 1]; + this.backingArray[0] = 0; + } + + static UnmodifiableArrayBackedMap getInstance(Object[] backingArray) { + if (backingArray == null || backingArray.length == 1) { + return EMPTY_MAP; + } else { + return new UnmodifiableArrayBackedMap(backingArray); + } + } + + private UnmodifiableArrayBackedMap(Object[] backingArray) { + this.numEntries = (backingArray == null ? 0 : (int) backingArray[0]); + this.backingArray = backingArray; + } + + UnmodifiableArrayBackedMap(UnmodifiableArrayBackedMap other) { + this.backingArray = other.backingArray; + this.numEntries = other.numEntries; + } + + private void add(String key, String value) { + backingArray[getArrayIndexForKey(numEntries)] = key; + backingArray[getArrayIndexForValue(numEntries)] = value; + numEntries++; + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Instance cannot be cleared, reuse EMPTY_MAP instead."); + } + + /** + * Scans the array to find a matching key. Linear performance. + */ + @Override + public boolean containsKey(Object key) { + int hashCode = key.hashCode(); + for (int i = 0; i < numEntries; i++) { + if (backingArray[getArrayIndexForKey(i)].hashCode() == hashCode + && backingArray[getArrayIndexForKey(i)].equals(key)) { + return true; + } + } + + return false; + } + + Object[] getBackingArray() { + return backingArray; + } + + /** + * Scans the array to find a matching value, with linear time. Allows null + * parameter. + */ + @Override + public boolean containsValue(Object value) { + for (int i = 0; i < numEntries; i++) { + Object valueInMap = backingArray[getArrayIndexForValue(i)]; + if (value == null) { + if (valueInMap == null) { + return true; + } + } else if (value.equals(valueInMap)) { + return true; + } + } + return false; + } + + /** + * Creates a new instance that contains the same entries as this map, plus + * either the new entry or updated value passed in the parameters. + * + * @param key + * @param value + * @return + */ + UnmodifiableArrayBackedMap copyAndPut(String key, String value) { + UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries + 1); + // include the numEntries value (array index 0) + if (this.numEntries > 0) { + System.arraycopy(this.backingArray, 1, newMap.backingArray, 1, numEntries * 2); + newMap.numEntries = numEntries; + } + newMap.addOrOverwriteKey(key, value); + newMap.updateNumEntriesInArray(); + return newMap; + } + + /** + * Creates a new instance that contains the same entries as this map, plus the + * new entries or updated values passed in the parameters. + * + * @param key + * @param value + * @return + */ + UnmodifiableArrayBackedMap copyAndPutAll(Map entriesToAdd) { + // create a new array that can hold the maximum output size + UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries + entriesToAdd.size()); + + // copy the contents of the current map (if any) + if (numEntries > 0) { + System.arraycopy(backingArray, 0, newMap.backingArray, 0, numEntries * 2 + 1); + } + + for (Map.Entry entry : entriesToAdd.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (!this.isEmpty()) { + // The unique elements passed in may overlap the unique elements here - must + // check + newMap.addOrOverwriteKey(key, value); + } else { + // There is no chance of overlapping keys, we can simply add + newMap.add(key, value); + } + } + + newMap.updateNumEntriesInArray(); + return newMap; + } + + /** + * Creates a new instance that contains the same entries as this map, minus the + * entry with the specified key (if such an entry exists). + * + * @param key + * @param value + * @return + */ + UnmodifiableArrayBackedMap copyAndRemove(String key) { + UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries); + int indexToRemove = -1; + for (int oldIndex = 0; oldIndex < numEntries; oldIndex++) { + if (backingArray[getArrayIndexForKey(oldIndex)].hashCode() == key.hashCode() + && backingArray[getArrayIndexForKey(oldIndex)].equals(key)) { + indexToRemove = oldIndex; + break; + } + } + + if (indexToRemove == -1) { + // key not found, no change necessary + return this; + } + if (indexToRemove > 0) { + // copy entries before the removed one + System.arraycopy(backingArray, 1, newMap.backingArray, 1, indexToRemove * 2); + } + if (indexToRemove < (numEntries + 1)) { + // copy entries after the removed one + int nextIndexToCopy = indexToRemove + 1; + int numRemainingEntries = numEntries - nextIndexToCopy; + System.arraycopy( + backingArray, + getArrayIndexForKey(nextIndexToCopy), + newMap.backingArray, + getArrayIndexForKey(indexToRemove), + numRemainingEntries * 2); + } + + newMap.numEntries = numEntries - 1; + newMap.updateNumEntriesInArray(); + return newMap; + } + + /** + * Creates a new instance that contains the same entries as this map, minus all + * of the keys passed in the arguments. + * + * @param key + * @param value + * @return + */ + UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable keysToRemoveIterable) { + if (isEmpty()) { + // shortcut: if this map is empty, the result will continue to be empty + return EMPTY_MAP; + } + + // now we build a Set of keys to remove + Set keysToRemoveSet; + if (keysToRemoveIterable instanceof Set) { + // we already have a set, let's cast it and reuse it + keysToRemoveSet = (Set) keysToRemoveIterable; + } else { + // iterate through the keys and build a set + keysToRemoveSet = new HashSet<>(); + for (String key : keysToRemoveIterable) { + keysToRemoveSet.add(key); + } + } + + int firstIndexToKeep = -1; + int lastIndexToKeep = -1; + int destinationIndex = 0; + int numEntriesKept = 0; + // build the new map + UnmodifiableArrayBackedMap newMap = new UnmodifiableArrayBackedMap(numEntries); + for (int indexInCurrentMap = 0; indexInCurrentMap < numEntries; indexInCurrentMap++) { + // for each key in this map, check whether it's in the set we built above + Object key = backingArray[getArrayIndexForKey(indexInCurrentMap)]; + if (!keysToRemoveSet.contains(key)) { + // this key should be kept + if (firstIndexToKeep == -1) { + firstIndexToKeep = indexInCurrentMap; + } + lastIndexToKeep = indexInCurrentMap; + } else if (lastIndexToKeep > 0) { + // we hit a remove, copy any keys that are known ready + int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1; + System.arraycopy( + backingArray, + getArrayIndexForKey(firstIndexToKeep), + newMap.backingArray, + getArrayIndexForKey(destinationIndex), + numEntriesToCopy * 2); + firstIndexToKeep = -1; + lastIndexToKeep = -1; + destinationIndex += numEntriesToCopy; + numEntriesKept += numEntriesToCopy; + } + } + + if (lastIndexToKeep > -1) { + // at least one key still requires copying + int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1; + System.arraycopy( + backingArray, + getArrayIndexForKey(firstIndexToKeep), + newMap.backingArray, + getArrayIndexForKey(destinationIndex), + numEntriesToCopy * 2); + numEntriesKept += numEntriesToCopy; + } + + newMap.numEntries = numEntriesKept; + newMap.updateNumEntriesInArray(); + + return newMap; + } + + /** + * Copies the locally-tracked numEntries into the first array slot. Requires + * autoboxing so call should be minimized - for example, once per bulk update + * operation. + */ + private void updateNumEntriesInArray() { + backingArray[0] = numEntries; + } + + @Override + public Set> entrySet() { + return new UnmodifiableEntrySet(); + } + + /** + * Scans the array to find a matching key. Linear-time. + */ + @Override + public String get(Object key) { + if (numEntries == 0) { + return null; + } + int hashCode = key.hashCode(); + for (int i = 0; i < numEntries; i++) { + if (backingArray[getArrayIndexForKey(i)].hashCode() == hashCode + && backingArray[getArrayIndexForKey(i)].equals(key)) { + return (String) backingArray[getArrayIndexForValue(i)]; + } + } + return null; + } + + /** + * Find an existing entry (if any) and overwrites the value, if found + * + * @param key + * @param value + * @return + */ + private void addOrOverwriteKey(String key, String value) { + int keyHashCode = key.hashCode(); + for (int i = 0; i < numEntries; i++) { + if (backingArray[getArrayIndexForKey(i)].hashCode() == keyHashCode + && backingArray[getArrayIndexForKey(i)].equals(key)) { + // found a match, overwrite then return + backingArray[getArrayIndexForValue(i)] = value; + return; + } + } + + // no match found, add to the end + add(key, value); + } + + @Override + public String put(String key, String value) { + throw new UnsupportedOperationException("put() is not supported, use copyAndPut instead"); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException("putAll() is not supported, use copyAndPutAll instead"); + } + + @Override + public String remove(Object key) { + throw new UnsupportedOperationException("remove() is not supported, use copyAndRemove instead"); + } + + @Override + public int size() { + return numEntries; + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java index 22200e3206a..8fff7b80978 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java @@ -19,7 +19,7 @@ * API classes. */ @Export -@Version("2.20.1") +@Version("2.24.0") package org.apache.logging.log4j.spi; import org.osgi.annotation.bundle.Export; diff --git a/pom.xml b/pom.xml index 1056c418dc9..9c4bb51b8a5 100644 --- a/pom.xml +++ b/pom.xml @@ -304,7 +304,7 @@ - 2.23.1-SNAPSHOT + 2.24.0-SNAPSHOT