/* * Copyright 2010-2014 Ning, Inc. * Copyright 2014 The Billing Project, LLC * * Ning 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.killbill.billing.plugin.meter.timeline.persistent; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.killbill.billing.plugin.meter.MeterConfig; import org.killbill.billing.plugin.meter.MeterTestSuiteNoDB; import org.killbill.billing.plugin.meter.timeline.BackgroundDBChunkWriter; import org.killbill.billing.plugin.meter.timeline.MockTimelineDao; import org.killbill.billing.plugin.meter.timeline.TimelineEventHandler; import org.killbill.billing.plugin.meter.timeline.codec.DefaultSampleCoder; import org.killbill.billing.plugin.meter.timeline.codec.SampleCoder; import org.killbill.billing.plugin.meter.timeline.sources.SourceSamplesForTimestamp; import org.killbill.billing.plugin.meter.timeline.times.DefaultTimelineCoder; import org.killbill.billing.plugin.meter.timeline.times.TimelineCoder; import org.skife.config.ConfigurationObjectFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.google.common.collect.ImmutableMap; public class TestFileBackedBuffer extends MeterTestSuiteNoDB { private static final Logger log = LoggerFactory.getLogger(TestFileBackedBuffer.class); private static final UUID HOST_UUID = UUID.randomUUID(); private static final String KIND_A = "kindA"; private static final String KIND_B = "kindB"; private static final Map<String, Object> EVENT = ImmutableMap.<String, Object>of(KIND_A, 12, KIND_B, 42); // ~105 bytes per event, 10 1MB buffers -> need at least 100,000 events to spill over private static final int NB_EVENTS = 100000; private static final File basePath = new File(System.getProperty("java.io.tmpdir"), "TestFileBackedBuffer-" + System.currentTimeMillis()); private static final TimelineCoder timelineCoder = new DefaultTimelineCoder(); private static final SampleCoder sampleCoder = new DefaultSampleCoder(); private final TimelineDao dao = new MockTimelineDao(); private TimelineEventHandler timelineEventHandler; @BeforeMethod(groups = "fast") public void setUp() throws Exception { Assert.assertTrue(basePath.mkdir()); System.setProperty("org.killbill.billing.plugin.meter.timelines.spoolDir", basePath.getAbsolutePath()); System.setProperty("org.killbill.billing.plugin.meter.timelines.length", "60s"); final MeterConfig config = new ConfigurationObjectFactory(System.getProperties()).build(MeterConfig.class); timelineEventHandler = new TimelineEventHandler(config, dao, timelineCoder, sampleCoder, new BackgroundDBChunkWriter(dao, config), new FileBackedBuffer(config.getSpoolDir(), "TimelineEventHandler", 1024 * 1024, 10)); dao.getOrAddSource(HOST_UUID.toString(), callContext); } @Test(groups = "fast") // Not really fast, but doesn't require a database public void testAppend() throws Exception { log.info("Writing files to " + basePath); final List<File> binFiles = new ArrayList<File>(); final List<DateTime> timestampsRecorded = new ArrayList<DateTime>(); final List<String> categoriesRecorded = new ArrayList<String>(); // Sanity check before the tests Assert.assertEquals(timelineEventHandler.getBackingBuffer().getFilesCreated(), 0); findBinFiles(binFiles, basePath); Assert.assertEquals(binFiles.size(), 0); // Send enough events to spill over to disk final DateTime startTime = new DateTime(DateTimeZone.UTC); for (int i = 0; i < NB_EVENTS; i++) { final String category = UUID.randomUUID().toString(); final DateTime eventTimestamp = startTime.plusSeconds(i); timelineEventHandler.record(HOST_UUID.toString(), category, eventTimestamp, EVENT, callContext); timestampsRecorded.add(eventTimestamp); categoriesRecorded.add(category); } // Check the files have been created (at least one per accumulator) final long bytesOnDisk = timelineEventHandler.getBackingBuffer().getBytesOnDisk(); Assert.assertTrue(timelineEventHandler.getBackingBuffer().getFilesCreated() > 0); binFiles.clear(); findBinFiles(binFiles, basePath); Assert.assertTrue(binFiles.size() > 0); log.info("Sent {} events and wrote {} bytes on disk ({} bytes/event)", new Object[]{NB_EVENTS, bytesOnDisk, bytesOnDisk / NB_EVENTS}); // Replay the events. Note that size of timestamp recorded != eventsReplayed as some of the ones sent are still in memory final Replayer replayer = new Replayer(basePath.getAbsolutePath()); final List<SourceSamplesForTimestamp> eventsReplayed = replayer.readAll(); for (int i = 0; i < eventsReplayed.size(); i++) { // Looks like Jackson maps it back using the JVM timezone Assert.assertEquals(eventsReplayed.get(i).getTimestamp().toDateTime(DateTimeZone.UTC), timestampsRecorded.get(i)); Assert.assertEquals(eventsReplayed.get(i).getCategory(), categoriesRecorded.get(i)); } // Make sure files have been deleted binFiles.clear(); findBinFiles(binFiles, basePath); Assert.assertEquals(binFiles.size(), 0); } private static void findBinFiles(final Collection<File> files, final File directory) { final File[] found = directory.listFiles(new FileFilter() { @Override public boolean accept(final File pathname) { return pathname.getName().endsWith(".bin"); } }); if (found != null) { for (final File file : found) { if (file.isDirectory()) { findBinFiles(files, file); } else { files.add(file); } } } } }