diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java b/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java
index 757a970126..7793cc39cb 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java
@@ -72,7 +72,7 @@ public final class Logger
/**
* It is assumed that once the 'aai' variable is set to a non-null value, it
* will never be reset to null. it is further assumed that only place where the
- * 'aai'ariable is set is within the addAppender method. This method is
+ * 'aai variable is set is within the addAppender method. This method is
* synchronized on 'this' (Logger) protecting against simultaneous
* re-configuration of this logger (a very unlikely scenario).
*
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/pattern/MaskedKeyValuePairConverter.java b/logback-classic/src/main/java/ch/qos/logback/classic/pattern/MaskedKeyValuePairConverter.java
index 9277943497..5409e1da20 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/pattern/MaskedKeyValuePairConverter.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/pattern/MaskedKeyValuePairConverter.java
@@ -30,6 +30,8 @@
* Assuming the specified key is k2, and the kvp list of an event contains {k1, v1}, {k2, v2}, the String output
* will be "k1=v1 k2=XXX", without the quotes.
*
+ * Value quotes can be specified as the first option, e.g %maskedKvp{SINGLE, k1}
+ *
* @author Ceki Gülcü
* @since 1.5.7
*/
diff --git a/logback-classic/src/test/input/joran/conversionRule/conversionRuleAtEnd.xml b/logback-classic/src/test/input/joran/conversionRule/conversionRuleAtEnd.xml
new file mode 100644
index 0000000000..4e06dac0de
--- /dev/null
+++ b/logback-classic/src/test/input/joran/conversionRule/conversionRuleAtEnd.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %sample - %msg
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logback-classic/src/test/input/joran/conversionRule/conversionRuleIncluded.xml b/logback-classic/src/test/input/joran/conversionRule/conversionRuleIncluded.xml
new file mode 100644
index 0000000000..a28eba2e08
--- /dev/null
+++ b/logback-classic/src/test/input/joran/conversionRule/conversionRuleIncluded.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %sample - %msg
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logback-classic/src/test/input/joran/conversionRule/conversionRuleTop0.xml b/logback-classic/src/test/input/joran/conversionRule/conversionRuleTop0.xml
new file mode 100644
index 0000000000..3ed44138ac
--- /dev/null
+++ b/logback-classic/src/test/input/joran/conversionRule/conversionRuleTop0.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ %sample - %msg
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/PatternLayoutTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/PatternLayoutTest.java
index 00238c61e2..56fcf00ecd 100644
--- a/logback-classic/src/test/java/ch/qos/logback/classic/PatternLayoutTest.java
+++ b/logback-classic/src/test/java/ch/qos/logback/classic/PatternLayoutTest.java
@@ -236,6 +236,30 @@ public void testConversionRuleSupportInPatternLayout() throws JoranException {
assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
}
+ @Test
+ public void testConversionRuleAtEnd() throws JoranException {
+ configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleAtEnd.xml");
+ root.getAppender("LIST");
+ String msg = "testConversionRuleAtEnd";
+ logger.debug(msg);
+ StringListAppender sla = (StringListAppender) root.getAppender("LIST");
+ assertNotNull(sla);
+ assertEquals(1, sla.strList.size());
+ assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
+ }
+
+ @Test
+ public void testConversionRuleInIncluded() throws JoranException {
+ configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleTop0.xml");
+ root.getAppender("LIST");
+ String msg = "testConversionRuleInIncluded";
+ logger.debug(msg);
+ StringListAppender sla = (StringListAppender) root.getAppender("LIST");
+ assertNotNull(sla);
+ assertEquals(1, sla.strList.size());
+ assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
+ }
+
@Test
public void smokeReplace() {
pl.setPattern("%replace(a1234b){'\\d{4}', 'XXXX'}");
diff --git a/logback-core/src/test/java/ch/qos/logback/core/util/COWArrayListConcurrencyTest.java b/logback-core/src/test/java/ch/qos/logback/core/util/COWArrayListConcurrencyTest.java
new file mode 100644
index 0000000000..8d8a2e7261
--- /dev/null
+++ b/logback-core/src/test/java/ch/qos/logback/core/util/COWArrayListConcurrencyTest.java
@@ -0,0 +1,188 @@
+/*
+ * Logback: the reliable, generic, fast and flexible logging framework.
+ * Copyright (C) 1999-2024, QOS.ch. All rights reserved.
+ *
+ * This program and the accompanying materials are dual-licensed under
+ * either the terms of the Eclipse Public License v1.0 as published by
+ * the Eclipse Foundation
+ *
+ * or (per the licensee's choosing)
+ *
+ * under the terms of the GNU Lesser General Public License version 2.1
+ * as published by the Free Software Foundation.
+ */
+
+package ch.qos.logback.core.util;
+
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.AppenderBase;
+import ch.qos.logback.core.Context;
+import ch.qos.logback.core.ContextBase;
+import ch.qos.logback.core.spi.AppenderAttachableImpl;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.locks.ReentrantLock;
+
+import static org.assertj.core.api.Fail.fail;
+
+@Disabled
+public class COWArrayListConcurrencyTest {
+
+ //private static final int LIST_SIZE = 1_000_000;
+ private static final int LOOP_LEN = 1_0;
+ private static final int RECONFIGURE_DELAY = 1;
+
+ ReentrantLock reconfigureLock = new ReentrantLock(true);
+ ReentrantLock writeLock = new ReentrantLock(true);
+
+ private static int THREAD_COUNT = 200; //Runtime.getRuntime().availableProcessors()*200;
+ //private static int THREAD_COUNT = 5000;
+
+ private final ExecutorService tasksExecutor = Executors.newVirtualThreadPerTaskExecutor();
+ LoopingRunnable[] loopingThreads = new LoopingRunnable[THREAD_COUNT];
+ ReconfiguringThread[] reconfiguringThreads = new ReconfiguringThread[THREAD_COUNT];
+ Future>[] futures = new Future[THREAD_COUNT];
+
+ AppenderAttachableImpl aai = new AppenderAttachableImpl<>();
+ Context context = new ContextBase();
+
+ void reconfigureWithDelay(AppenderAttachableImpl aai) {
+ try {
+ reconfigureLock.lock();
+ aai.addAppender(makeNewNOPAppender());
+ aai.addAppender(makeNewNOPAppender());
+ delay(RECONFIGURE_DELAY);
+ aai.detachAndStopAllAppenders();
+ } finally {
+ reconfigureLock.unlock();
+ }
+ }
+
+ private Appender makeNewNOPAppender() {
+ List longList = new ArrayList<>();
+// for (int j = 0; j < LIST_SIZE; j++) {
+// longList.add(0L);
+// }
+ Appender nopAppenderWithDelay = new NOPAppenderWithDelay<>(longList);
+ nopAppenderWithDelay.setContext(context);
+ nopAppenderWithDelay.start();
+ return nopAppenderWithDelay;
+ }
+
+ private void delay(int delay) {
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ void smoke() throws InterruptedException, ExecutionException {
+
+ for (int i = 0; i < THREAD_COUNT; i++) {
+ System.out.println("i="+i);
+ ReconfiguringThread rt = new ReconfiguringThread(aai);
+ futures[i] = tasksExecutor.submit(rt);
+ reconfiguringThreads[i] = rt;
+ }
+
+ for (int i = 0; i < THREAD_COUNT; i++) {
+ LoopingRunnable loopingThread = new LoopingRunnable(i, aai);
+ tasksExecutor.submit(loopingThread);
+ loopingThreads[i] = loopingThread;
+ }
+
+ for (int i = 0; i < THREAD_COUNT; i++) {
+ futures[i].get();
+ }
+
+ //reconfiguringThread.join();
+ Arrays.stream(loopingThreads).forEach(lt -> lt.active = false);
+
+ }
+
+ public class NOPAppenderWithDelay extends AppenderBase {
+
+ List longList;
+
+ NOPAppenderWithDelay(List longList) {
+ this.longList = new ArrayList<>(longList);
+ }
+
+ int i = 0;
+
+ @Override
+ protected void append(E eventObject) {
+ i++;
+ try {
+ writeLock.lock();
+ if ((i & 0xF) == 0) {
+ delay(1);
+ } else {
+ //longList.stream().map(x-> x+1);
+ }
+ } finally {
+ writeLock.unlock();
+ }
+
+ }
+
+ }
+
+ class ReconfiguringThread extends Thread {
+
+ AppenderAttachableImpl aai;
+
+ ReconfiguringThread(AppenderAttachableImpl aai) {
+ this.aai = aai;
+ }
+
+ public void run() {
+ Thread.yield();
+ for (int i = 0; i < LOOP_LEN; i++) {
+ reconfigureWithDelay(aai);
+ }
+ }
+
+
+ }
+
+
+ class LoopingRunnable implements Runnable {
+
+ int num;
+ AppenderAttachableImpl aai;
+ public boolean active = true;
+
+ LoopingRunnable(int num, AppenderAttachableImpl aai) {
+ this.num = num;
+ this.aai = aai;
+ }
+
+ public void run() {
+ System.out.println("LoopingRunnable.run.num="+num);
+ int i = 0;
+ while (active) {
+ if ((i & 0xFFFFF) == 0) {
+ long id = Thread.currentThread().threadId();
+ System.out.println("thread=" + id + " reconfigure=" + i);
+ }
+ aai.appendLoopOnAppenders(Integer.toString(i));
+ i++;
+ //Thread.yield();
+ }
+ }
+ }
+
+
+}