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.turbo;
015
016import ch.qos.logback.classic.ClassicTestConstants;
017import ch.qos.logback.classic.Level;
018import ch.qos.logback.classic.Logger;
019import ch.qos.logback.classic.LoggerContext;
020import ch.qos.logback.classic.issue.lbclassic135.LoggingRunnable;
021import ch.qos.logback.classic.joran.JoranConfigurator;
022import ch.qos.logback.classic.spi.TurboFilterList;
023import ch.qos.logback.core.CoreConstants;
024import ch.qos.logback.core.contention.AbstractMultiThreadedHarness;
025import ch.qos.logback.core.contention.RunnableWithCounterAndDone;
026import ch.qos.logback.core.contention.WaitOnExecutionMultiThreadedHarness;
027import ch.qos.logback.core.joran.spi.ConfigurationWatchList;
028import ch.qos.logback.core.joran.spi.JoranException;
029import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
030import ch.qos.logback.core.status.InfoStatus;
031import ch.qos.logback.core.status.Status;
032import ch.qos.logback.core.testUtil.CoreTestConstants;
033import ch.qos.logback.core.testUtil.EnvUtilForTests;
034import ch.qos.logback.core.testUtil.FileTestUtil;
035import ch.qos.logback.core.testUtil.RandomUtil;
036import ch.qos.logback.core.testUtil.StatusChecker;
037import ch.qos.logback.core.util.StatusPrinter;
038import org.junit.*;
039
040import java.io.*;
041import java.net.MalformedURLException;
042import java.util.List;
043import java.util.concurrent.ExecutorService;
044import java.util.concurrent.ThreadPoolExecutor;
045import java.util.concurrent.TimeUnit;
046
047import static org.junit.Assert.*;
048
049@Ignore
050public class ReconfigureOnChangeTest {
051    final static int THREAD_COUNT = 5;
052    final static int LOOP_LEN = 1000 * 1000;
053
054    int diff = RandomUtil.getPositiveInt();
055
056    // the space in the file name mandated by
057    // http://jira.qos.ch/browse/LBCORE-119
058    final static String SCAN1_FILE_AS_STR = ClassicTestConstants.INPUT_PREFIX + "turbo/scan 1.xml";
059
060    final static String G_SCAN1_FILE_AS_STR = ClassicTestConstants.INPUT_PREFIX + "turbo/scan 1.groovy";
061
062    final static String SCAN_LOGBACK_474_FILE_AS_STR = ClassicTestConstants.INPUT_PREFIX + "turbo/scan_logback_474.xml";
063
064    final static String INCLUSION_SCAN_TOPLEVEL0_AS_STR = ClassicTestConstants.INPUT_PREFIX + "turbo/inclusion/topLevel0.xml";
065
066    final static String INCLUSION_SCAN_TOP_BY_RESOURCE_AS_STR = ClassicTestConstants.INPUT_PREFIX + "turbo/inclusion/topByResource.xml";
067
068    final static String INCLUSION_SCAN_INNER0_AS_STR = ClassicTestConstants.INPUT_PREFIX + "turbo/inclusion/inner0.xml";
069
070    final static String INCLUSION_SCAN_INNER1_AS_STR = "target/test-classes/asResource/inner1.xml";
071
072    // it actually takes time for Windows to propagate file modification changes
073    // values below 100 milliseconds can be problematic the same propagation
074    // latency occurs in Linux but is even larger (>600 ms)
075    // final static int DEFAULT_SLEEP_BETWEEN_UPDATES = 60;
076
077    int sleepBetweenUpdates = 100;
078
079    LoggerContext loggerContext = new LoggerContext();
080    Logger logger = loggerContext.getLogger(this.getClass());
081    ExecutorService executorService = loggerContext.getExecutorService();
082
083    StatusChecker checker = new StatusChecker(loggerContext);
084    AbstractMultiThreadedHarness harness;
085
086    ThreadPoolExecutor executor = (ThreadPoolExecutor) loggerContext.getExecutorService();
087
088    int expectedResets = 2;
089
090    @BeforeClass
091    static public void classSetup() {
092        FileTestUtil.makeTestOutputDir();
093    }
094
095    @Before
096    public void setUp() {
097        harness = new WaitOnExecutionMultiThreadedHarness(executor, expectedResets);
098    }
099
100    @After
101    public void tearDown() {
102    }
103
104    void configure(File file) throws JoranException {
105        JoranConfigurator jc = new JoranConfigurator();
106        jc.setContext(loggerContext);
107        jc.doConfigure(file);
108    }
109
110    void configure(InputStream is) throws JoranException {
111        JoranConfigurator jc = new JoranConfigurator();
112        jc.setContext(loggerContext);
113        jc.doConfigure(is);
114    }
115
116//    void gConfigure(File file) throws JoranException {
117//        GafferConfigurator gc = new GafferConfigurator(loggerContext);
118//        gc.run(file);
119//    }
120
121    RunnableWithCounterAndDone[] buildRunnableArray(File configFile, UpdateType updateType) {
122        RunnableWithCounterAndDone[] rArray = new RunnableWithCounterAndDone[THREAD_COUNT];
123        rArray[0] = new Updater(configFile, updateType);
124        for (int i = 1; i < THREAD_COUNT; i++) {
125            rArray[i] = new LoggingRunnable(logger);
126        }
127        return rArray;
128    }
129
130    // Tests whether ConfigurationAction is installing ReconfigureOnChangeFilter
131    @Test
132    public void installFilter() throws JoranException, IOException, InterruptedException {
133        File file = new File(SCAN1_FILE_AS_STR);
134        configure(file);
135        List<File> fileList = getConfigurationFileList(loggerContext);
136        assertThatListContainsFile(fileList, file);
137        assertThatFirstFilterIsROCF();
138        StatusPrinter.print(loggerContext);
139    }
140
141//    @Test
142//    public void gafferInstallFilter() throws JoranException, IOException, InterruptedException {
143//        File file = new File(G_SCAN1_FILE_AS_STR);
144//        gConfigure(file);
145//        List<File> fileList = getConfigurationFileList(loggerContext);
146//        assertThatListContainsFile(fileList, file);
147//        assertThatFirstFilterIsROCF();
148//
149//        rocfDetachReconfigurationToNewThreadAndAwaitTermination();
150//
151//        fileList = getConfigurationFileList(loggerContext);
152//        assertThatListContainsFile(fileList, file);
153//        assertThatFirstFilterIsROCF();
154//
155//        // check that rcof filter installed on two occasions
156//        assertEquals(2, checker.matchCount("Will scan for changes in"));
157//    }
158
159    private void rocfDetachReconfigurationToNewThreadAndAwaitTermination() throws InterruptedException {
160        ReconfigureOnChangeFilter reconfigureOnChangeFilter = (ReconfigureOnChangeFilter) getFirstTurboFilter();
161        reconfigureOnChangeFilter.detachReconfigurationToNewThread();
162        executorService.shutdown();
163        executorService.awaitTermination(1000, TimeUnit.MILLISECONDS);
164    }
165
166    List<File> getConfigurationFileList(LoggerContext context) {
167        ConfigurationWatchList configurationWatchList = ConfigurationWatchListUtil.getConfigurationWatchList(loggerContext);
168        return configurationWatchList.getCopyOfFileWatchList();
169    }
170
171    @Test(timeout = 4000L)
172    public void scanWithFileInclusion() throws JoranException, IOException, InterruptedException {
173        File topLevelFile = new File(INCLUSION_SCAN_TOPLEVEL0_AS_STR);
174        File innerFile = new File(INCLUSION_SCAN_INNER0_AS_STR);
175        configure(topLevelFile);
176        List<File> fileList = getConfigurationFileList(loggerContext);
177        assertThatListContainsFile(fileList, topLevelFile);
178        assertThatListContainsFile(fileList, innerFile);
179    }
180
181    @Test(timeout = 4000L)
182    public void scanWithResourceInclusion() throws JoranException, IOException, InterruptedException {
183        File topLevelFile = new File(INCLUSION_SCAN_TOP_BY_RESOURCE_AS_STR);
184        File innerFile = new File(INCLUSION_SCAN_INNER1_AS_STR);
185        configure(topLevelFile);
186
187        List<File> fileList = getConfigurationFileList(loggerContext);
188        assertThatListContainsFile(fileList, topLevelFile);
189        assertThatListContainsFile(fileList, innerFile);
190    }
191
192    // See also http://jira.qos.ch/browse/LOGBACK-338
193    @Test
194    public void includeScanViaInputStreamSuppliedConfigFile() throws IOException, JoranException, InterruptedException {
195        String configurationStr = "<configuration scan=\"true\" scanPeriod=\"50 millisecond\"><include resource=\"asResource/inner1.xml\"/></configuration>";
196        configure(new ByteArrayInputStream(configurationStr.getBytes("UTF-8")));
197
198        ConfigurationWatchList configurationWatchList = ConfigurationWatchListUtil.getConfigurationWatchList(loggerContext);
199        assertNull(configurationWatchList.getMainURL());
200
201        ReconfigureOnChangeFilter reconfigureOnChangeFilter = (ReconfigureOnChangeFilter) getFirstTurboFilter();
202        // without a top level file, reconfigureOnChangeFilter should not start
203        assertFalse(reconfigureOnChangeFilter.isStarted());
204    }
205
206    @Test(timeout = 4000L)
207    public void fallbackToSafe() throws IOException, JoranException, InterruptedException {
208        String path = CoreTestConstants.OUTPUT_DIR_PREFIX + "reconfigureOnChangeConfig_fallbackToSafe-" + diff + ".xml";
209        File topLevelFile = new File(path);
210        writeToFile(topLevelFile, "<configuration scan=\"true\" scanPeriod=\"50 millisecond\"><root level=\"ERROR\"/></configuration> ");
211        configure(topLevelFile);
212
213        writeToFile(topLevelFile, "<configuration scan=\"true\" scanPeriod=\"50 millisecond\">\n" + "  <root></configuration>");
214
215        rocfDetachReconfigurationToNewThreadAndAwaitTermination();
216
217        checker.assertContainsMatch(Status.WARN, "Falling back to previously registered safe configuration.");
218        checker.assertContainsMatch(Status.INFO, "Re-registering previous fallback configuration once more");
219
220        assertThatFirstFilterIsROCF();
221    }
222
223    @Test(timeout = 4000L)
224    public void fallbackToSafeWithIncludedFile() throws IOException, JoranException, InterruptedException {
225        String topLevelFileAsStr = CoreTestConstants.OUTPUT_DIR_PREFIX + "reconfigureOnChangeConfig_top-" + diff + ".xml";
226        String innerFileAsStr = CoreTestConstants.OUTPUT_DIR_PREFIX + "reconfigureOnChangeConfig_inner-" + diff + ".xml";
227        File topLevelFile = new File(topLevelFileAsStr);
228        writeToFile(topLevelFile, "<configuration scan=\"true\" scanPeriod=\"50 millisecond\"><include file=\"" + innerFileAsStr + "\"/></configuration> ");
229
230        File innerFile = new File(innerFileAsStr);
231        writeToFile(innerFile, "<included><root level=\"ERROR\"/></included> ");
232        configure(topLevelFile);
233        writeToFile(innerFile, "<included>\n<root>\n</included>");
234        rocfDetachReconfigurationToNewThreadAndAwaitTermination();
235
236        checker.assertContainsMatch(Status.WARN, "Falling back to previously registered safe configuration.");
237        checker.assertContainsMatch(Status.INFO, "Re-registering previous fallback configuration once more");
238
239        assertThatFirstFilterIsROCF();
240    }
241
242    // check for deadlocks
243    @Test(timeout = 4000L)
244    public void scan_LOGBACK_474() throws JoranException, IOException, InterruptedException {
245        File file = new File(SCAN_LOGBACK_474_FILE_AS_STR);
246        configure(file);
247
248        RunnableWithCounterAndDone[] runnableArray = buildRunnableArray(file, UpdateType.TOUCH);
249        harness.execute(runnableArray);
250
251        loggerContext.getStatusManager().add(new InfoStatus("end of execution ", this));
252
253        verify(expectedResets);
254    }
255
256    private void assertThatListContainsFile(List<File> fileList, File file) {
257        // conversion to absolute file seems to work nicely
258        assertTrue(fileList.contains(file.getAbsoluteFile()));
259    }
260
261    private TurboFilter getFirstTurboFilter() {
262        TurboFilterList turboFilterList = loggerContext.getTurboFilterList();
263        return turboFilterList.get(0);
264    }
265
266    private void assertThatFirstFilterIsROCF() {
267        assertTrue(getFirstTurboFilter() instanceof ReconfigureOnChangeFilter);
268    }
269
270    private void verify(int expected) {
271        StatusChecker checker = new StatusChecker(loggerContext);
272        // StatusPrinter.print(loggerContext);
273        checker.assertIsErrorFree();
274
275        int effectiveResets = checker.matchCount(CoreConstants.RESET_MSG_PREFIX);
276
277        String failMsg = "effective=" + effectiveResets + ", expected=" + expected;
278
279        // there might be more effective resets than the expected amount
280        // since the harness may be sleeping while a reset occurs
281        assertTrue(failMsg, expected <= effectiveResets && (expected + 2) >= effectiveResets);
282
283    }
284
285    ReconfigureOnChangeFilter initROCF() throws MalformedURLException {
286        ReconfigureOnChangeFilter rocf = new ReconfigureOnChangeFilter();
287        rocf.setContext(loggerContext);
288        File file = new File(SCAN1_FILE_AS_STR);
289        ConfigurationWatchListUtil.setMainWatchURL(loggerContext, file.toURI().toURL());
290        rocf.start();
291        return rocf;
292    }
293
294    @Test
295    @Ignore
296    public void directPerfTest() throws MalformedURLException {
297        if (EnvUtilForTests.isLinux()) {
298            // for some reason this test does not pass on Linux (AMD 64 bit,
299            // Dual Core Opteron 170)
300            return;
301        }
302
303        ReconfigureOnChangeFilter rocf = initROCF();
304        assertTrue(rocf.isStarted());
305
306        for (int i = 0; i < 30; i++) {
307            directLoop(rocf);
308        }
309        double avg = directLoop(rocf);
310        System.out.println("directPerfTest: " + avg);
311    }
312
313    public double directLoop(ReconfigureOnChangeFilter rocf) {
314        long start = System.nanoTime();
315        for (int i = 0; i < LOOP_LEN; i++) {
316            rocf.decide(null, logger, Level.DEBUG, " ", null, null);
317        }
318        long end = System.nanoTime();
319        return (end - start) / (1.0d * LOOP_LEN);
320    }
321
322    @Ignore
323    @Test
324    public void indirectPerfTest() throws MalformedURLException {
325        if (EnvUtilForTests.isLinux()) {
326            // for some reason this test does not pass on Linux (AMD 64 bit,
327            // Dual Core
328            // Opteron 170)
329            return;
330        }
331
332        ReconfigureOnChangeFilter rocf = initROCF();
333        assertTrue(rocf.isStarted());
334        loggerContext.addTurboFilter(rocf);
335        logger.setLevel(Level.ERROR);
336
337        indirectLoop();
338        double avg = indirectLoop();
339        System.out.println(avg);
340        // the reference was computed on Orion (Ceki's computer)
341        @SuppressWarnings("unused")
342        long referencePerf = 68;
343        //BogoPerf.assertDuration(avg, referencePerf, CoreConstants.REFERENCE_BIPS);
344    }
345
346    void addInfo(String msg, Object o) {
347        loggerContext.getStatusManager().add(new InfoStatus(msg, o));
348    }
349
350    public double indirectLoop() {
351        long start = System.nanoTime();
352        for (int i = 0; i < LOOP_LEN; i++) {
353            logger.debug("hello");
354        }
355        long end = System.nanoTime();
356        return (end - start) / (1.0d * LOOP_LEN);
357    }
358
359    enum UpdateType {
360        TOUCH, MALFORMED, MALFORMED_INNER
361    }
362
363    void writeToFile(File file, String contents) throws IOException {
364        FileWriter fw = new FileWriter(file);
365        fw.write(contents);
366        fw.close();
367    }
368
369    class Updater extends RunnableWithCounterAndDone {
370        File configFile;
371        UpdateType updateType;
372
373        Updater(File configFile, UpdateType updateType) {
374            this.configFile = configFile;
375            this.updateType = updateType;
376        }
377
378        Updater(File configFile) {
379            this(configFile, UpdateType.TOUCH);
380        }
381
382        public void run() {
383            while (!isDone()) {
384                try {
385                    Thread.sleep(sleepBetweenUpdates);
386                } catch (InterruptedException e) {
387                }
388                if (isDone()) {
389                    return;
390                }
391                counter++;
392                ReconfigureOnChangeTest.this.addInfo("***settting last modified", this);
393                switch (updateType) {
394                case TOUCH:
395                    touchFile();
396                    break;
397                case MALFORMED:
398                    try {
399                        malformedUpdate();
400                    } catch (IOException e) {
401                        e.printStackTrace();
402                        fail("malformedUpdate failed");
403                    }
404                    break;
405                case MALFORMED_INNER:
406                    try {
407                        malformedInnerUpdate();
408                    } catch (IOException e) {
409                        e.printStackTrace();
410                        fail("malformedInnerUpdate failed");
411                    }
412                }
413            }
414        }
415
416        private void malformedUpdate() throws IOException {
417            writeToFile(configFile, "<configuration scan=\"true\" scanPeriod=\"50 millisecond\">\n" + "  <root level=\"ERROR\">\n" + "</configuration>");
418        }
419
420        private void malformedInnerUpdate() throws IOException {
421            writeToFile(configFile, "<included>\n" + "  <root>\n" + "</included>");
422        }
423
424        void touchFile() {
425            configFile.setLastModified(System.currentTimeMillis());
426        }
427    }
428
429}