001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.classic.joran;
015
016import static ch.qos.logback.classic.ClassicTestConstants.JORAN_INPUT_PREFIX;
017import static ch.qos.logback.classic.joran.ReconfigureOnChangeTask.DETECTED_CHANGE_IN_CONFIGURATION_FILES;
018import static ch.qos.logback.classic.joran.ReconfigureOnChangeTask.FALLING_BACK_TO_SAFE_CONFIGURATION;
019import static ch.qos.logback.classic.joran.ReconfigureOnChangeTask.RE_REGISTERING_PREVIOUS_SAFE_CONFIGURATION;
020import static ch.qos.logback.core.CoreConstants.RECONFIGURE_ON_CHANGE_TASK;
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertFalse;
023import static org.junit.Assert.assertNotNull;
024import static org.junit.Assert.assertNull;
025import static org.junit.Assert.assertTrue;
026import static org.junit.Assert.fail;
027
028import java.io.ByteArrayInputStream;
029import java.io.File;
030import java.io.FileWriter;
031import java.io.IOException;
032import java.io.InputStream;
033import java.util.List;
034import java.util.concurrent.CountDownLatch;
035import java.util.concurrent.ExecutionException;
036import java.util.concurrent.ScheduledFuture;
037
038import org.junit.BeforeClass;
039import org.junit.Test;
040
041import ch.qos.logback.classic.Logger;
042import ch.qos.logback.classic.LoggerContext;
043import ch.qos.logback.classic.issue.lbclassic135.LoggingRunnable;
044import ch.qos.logback.core.CoreConstants;
045import ch.qos.logback.core.contention.AbstractMultiThreadedHarness;
046import ch.qos.logback.core.contention.RunnableWithCounterAndDone;
047import ch.qos.logback.core.joran.spi.ConfigurationWatchList;
048import ch.qos.logback.core.joran.spi.JoranException;
049import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
050import ch.qos.logback.core.status.InfoStatus;
051import ch.qos.logback.core.status.Status;
052import ch.qos.logback.core.testUtil.CoreTestConstants;
053import ch.qos.logback.core.testUtil.FileTestUtil;
054import ch.qos.logback.core.testUtil.RandomUtil;
055import ch.qos.logback.core.testUtil.StatusChecker;
056import ch.qos.logback.core.util.StatusPrinter;
057
058public class ReconfigureOnChangeTaskTest {
059    final static int THREAD_COUNT = 5;
060
061    int diff = RandomUtil.getPositiveInt();
062
063    // the space in the file name mandated by
064    // http://jira.qos.ch/browse/LBCORE-119
065    final static String SCAN1_FILE_AS_STR = JORAN_INPUT_PREFIX + "roct/scan 1.xml";
066
067    final static String G_SCAN1_FILE_AS_STR = JORAN_INPUT_PREFIX + "roct/scan 1.groovy";
068
069    final static String SCAN_LOGBACK_474_FILE_AS_STR = JORAN_INPUT_PREFIX + "roct/scan_logback_474.xml";
070
071    final static String INCLUSION_SCAN_TOPLEVEL0_AS_STR = JORAN_INPUT_PREFIX + "roct/inclusion/topLevel0.xml";
072
073    final static String INCLUSION_SCAN_TOP_BY_RESOURCE_AS_STR = JORAN_INPUT_PREFIX + "roct/inclusion/topByResource.xml";
074
075    final static String INCLUSION_SCAN_INNER0_AS_STR = JORAN_INPUT_PREFIX + "roct/inclusion/inner0.xml";
076
077    final static String INCLUSION_SCAN_INNER1_AS_STR = "target/test-classes/asResource/inner1.xml";
078
079    private static final String SCAN_PERIOD_DEFAULT_FILE_AS_STR =  JORAN_INPUT_PREFIX + "roct/scan_period_default.xml";
080    
081    LoggerContext loggerContext = new LoggerContext();
082    Logger logger = loggerContext.getLogger(this.getClass());
083    StatusChecker statusChecker = new StatusChecker(loggerContext);
084
085    @BeforeClass
086    static public void classSetup() {
087        FileTestUtil.makeTestOutputDir();
088    }
089
090    void configure(File file) throws JoranException {
091        JoranConfigurator jc = new JoranConfigurator();
092        jc.setContext(loggerContext);
093        jc.doConfigure(file);
094    }
095
096    void configure(InputStream is) throws JoranException {
097        JoranConfigurator jc = new JoranConfigurator();
098        jc.setContext(loggerContext);
099        jc.doConfigure(is);
100    }
101
102//    void gConfigure(File file) throws JoranException {
103//        GafferConfigurator gc = new GafferConfigurator(loggerContext);
104//        gc.run(file);
105//    }
106
107    @Test(timeout = 4000L)
108    public void checkBasicLifecyle() throws JoranException, IOException, InterruptedException {
109        File file = new File(SCAN1_FILE_AS_STR);
110        configure(file);
111        List<File> fileList = getConfigurationWatchList(loggerContext);
112        assertThatListContainsFile(fileList, file);
113        checkThatTaskHasRan();
114        checkThatTaskCanBeStopped();
115    }
116
117//    @Test(timeout = 4000L)
118//    public void checkBasicLifecyleWithGaffer() throws JoranException, IOException, InterruptedException {
119//        File file = new File(G_SCAN1_FILE_AS_STR);
120//        gConfigure(file);
121//        List<File> fileList = getConfigurationWatchList(loggerContext);
122//        assertThatListContainsFile(fileList, file);
123//        checkThatTaskHasRan();
124//        checkThatTaskCanBeStopped();
125//    }
126
127    private void checkThatTaskCanBeStopped() {
128        ScheduledFuture<?> future = loggerContext.getScheduledFutures().get(0);
129        loggerContext.stop();
130        assertTrue(future.isCancelled());
131    }
132
133    private void checkThatTaskHasRan() throws InterruptedException {
134        waitForReconfigureOnChangeTaskToRun();
135    }
136
137    List<File> getConfigurationWatchList(LoggerContext context) {
138        ConfigurationWatchList configurationWatchList = ConfigurationWatchListUtil.getConfigurationWatchList(loggerContext);
139        return configurationWatchList.getCopyOfFileWatchList();
140    }
141
142    @Test(timeout = 4000L)
143    public void scanWithFileInclusion() throws JoranException, IOException, InterruptedException {
144        File topLevelFile = new File(INCLUSION_SCAN_TOPLEVEL0_AS_STR);
145        File innerFile = new File(INCLUSION_SCAN_INNER0_AS_STR);
146        configure(topLevelFile);
147        List<File> fileList = getConfigurationWatchList(loggerContext);
148        assertThatListContainsFile(fileList, topLevelFile);
149        assertThatListContainsFile(fileList, innerFile);
150        checkThatTaskHasRan();
151        checkThatTaskCanBeStopped();
152    }
153
154    @Test(timeout = 4000L)
155    public void scanWithResourceInclusion() throws JoranException, IOException, InterruptedException {
156        File topLevelFile = new File(INCLUSION_SCAN_TOP_BY_RESOURCE_AS_STR);
157        File innerFile = new File(INCLUSION_SCAN_INNER1_AS_STR);
158        configure(topLevelFile);
159        List<File> fileList = getConfigurationWatchList(loggerContext);
160        assertThatListContainsFile(fileList, topLevelFile);
161        assertThatListContainsFile(fileList, innerFile);
162    }
163
164    // See also http://jira.qos.ch/browse/LOGBACK-338
165    @Test(timeout = 4000L)
166    public void reconfigurationIsNotPossibleInTheAbsenceOfATopFile() throws IOException, JoranException, InterruptedException {
167        String configurationStr = "<configuration scan=\"true\" scanPeriod=\"50 millisecond\"><include resource=\"asResource/inner1.xml\"/></configuration>";
168        configure(new ByteArrayInputStream(configurationStr.getBytes("UTF-8")));
169
170        ConfigurationWatchList configurationWatchList = ConfigurationWatchListUtil.getConfigurationWatchList(loggerContext);
171        assertNull(configurationWatchList);
172        // assertNull(configurationWatchList.getMainURL());
173
174        statusChecker.containsMatch(Status.WARN, "Due to missing top level");
175        StatusPrinter.print(loggerContext);
176        ReconfigureOnChangeTask roct = getRegisteredReconfigureTask();
177        assertNull(roct);
178        assertEquals(0, loggerContext.getScheduledFutures().size());
179    }
180
181    @Test(timeout = 3000L)
182    public void fallbackToSafe_FollowedByRecovery() throws IOException, JoranException, InterruptedException {
183        String path = CoreTestConstants.OUTPUT_DIR_PREFIX + "reconfigureOnChangeConfig_fallbackToSafe-" + diff + ".xml";
184        File topLevelFile = new File(path);
185        writeToFile(topLevelFile, "<configuration scan=\"true\" scanPeriod=\"5 millisecond\"><root level=\"ERROR\"/></configuration> ");
186        configure(topLevelFile);
187        CountDownLatch changeDetectedLatch = waitForReconfigurationToBeDone(null);
188        ReconfigureOnChangeTask oldRoct = getRegisteredReconfigureTask();
189        assertNotNull(oldRoct);
190        writeToFile(topLevelFile, "<configuration scan=\"true\" scanPeriod=\"5 millisecond\">\n" + "  <root></configuration>");
191        changeDetectedLatch.await();
192        statusChecker.assertContainsMatch(Status.WARN, FALLING_BACK_TO_SAFE_CONFIGURATION);
193        statusChecker.assertContainsMatch(Status.INFO, RE_REGISTERING_PREVIOUS_SAFE_CONFIGURATION);
194
195        loggerContext.getStatusManager().clear();
196
197        CountDownLatch secondDoneLatch = waitForReconfigurationToBeDone(oldRoct);
198        writeToFile(topLevelFile, "<configuration scan=\"true\" scanPeriod=\"5 millisecond\"><root level=\"ERROR\"/></configuration> ");
199        secondDoneLatch.await();
200        StatusPrinter.print(loggerContext);
201        statusChecker.assertIsErrorFree();
202        statusChecker.containsMatch(DETECTED_CHANGE_IN_CONFIGURATION_FILES);
203    }
204
205    @Test(timeout = 4000L)
206    public void fallbackToSafeWithIncludedFile_FollowedByRecovery() throws IOException, JoranException, InterruptedException, ExecutionException {
207        String topLevelFileAsStr = CoreTestConstants.OUTPUT_DIR_PREFIX + "reconfigureOnChangeConfig_top-" + diff + ".xml";
208        String innerFileAsStr = CoreTestConstants.OUTPUT_DIR_PREFIX + "reconfigureOnChangeConfig_inner-" + diff + ".xml";
209        File topLevelFile = new File(topLevelFileAsStr);
210        writeToFile(topLevelFile, "<configuration xdebug=\"true\" scan=\"true\" scanPeriod=\"5 millisecond\"><include file=\"" + innerFileAsStr
211                        + "\"/></configuration> ");
212
213        File innerFile = new File(innerFileAsStr);
214        writeToFile(innerFile, "<included><root level=\"ERROR\"/></included> ");
215        configure(topLevelFile);
216
217        CountDownLatch doneLatch = waitForReconfigurationToBeDone(null);
218        ReconfigureOnChangeTask oldRoct = getRegisteredReconfigureTask();
219        assertNotNull(oldRoct);
220        
221        writeToFile(innerFile, "<included>\n<root>\n</included>");
222        doneLatch.await();
223
224        statusChecker.assertContainsMatch(Status.WARN, FALLING_BACK_TO_SAFE_CONFIGURATION);
225        statusChecker.assertContainsMatch(Status.INFO, RE_REGISTERING_PREVIOUS_SAFE_CONFIGURATION);
226
227        loggerContext.getStatusManager().clear();
228
229        CountDownLatch secondDoneLatch = waitForReconfigurationToBeDone(oldRoct);
230        writeToFile(innerFile, "<included><root level=\"ERROR\"/></included> ");
231        secondDoneLatch.await();
232        
233        StatusPrinter.print(loggerContext);
234        statusChecker.assertIsErrorFree();
235        statusChecker.containsMatch(DETECTED_CHANGE_IN_CONFIGURATION_FILES);
236    }
237
238    private ReconfigureOnChangeTask getRegisteredReconfigureTask() {
239        return (ReconfigureOnChangeTask) loggerContext.getObject(RECONFIGURE_ON_CHANGE_TASK);
240    }
241
242    class RunMethodInvokedListener extends ReconfigureOnChangeTaskListener {
243        CountDownLatch countDownLatch;
244
245        RunMethodInvokedListener(CountDownLatch countDownLatch) {
246            this.countDownLatch = countDownLatch;
247        }
248
249        @Override
250        public void enteredRunMethod() {
251            countDownLatch.countDown();
252        }
253    };
254
255    class ChangeDetectedListener extends ReconfigureOnChangeTaskListener {
256        CountDownLatch countDownLatch;
257
258        ChangeDetectedListener(CountDownLatch countDownLatch) {
259            this.countDownLatch = countDownLatch;
260        }
261
262        @Override
263        public void changeDetected() {
264            countDownLatch.countDown();
265        }
266    };
267
268    class ReconfigurationDoneListener extends ReconfigureOnChangeTaskListener {
269        CountDownLatch countDownLatch;
270
271        ReconfigurationDoneListener(CountDownLatch countDownLatch) {
272            this.countDownLatch = countDownLatch;
273        }
274
275        @Override
276        public void doneReconfiguring() {
277            countDownLatch.countDown();
278        }
279    };
280
281    private ReconfigureOnChangeTask waitForReconfigureOnChangeTaskToRun() throws InterruptedException {
282        ReconfigureOnChangeTask roct = null;
283        while (roct == null) {
284            roct = getRegisteredReconfigureTask();
285            Thread.yield();
286        }
287
288        CountDownLatch countDownLatch = new CountDownLatch(1);
289        roct.addListener(new RunMethodInvokedListener(countDownLatch));
290        countDownLatch.await();
291        return roct;
292    }
293
294    private CountDownLatch waitForReconfigurationToBeDone(ReconfigureOnChangeTask oldTask) throws InterruptedException {
295        ReconfigureOnChangeTask roct = oldTask;
296        while (roct == oldTask) {
297            roct = getRegisteredReconfigureTask();
298            Thread.yield();
299        }
300
301        CountDownLatch countDownLatch = new CountDownLatch(1);
302        roct.addListener(new ReconfigurationDoneListener(countDownLatch));
303        return countDownLatch;
304    }
305
306    private RunnableWithCounterAndDone[] buildRunnableArray(File configFile, UpdateType updateType) {
307        RunnableWithCounterAndDone[] rArray = new RunnableWithCounterAndDone[THREAD_COUNT];
308        rArray[0] = new Updater(configFile, updateType);
309        for (int i = 1; i < THREAD_COUNT; i++) {
310            rArray[i] = new LoggingRunnable(logger);
311        }
312        return rArray;
313    }
314
315    @Test
316    public void checkReconfigureTaskScheduledWhenDefaultScanPeriodUsed() throws JoranException {
317        File file = new File(SCAN_PERIOD_DEFAULT_FILE_AS_STR);
318        configure(file);
319
320        final List<ScheduledFuture<?>> scheduledFutures = loggerContext.getScheduledFutures();
321        StatusPrinter.print(loggerContext);
322        assertFalse(scheduledFutures.isEmpty());
323        statusChecker.containsMatch("No 'scanPeriod' specified. Defaulting to");
324   
325    }
326
327    // check for deadlocks
328    @Test(timeout = 4000L)
329    public void scan_LOGBACK_474() throws JoranException, IOException, InterruptedException {
330        loggerContext.setName("scan_LOGBACK_474");
331        File file = new File(SCAN_LOGBACK_474_FILE_AS_STR);
332        // StatusListenerConfigHelper.addOnConsoleListenerInstance(loggerContext, new OnConsoleStatusListener());
333        configure(file);
334
335        // ReconfigureOnChangeTask roct = waitForReconfigureOnChangeTaskToRun();
336
337        int expectedResets = 2;
338        Harness harness = new Harness(expectedResets);
339
340        RunnableWithCounterAndDone[] runnableArray = buildRunnableArray(file, UpdateType.TOUCH);
341        harness.execute(runnableArray);
342
343        loggerContext.getStatusManager().add(new InfoStatus("end of execution ", this));
344        StatusPrinter.print(loggerContext);
345        checkResetCount(expectedResets);
346    }
347
348    private void assertThatListContainsFile(List<File> fileList, File file) {
349        // conversion to absolute file seems to work nicely
350        assertTrue(fileList.contains(file.getAbsoluteFile()));
351    }
352
353    private void checkResetCount(int expected) {
354        StatusChecker checker = new StatusChecker(loggerContext);
355        checker.assertIsErrorFree();
356
357        int effectiveResets = checker.matchCount(CoreConstants.RESET_MSG_PREFIX);
358        assertEquals(expected, effectiveResets);
359
360        // String failMsg = "effective=" + effectiveResets + ", expected=" + expected;
361        //
362        // there might be more effective resets than the expected amount
363        // since the harness may be sleeping while a reset occurs
364        // assertTrue(failMsg, expected <= effectiveResets && (expected + 2) >= effectiveResets);
365
366    }
367
368    void addInfo(String msg, Object o) {
369        loggerContext.getStatusManager().add(new InfoStatus(msg, o));
370    }
371
372    enum UpdateType {
373        TOUCH, MALFORMED, MALFORMED_INNER
374    }
375
376    void writeToFile(File file, String contents) throws IOException {
377        FileWriter fw = new FileWriter(file);
378        fw.write(contents);
379        fw.close();
380        // on linux changes to last modified are not propagated if the
381        // time stamp is near the previous time stamp hence the random delta
382        file.setLastModified(System.currentTimeMillis()+RandomUtil.getPositiveInt());
383    }
384
385    class Harness extends AbstractMultiThreadedHarness {
386        int changeCountLimit;
387
388        Harness(int changeCount) {
389            this.changeCountLimit = changeCount;
390        }
391
392        public void waitUntilEndCondition() throws InterruptedException {
393            ReconfigureOnChangeTaskTest.this.addInfo("Entering " + this.getClass() + ".waitUntilEndCondition()", this);
394
395            int changeCount = 0;
396            ReconfigureOnChangeTask lastRoct = null;
397            CountDownLatch countDownLatch = null;
398
399            while (changeCount < changeCountLimit) {
400                ReconfigureOnChangeTask roct = (ReconfigureOnChangeTask) loggerContext.getObject(RECONFIGURE_ON_CHANGE_TASK);
401                if (lastRoct != roct && roct != null) {
402                    lastRoct = roct;
403                    countDownLatch = new CountDownLatch(1);
404                    roct.addListener(new ChangeDetectedListener(countDownLatch));
405                } else if (countDownLatch != null) {
406                    countDownLatch.await();
407                    countDownLatch = null;
408                    changeCount++;
409                }
410                Thread.yield();
411            }
412            ReconfigureOnChangeTaskTest.this.addInfo("*****Exiting " + this.getClass() + ".waitUntilEndCondition()", this);
413        }
414
415    }
416
417    class Updater extends RunnableWithCounterAndDone {
418        File configFile;
419        UpdateType updateType;
420
421        // it actually takes time for Windows to propagate file modification changes
422        // values below 100 milliseconds can be problematic the same propagation
423        // latency occurs in Linux but is even larger (>600 ms)
424        // final static int DEFAULT_SLEEP_BETWEEN_UPDATES = 60;
425
426        int sleepBetweenUpdates = 100;
427
428        Updater(File configFile, UpdateType updateType) {
429            this.configFile = configFile;
430            this.updateType = updateType;
431        }
432
433        Updater(File configFile) {
434            this(configFile, UpdateType.TOUCH);
435        }
436
437        public void run() {
438            while (!isDone()) {
439                try {
440                    Thread.sleep(sleepBetweenUpdates);
441                } catch (InterruptedException e) {
442                }
443                if (isDone()) {
444                    ReconfigureOnChangeTaskTest.this.addInfo("Exiting Updater.run()", this);
445                    return;
446                }
447                counter++;
448                ReconfigureOnChangeTaskTest.this.addInfo("Touching [" + configFile + "]", this);
449                switch (updateType) {
450                case TOUCH:
451                    touchFile();
452                    break;
453                case MALFORMED:
454                    try {
455                        malformedUpdate();
456                    } catch (IOException e) {
457                        e.printStackTrace();
458                        fail("malformedUpdate failed");
459                    }
460                    break;
461                case MALFORMED_INNER:
462                    try {
463                        malformedInnerUpdate();
464                    } catch (IOException e) {
465                        e.printStackTrace();
466                        fail("malformedInnerUpdate failed");
467                    }
468                }
469            }
470            ReconfigureOnChangeTaskTest.this.addInfo("Exiting Updater.run()", this);
471        }
472
473        private void malformedUpdate() throws IOException {
474            writeToFile(configFile, "<configuration scan=\"true\" scanPeriod=\"50 millisecond\">\n" + "  <root level=\"ERROR\">\n" + "</configuration>");
475        }
476
477        private void malformedInnerUpdate() throws IOException {
478            writeToFile(configFile, "<included>\n" + "  <root>\n" + "</included>");
479        }
480
481        void touchFile() {
482            configFile.setLastModified(System.currentTimeMillis());
483        }
484    }
485
486}