From f8d9a3b19b8a69e20c4469b541a5ae96b07e7cc8 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Mon, 11 Mar 2013 11:08:44 -0700 Subject: [PATCH 01/18] IndexGranularity: Fix increment for day, week --- .../main/java/com/metamx/druid/index/v1/IndexGranularity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/metamx/druid/index/v1/IndexGranularity.java b/server/src/main/java/com/metamx/druid/index/v1/IndexGranularity.java index 4628028ee0b..75ebd7c0387 100644 --- a/server/src/main/java/com/metamx/druid/index/v1/IndexGranularity.java +++ b/server/src/main/java/com/metamx/druid/index/v1/IndexGranularity.java @@ -204,7 +204,7 @@ public enum IndexGranularity @Override public long increment(long timeMillis) { - return timeMillis - MILLIS_IN; + return timeMillis + MILLIS_IN; } @Override @@ -273,7 +273,7 @@ public enum IndexGranularity @Override public long increment(long timeMillis) { - return timeMillis - MILLIS_IN; + return timeMillis + MILLIS_IN; } @Override From 4126893ba3b9bd1da5512d569d82a22af1e5bfd3 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Mon, 11 Mar 2013 11:13:19 -0700 Subject: [PATCH 02/18] KafkaFirehoseFactory: Fix serialization --- .../java/com/metamx/druid/realtime/KafkaFirehoseFactory.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/realtime/src/main/java/com/metamx/druid/realtime/KafkaFirehoseFactory.java b/realtime/src/main/java/com/metamx/druid/realtime/KafkaFirehoseFactory.java index 12c74ad6b16..58d136d1b3d 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/KafkaFirehoseFactory.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/KafkaFirehoseFactory.java @@ -50,8 +50,13 @@ public class KafkaFirehoseFactory implements FirehoseFactory { private static final Logger log = new Logger(KafkaFirehoseFactory.class); + @JsonProperty private final Properties consumerProps; + + @JsonProperty private final String feed; + + @JsonProperty private final StringInputRowParser parser; @JsonCreator From 3fa46988f50c4cd697eebfa1366c450bcdf7a5e0 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Mon, 11 Mar 2013 11:14:51 -0700 Subject: [PATCH 03/18] Realtime: - MetadataUpdater now built from SegmentAnnouncer, SegmentPublisher instances. - Sinks can take a version instead of always using interval.start. The realtime plumber selects a version using a VersioningPolicy. - Plumbers gained a startJob method. - Realtime plumbers gained an implementation for finishJob. --- .../druid/guava/ThreadRenamingCallable.java | 52 ++ .../examples/RealtimeStandaloneMain.java | 11 +- .../examples/RealtimeStandaloneMain.java | 15 +- .../druid/realtime/DbSegmentPublisher.java | 91 +++ .../druid/realtime/MetadataUpdater.java | 192 +----- .../com/metamx/druid/realtime/Plumber.java | 20 +- .../druid/realtime/RealtimeManager.java | 2 + .../metamx/druid/realtime/RealtimeNode.java | 13 +- .../druid/realtime/RealtimePlumberSchool.java | 643 ++++++++++-------- .../druid/realtime/SegmentAnnouncer.java | 11 + .../druid/realtime/SegmentPublisher.java | 10 + .../java/com/metamx/druid/realtime/Sink.java | 16 +- .../druid/realtime/ZkSegmentAnnouncer.java | 104 +++ 13 files changed, 692 insertions(+), 488 deletions(-) create mode 100644 common/src/main/java/com/metamx/druid/guava/ThreadRenamingCallable.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/SegmentAnnouncer.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/SegmentPublisher.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java diff --git a/common/src/main/java/com/metamx/druid/guava/ThreadRenamingCallable.java b/common/src/main/java/com/metamx/druid/guava/ThreadRenamingCallable.java new file mode 100644 index 00000000000..6e034bfc156 --- /dev/null +++ b/common/src/main/java/com/metamx/druid/guava/ThreadRenamingCallable.java @@ -0,0 +1,52 @@ +/* + * Druid - a distributed column store. + * Copyright (C) 2012 Metamarkets Group Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package com.metamx.druid.guava; + +import java.util.concurrent.Callable; + +/** + */ +public abstract class ThreadRenamingCallable implements Callable +{ + private final String name; + + public ThreadRenamingCallable( + String name + ) + { + this.name = name; + } + + @Override + public final T call() + { + final Thread currThread = Thread.currentThread(); + String currName = currThread.getName(); + try { + currThread.setName(name); + return doCall(); + } + finally { + currThread.setName(currName); + } + } + + public abstract T doCall(); +} diff --git a/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java b/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java index 92eb86cc801..1a7e57ffbba 100644 --- a/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java +++ b/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java @@ -1,21 +1,17 @@ package druid.examples; import com.fasterxml.jackson.databind.jsontype.NamedType; -import com.metamx.common.config.Config; import com.metamx.common.lifecycle.Lifecycle; import com.metamx.common.logger.Logger; import com.metamx.druid.client.DataSegment; import com.metamx.druid.client.ZKPhoneBook; -import com.metamx.druid.initialization.Initialization; import com.metamx.druid.jackson.DefaultObjectMapper; +import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.log.LogLevelAdjuster; import com.metamx.druid.realtime.MetadataUpdater; -import com.metamx.druid.realtime.MetadataUpdaterConfig; import com.metamx.druid.realtime.RealtimeNode; -import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.phonebook.PhoneBook; - import java.io.File; import java.io.IOException; @@ -46,10 +42,7 @@ public class RealtimeStandaloneMain rn.setPhoneBook(dummyPhoneBook); MetadataUpdater dummyMetadataUpdater = - new MetadataUpdater(new DefaultObjectMapper(), - Config.createFactory(Initialization.loadProperties()).build(MetadataUpdaterConfig.class), - dummyPhoneBook, - null) { + new MetadataUpdater(null, null) { @Override public void publishSegment(DataSegment segment) throws IOException { diff --git a/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java b/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java index 5f4d25cb95b..53b41416366 100644 --- a/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java +++ b/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java @@ -1,22 +1,18 @@ package druid.examples; import com.fasterxml.jackson.databind.jsontype.NamedType; -import com.metamx.common.config.Config; import com.metamx.common.lifecycle.Lifecycle; import com.metamx.common.logger.Logger; import com.metamx.druid.client.DataSegment; import com.metamx.druid.client.ZKPhoneBook; -import com.metamx.druid.initialization.Initialization; import com.metamx.druid.jackson.DefaultObjectMapper; +import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.log.LogLevelAdjuster; import com.metamx.druid.realtime.MetadataUpdater; -import com.metamx.druid.realtime.MetadataUpdaterConfig; import com.metamx.druid.realtime.RealtimeNode; -import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.phonebook.PhoneBook; import druid.examples.twitter.TwitterSpritzerFirehoseFactory; - import java.io.File; import java.io.IOException; @@ -47,13 +43,8 @@ public class RealtimeStandaloneMain }; rn.setPhoneBook(dummyPhoneBook); - MetadataUpdater dummyMetadataUpdater = - new MetadataUpdater( - new DefaultObjectMapper(), - Config.createFactory(Initialization.loadProperties()).build(MetadataUpdaterConfig.class), - dummyPhoneBook, - null - ) { + final MetadataUpdater dummyMetadataUpdater = + new MetadataUpdater(null, null) { @Override public void publishSegment(DataSegment segment) throws IOException { diff --git a/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java b/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java new file mode 100644 index 00000000000..cec5172bdbd --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java @@ -0,0 +1,91 @@ +package com.metamx.druid.realtime; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.metamx.common.logger.Logger; +import com.metamx.druid.client.DataSegment; +import org.joda.time.DateTime; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.tweak.HandleCallback; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class DbSegmentPublisher implements SegmentPublisher +{ + private static final Logger log = new Logger(DbSegmentPublisher.class); + + private final ObjectMapper jsonMapper; + private final MetadataUpdaterConfig config; + private final DBI dbi; + + public DbSegmentPublisher( + ObjectMapper jsonMapper, + MetadataUpdaterConfig config, + DBI dbi + ) + { + this.jsonMapper = jsonMapper; + this.config = config; + this.dbi = dbi; + } + + public void publishSegment(final DataSegment segment) throws IOException + { + try { + List> exists = dbi.withHandle( + new HandleCallback>>() + { + @Override + public List> withHandle(Handle handle) throws Exception + { + return handle.createQuery( + String.format("SELECT id FROM %s WHERE id=:id", config.getSegmentTable()) + ) + .bind("id", segment.getIdentifier()) + .list(); + } + } + ); + + if (!exists.isEmpty()) { + log.info("Found [%s] in DB, not updating DB", segment.getIdentifier()); + return; + } + + dbi.withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + handle.createStatement( + String.format( + "INSERT INTO %s (id, dataSource, created_date, start, end, partitioned, version, used, payload) " + + "VALUES (:id, :dataSource, :created_date, :start, :end, :partitioned, :version, :used, :payload)", + config.getSegmentTable() + ) + ) + .bind("id", segment.getIdentifier()) + .bind("dataSource", segment.getDataSource()) + .bind("created_date", new DateTime().toString()) + .bind("start", segment.getInterval().getStart().toString()) + .bind("end", segment.getInterval().getEnd().toString()) + .bind("partitioned", segment.getShardSpec().getPartitionNum()) + .bind("version", segment.getVersion()) + .bind("used", true) + .bind("payload", jsonMapper.writeValueAsString(segment)) + .execute(); + + return null; + } + } + ); + } + catch (Exception e) { + log.error(e, "Exception inserting into DB"); + throw new RuntimeException(e); + } + } +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java b/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java index 2d377124cc3..b03fa70dc75 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java @@ -1,201 +1,35 @@ -/* - * Druid - a distributed column store. - * Copyright (C) 2012 Metamarkets Group Inc. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - package com.metamx.druid.realtime; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ImmutableMap; -import com.metamx.common.lifecycle.LifecycleStart; -import com.metamx.common.lifecycle.LifecycleStop; -import com.metamx.common.logger.Logger; import com.metamx.druid.client.DataSegment; -import com.metamx.phonebook.PhoneBook; - -import org.joda.time.DateTime; -import org.skife.jdbi.v2.DBI; -import org.skife.jdbi.v2.Handle; -import org.skife.jdbi.v2.tweak.HandleCallback; import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -/** - */ -public class MetadataUpdater +public class MetadataUpdater implements SegmentAnnouncer, SegmentPublisher { - private static final Logger log = new Logger(MetadataUpdater.class); + private final SegmentAnnouncer segmentAnnouncer; + private final SegmentPublisher segmentPublisher; - private final Object lock = new Object(); - - private final ObjectMapper jsonMapper; - private final MetadataUpdaterConfig config; - private final PhoneBook yp; - private final String servedSegmentsLocation; - private final DBI dbi; - - private volatile boolean started = false; - - public MetadataUpdater( - ObjectMapper jsonMapper, - MetadataUpdaterConfig config, - PhoneBook yp, - DBI dbi - ) + public MetadataUpdater(SegmentAnnouncer segmentAnnouncer, SegmentPublisher segmentPublisher) { - this.jsonMapper = jsonMapper; - this.config = config; - this.yp = yp; - this.servedSegmentsLocation = yp.combineParts( - Arrays.asList( - config.getServedSegmentsLocation(), config.getServerName() - ) - ); - - this.dbi = dbi; - } - - public Map getStringProps() - { - return ImmutableMap.of( - "name", config.getServerName(), - "host", config.getHost(), - "maxSize", String.valueOf(config.getMaxSize()), - "type", "realtime" - ); - } - - public boolean hasStarted() - { - return started; - } - - @LifecycleStart - public void start() - { - synchronized (lock) { - if (started) { - return; - } - - log.info("Starting zkCoordinator for server[%s] with config[%s]", config.getServerName(), config); - if (yp.lookup(servedSegmentsLocation, Object.class) == null) { - yp.post( - config.getServedSegmentsLocation(), - config.getServerName(), - ImmutableMap.of("created", new DateTime().toString()) - ); - } - - yp.announce( - config.getAnnounceLocation(), - config.getServerName(), - getStringProps() - ); - - started = true; - } - } - - @LifecycleStop - public void stop() - { - synchronized (lock) { - if (!started) { - return; - } - - log.info("Stopping MetadataUpdater with config[%s]", config); - yp.unannounce(config.getAnnounceLocation(), config.getServerName()); - - started = false; - } + this.segmentAnnouncer = segmentAnnouncer; + this.segmentPublisher = segmentPublisher; } + @Override public void announceSegment(DataSegment segment) throws IOException { - log.info("Announcing realtime segment %s", segment.getIdentifier()); - yp.announce(servedSegmentsLocation, segment.getIdentifier(), segment); + segmentAnnouncer.announceSegment(segment); } + @Override public void unannounceSegment(DataSegment segment) throws IOException { - log.info("Unannouncing realtime segment %s", segment.getIdentifier()); - yp.unannounce(servedSegmentsLocation, segment.getIdentifier()); + segmentAnnouncer.unannounceSegment(segment); } - public void publishSegment(final DataSegment segment) throws IOException + @Override + public void publishSegment(DataSegment segment) throws IOException { - try { - List> exists = dbi.withHandle( - new HandleCallback>>() - { - @Override - public List> withHandle(Handle handle) throws Exception - { - return handle.createQuery( - String.format("SELECT id FROM %s WHERE id=:id", config.getSegmentTable()) - ) - .bind("id", segment.getIdentifier()) - .list(); - } - } - ); - - if (!exists.isEmpty()) { - log.info("Found [%s] in DB, not updating DB", segment.getIdentifier()); - return; - } - - dbi.withHandle( - new HandleCallback() - { - @Override - public Void withHandle(Handle handle) throws Exception - { - handle.createStatement( - String.format( - "INSERT INTO %s (id, dataSource, created_date, start, end, partitioned, version, used, payload) " - + "VALUES (:id, :dataSource, :created_date, :start, :end, :partitioned, :version, :used, :payload)", - config.getSegmentTable() - ) - ) - .bind("id", segment.getIdentifier()) - .bind("dataSource", segment.getDataSource()) - .bind("created_date", new DateTime().toString()) - .bind("start", segment.getInterval().getStart().toString()) - .bind("end", segment.getInterval().getEnd().toString()) - .bind("partitioned", segment.getShardSpec().getPartitionNum()) - .bind("version", segment.getVersion()) - .bind("used", true) - .bind("payload", jsonMapper.writeValueAsString(segment)) - .execute(); - - return null; - } - } - ); - } - catch (Exception e) { - log.error(e, "Exception inserting into DB"); - throw new RuntimeException(e); - } + segmentPublisher.publishSegment(segment); } } diff --git a/realtime/src/main/java/com/metamx/druid/realtime/Plumber.java b/realtime/src/main/java/com/metamx/druid/realtime/Plumber.java index d68442670e7..57366b5ee5b 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/Plumber.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/Plumber.java @@ -22,12 +22,28 @@ package com.metamx.druid.realtime; import com.metamx.druid.Query; import com.metamx.druid.query.QueryRunner; -/** - */ public interface Plumber { + /** + * Perform any initial setup. Should be called before using any other methods, and should be paired + * with a corresponding call to {@link #finishJob}. + */ + public void startJob(); + public Sink getSink(long timestamp); public QueryRunner getQueryRunner(Query query); + + /** + * Persist any in-memory indexed data to durable storage. This may be only somewhat durable, e.g. the + * machine's local disk. + * + * @param commitRunnable code to run after persisting data + */ void persist(Runnable commitRunnable); + + /** + * Perform any final processing and clean up after ourselves. Should be called after all data has been + * fed into sinks and persisted. + */ public void finishJob(); } diff --git a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java index 97c7611801a..17dba60a847 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java @@ -154,6 +154,8 @@ public class RealtimeManager implements QuerySegmentWalker final Period intermediatePersistPeriod = config.getIntermediatePersistPeriod(); try { + plumber.startJob(); + long nextFlush = new DateTime().plus(intermediatePersistPeriod).getMillis(); while (firehose.hasMore()) { final InputRow inputRow; diff --git a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java index fb47abab945..b523bb94a57 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java @@ -256,13 +256,18 @@ public class RealtimeNode extends BaseServerNode protected void initializeMetadataUpdater() { if (metadataUpdater == null) { - metadataUpdater = new MetadataUpdater( + final MetadataUpdaterConfig metadataUpdaterConfig = getConfigFactory().build(MetadataUpdaterConfig.class); + final SegmentAnnouncer segmentAnnouncer = new ZkSegmentAnnouncer(metadataUpdaterConfig, getPhoneBook()); + final SegmentPublisher segmentPublisher = new DbSegmentPublisher( getJsonMapper(), - getConfigFactory().build(MetadataUpdaterConfig.class), - getPhoneBook(), + metadataUpdaterConfig, new DbConnector(getConfigFactory().build(DbConnectorConfig.class)).getDBI() ); - getLifecycle().addManagedInstance(metadataUpdater); + + getLifecycle().addManagedInstance(segmentAnnouncer); + getLifecycle().addManagedInstance(segmentPublisher); + + metadataUpdater = new MetadataUpdater(segmentAnnouncer, segmentPublisher); } } diff --git a/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java b/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java index 775dc7d5305..2adee9623e4 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java @@ -41,6 +41,7 @@ import com.metamx.druid.Query; import com.metamx.druid.client.DataSegment; import com.metamx.druid.client.DruidServer; import com.metamx.druid.client.ServerView; +import com.metamx.druid.guava.ThreadRenamingCallable; import com.metamx.druid.guava.ThreadRenamingRunnable; import com.metamx.druid.index.QueryableIndex; import com.metamx.druid.index.QueryableIndexSegment; @@ -58,11 +59,6 @@ import com.metamx.emitter.EmittingLogger; import com.metamx.emitter.service.ServiceEmitter; import com.metamx.emitter.service.ServiceMetricEvent; import org.apache.commons.io.FileUtils; - - - - - import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.Interval; @@ -75,7 +71,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -90,9 +86,7 @@ public class RealtimePlumberSchool implements PlumberSchool private final File basePersistDirectory; private final IndexGranularity segmentGranularity; - private volatile Executor persistExecutor = null; - private volatile ScheduledExecutorService scheduledExecutor = null; - + private volatile VersioningPolicy versioningPolicy = null; private volatile RejectionPolicyFactory rejectionPolicyFactory = null; private volatile QueryRunnerFactoryConglomerate conglomerate = null; private volatile DataSegmentPusher dataSegmentPusher = null; @@ -110,6 +104,7 @@ public class RealtimePlumberSchool implements PlumberSchool this.windowPeriod = windowPeriod; this.basePersistDirectory = basePersistDirectory; this.segmentGranularity = segmentGranularity; + this.versioningPolicy = new IntervalStartVersioningPolicy(); this.rejectionPolicyFactory = new ServerTimeRejectionPolicyFactory(); Preconditions.checkNotNull(windowPeriod, "RealtimePlumberSchool requires a windowPeriod."); @@ -117,6 +112,12 @@ public class RealtimePlumberSchool implements PlumberSchool Preconditions.checkNotNull(segmentGranularity, "RealtimePlumberSchool requires a segmentGranularity."); } + @JsonProperty("versioningPolicy") + public void setVersioningPolicy(VersioningPolicy versioningPolicy) + { + this.versioningPolicy = versioningPolicy; + } + @JsonProperty("rejectionPolicy") public void setRejectionPolicyFactory(RejectionPolicyFactory factory) { @@ -157,209 +158,28 @@ public class RealtimePlumberSchool implements PlumberSchool public Plumber findPlumber(final Schema schema, final FireDepartmentMetrics metrics) { verifyState(); - initializeExecutors(); - computeBaseDir(schema).mkdirs(); - - final Map sinks = Maps.newConcurrentMap(); - - for (File sinkDir : computeBaseDir(schema).listFiles()) { - Interval sinkInterval = new Interval(sinkDir.getName().replace("_", "/")); - - final File[] sinkFiles = sinkDir.listFiles(); - Arrays.sort( - sinkFiles, - new Comparator() - { - @Override - public int compare(File o1, File o2) - { - try { - return Ints.compare(Integer.parseInt(o1.getName()), Integer.parseInt(o2.getName())); - } - catch (NumberFormatException e) { - log.error(e, "Couldn't compare as numbers? [%s][%s]", o1, o2); - return o1.compareTo(o2); - } - } - } - ); - - try { - List hydrants = Lists.newArrayList(); - for (File segmentDir : sinkFiles) { - log.info("Loading previously persisted segment at [%s]", segmentDir); - hydrants.add( - new FireHydrant( - new QueryableIndexSegment(null, IndexIO.loadIndex(segmentDir)), - Integer.parseInt(segmentDir.getName()) - ) - ); - } - - Sink currSink = new Sink(sinkInterval, schema, hydrants); - sinks.put(sinkInterval.getStartMillis(), currSink); - - metadataUpdater.announceSegment(currSink.getSegment()); - } - catch (IOException e) { - log.makeAlert(e, "Problem loading sink[%s] from disk.", schema.getDataSource()) - .addData("interval", sinkInterval) - .emit(); - } - } - - serverView.registerSegmentCallback( - persistExecutor, - new ServerView.BaseSegmentCallback() - { - @Override - public ServerView.CallbackAction segmentAdded(DruidServer server, DataSegment segment) - { - if ("realtime".equals(server.getType())) { - return ServerView.CallbackAction.CONTINUE; - } - - log.debug("Checking segment[%s] on server[%s]", segment, server); - if (schema.getDataSource().equals(segment.getDataSource())) { - final Interval interval = segment.getInterval(); - for (Map.Entry entry : sinks.entrySet()) { - final Long sinkKey = entry.getKey(); - if (interval.contains(sinkKey)) { - final Sink sink = entry.getValue(); - log.info("Segment matches sink[%s]", sink); - - if (segment.getVersion().compareTo(sink.getSegment().getVersion()) >= 0) { - try { - metadataUpdater.unannounceSegment(sink.getSegment()); - FileUtils.deleteDirectory(computePersistDir(schema, sink.getInterval())); - sinks.remove(sinkKey); - } - catch (IOException e) { - log.makeAlert(e, "Unable to delete old segment for dataSource[%s].", schema.getDataSource()) - .addData("interval", sink.getInterval()) - .emit(); - } - } - } - } - } - - return ServerView.CallbackAction.CONTINUE; - } - } - ); - - final long truncatedNow = segmentGranularity.truncate(new DateTime()).getMillis(); - final long windowMillis = windowPeriod.toStandardDuration().getMillis(); final RejectionPolicy rejectionPolicy = rejectionPolicyFactory.create(windowPeriod); log.info("Creating plumber using rejectionPolicy[%s]", rejectionPolicy); - log.info( - "Expect to run at [%s]", - new DateTime().plus( - new Duration(System.currentTimeMillis(), segmentGranularity.increment(truncatedNow) + windowMillis) - ) - ); - - ScheduledExecutors - .scheduleAtFixedRate( - scheduledExecutor, - new Duration(System.currentTimeMillis(), segmentGranularity.increment(truncatedNow) + windowMillis), - new Duration(truncatedNow, segmentGranularity.increment(truncatedNow)), - new ThreadRenamingRunnable(String.format("%s-overseer", schema.getDataSource())) - { - @Override - public void doRun() - { - log.info("Starting merge and push."); - - long minTimestamp = segmentGranularity.truncate(rejectionPolicy.getCurrMaxTime()).getMillis() - windowMillis; - - List> sinksToPush = Lists.newArrayList(); - for (Map.Entry entry : sinks.entrySet()) { - final Long intervalStart = entry.getKey(); - if (intervalStart < minTimestamp) { - log.info("Adding entry[%s] for merge and push.", entry); - sinksToPush.add(entry); - } - } - - for (final Map.Entry entry : sinksToPush) { - final Sink sink = entry.getValue(); - - final String threadName = String.format( - "%s-%s-persist-n-merge", schema.getDataSource(), new DateTime(entry.getKey()) - ); - persistExecutor.execute( - new ThreadRenamingRunnable(threadName) - { - @Override - public void doRun() - { - final Interval interval = sink.getInterval(); - - for (FireHydrant hydrant : sink) { - if (!hydrant.hasSwapped()) { - log.info("Hydrant[%s] hasn't swapped yet, swapping. Sink[%s]", hydrant, sink); - final int rowCount = persistHydrant(hydrant, schema, interval); - metrics.incrementRowOutputCount(rowCount); - } - } - - File mergedFile = null; - try { - List indexes = Lists.newArrayList(); - for (FireHydrant fireHydrant : sink) { - Segment segment = fireHydrant.getSegment(); - final QueryableIndex queryableIndex = segment.asQueryableIndex(); - log.info("Adding hydrant[%s]", fireHydrant); - indexes.add(queryableIndex); - } - - mergedFile = IndexMerger.mergeQueryableIndex( - indexes, - schema.getAggregators(), - new File(computePersistDir(schema, interval), "merged") - ); - - QueryableIndex index = IndexIO.loadIndex(mergedFile); - - DataSegment segment = dataSegmentPusher.push( - mergedFile, - sink.getSegment().withDimensions(Lists.newArrayList(index.getAvailableDimensions())) - ); - - metadataUpdater.publishSegment(segment); - } - catch (IOException e) { - log.makeAlert(e, "Failed to persist merged index[%s]", schema.getDataSource()) - .addData("interval", interval) - .emit(); - } - - - if (mergedFile != null) { - try { - if (mergedFile != null) { - log.info("Deleting Index File[%s]", mergedFile); - FileUtils.deleteDirectory(mergedFile); - } - } - catch (IOException e) { - log.warn(e, "Error deleting directory[%s]", mergedFile); - } - } - } - } - ); - } - } - } - ); - return new Plumber() { + private volatile boolean stopped = false; + private volatile ExecutorService persistExecutor = null; + private volatile ScheduledExecutorService scheduledExecutor = null; + + private final Map sinks = Maps.newConcurrentMap(); + + @Override + public void startJob() + { + computeBaseDir(schema).mkdirs(); + initializeExecutors(); + bootstrapSinksFromDisk(); + registerServerViewCallback(); + startPersistThread(); + } + @Override public Sink getSink(long timestamp) { @@ -372,14 +192,15 @@ public class RealtimePlumberSchool implements PlumberSchool Sink retVal = sinks.get(truncatedTime); if (retVal == null) { - retVal = new Sink( - new Interval(new DateTime(truncatedTime), segmentGranularity.increment(new DateTime(truncatedTime))), - schema + final Interval sinkInterval = new Interval( + new DateTime(truncatedTime), + segmentGranularity.increment(new DateTime(truncatedTime)) ); + retVal = new Sink(sinkInterval, schema, versioningPolicy.getVersion(sinkInterval)); + try { metadataUpdater.announceSegment(retVal.getSegment()); - sinks.put(truncatedTime, retVal); } catch (IOException e) { @@ -408,7 +229,6 @@ public class RealtimePlumberSchool implements PlumberSchool } }; - return factory.mergeRunners( EXEC, FunctionalIterable @@ -473,83 +293,293 @@ public class RealtimePlumberSchool implements PlumberSchool @Override public void finishJob() { - throw new UnsupportedOperationException(); + stopped = true; + + for (final Sink sink : sinks.values()) { + try { + metadataUpdater.unannounceSegment(sink.getSegment()); + } + catch (Exception e) { + log.makeAlert("Failed to unannounce segment on shutdown") + .addData("segment", sink.getSegment()) + .emit(); + } + } + + // scheduledExecutor is shutdown here, but persistExecutor is shutdown when the + // ServerView sends it a new segment callback + + if (scheduledExecutor != null) { + scheduledExecutor.shutdown(); + } + } + + private void initializeExecutors() + { + if (persistExecutor == null) { + persistExecutor = Executors.newFixedThreadPool( + 1, + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("plumber_persist_%d") + .build() + ); + } + if (scheduledExecutor == null) { + scheduledExecutor = Executors.newScheduledThreadPool( + 1, + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("plumber_scheduled_%d") + .build() + ); + } + } + + private void bootstrapSinksFromDisk() + { + for (File sinkDir : computeBaseDir(schema).listFiles()) { + Interval sinkInterval = new Interval(sinkDir.getName().replace("_", "/")); + + final File[] sinkFiles = sinkDir.listFiles(); + Arrays.sort( + sinkFiles, + new Comparator() + { + @Override + public int compare(File o1, File o2) + { + try { + return Ints.compare(Integer.parseInt(o1.getName()), Integer.parseInt(o2.getName())); + } + catch (NumberFormatException e) { + log.error(e, "Couldn't compare as numbers? [%s][%s]", o1, o2); + return o1.compareTo(o2); + } + } + } + ); + + try { + List hydrants = Lists.newArrayList(); + for (File segmentDir : sinkFiles) { + log.info("Loading previously persisted segment at [%s]", segmentDir); + hydrants.add( + new FireHydrant( + new QueryableIndexSegment(null, IndexIO.loadIndex(segmentDir)), + Integer.parseInt(segmentDir.getName()) + ) + ); + } + + Sink currSink = new Sink(sinkInterval, schema, versioningPolicy.getVersion(sinkInterval), hydrants); + sinks.put(sinkInterval.getStartMillis(), currSink); + + metadataUpdater.announceSegment(currSink.getSegment()); + } + catch (IOException e) { + log.makeAlert(e, "Problem loading sink[%s] from disk.", schema.getDataSource()) + .addData("interval", sinkInterval) + .emit(); + } + } + } + + private void registerServerViewCallback() + { + serverView.registerSegmentCallback( + persistExecutor, + new ServerView.BaseSegmentCallback() + { + @Override + public ServerView.CallbackAction segmentAdded(DruidServer server, DataSegment segment) + { + if (stopped) { + log.info("Unregistering ServerViewCallback"); + persistExecutor.shutdown(); + return ServerView.CallbackAction.UNREGISTER; + } + + if ("realtime".equals(server.getType())) { + return ServerView.CallbackAction.CONTINUE; + } + + log.debug("Checking segment[%s] on server[%s]", segment, server); + if (schema.getDataSource().equals(segment.getDataSource())) { + final Interval interval = segment.getInterval(); + for (Map.Entry entry : sinks.entrySet()) { + final Long sinkKey = entry.getKey(); + if (interval.contains(sinkKey)) { + final Sink sink = entry.getValue(); + log.info("Segment matches sink[%s]", sink); + + if (segment.getVersion().compareTo(sink.getSegment().getVersion()) >= 0) { + try { + metadataUpdater.unannounceSegment(sink.getSegment()); + FileUtils.deleteDirectory(computePersistDir(schema, sink.getInterval())); + sinks.remove(sinkKey); + } + catch (IOException e) { + log.makeAlert(e, "Unable to delete old segment for dataSource[%s].", schema.getDataSource()) + .addData("interval", sink.getInterval()) + .emit(); + } + } + } + } + } + + return ServerView.CallbackAction.CONTINUE; + } + } + ); + } + + private void startPersistThread() + { + final long truncatedNow = segmentGranularity.truncate(new DateTime()).getMillis(); + final long windowMillis = windowPeriod.toStandardDuration().getMillis(); + + log.info( + "Expect to run at [%s]", + new DateTime().plus( + new Duration(System.currentTimeMillis(), segmentGranularity.increment(truncatedNow) + windowMillis) + ) + ); + + ScheduledExecutors + .scheduleAtFixedRate( + scheduledExecutor, + new Duration(System.currentTimeMillis(), segmentGranularity.increment(truncatedNow) + windowMillis), + new Duration(truncatedNow, segmentGranularity.increment(truncatedNow)), + new ThreadRenamingCallable( + String.format( + "%s-overseer-%d", + schema.getDataSource(), + schema.getShardSpec().getPartitionNum() + ) + ) + { + @Override + public ScheduledExecutors.Signal doCall() + { + if (stopped) { + log.info("Stopping merge-n-push overseer thread"); + return ScheduledExecutors.Signal.STOP; + } + + log.info("Starting merge and push."); + + long minTimestamp = segmentGranularity.truncate(rejectionPolicy.getCurrMaxTime()).getMillis() + - windowMillis; + + List> sinksToPush = Lists.newArrayList(); + for (Map.Entry entry : sinks.entrySet()) { + final Long intervalStart = entry.getKey(); + if (intervalStart < minTimestamp) { + log.info("Adding entry[%s] for merge and push.", entry); + sinksToPush.add(entry); + } + } + + for (final Map.Entry entry : sinksToPush) { + final Sink sink = entry.getValue(); + + final String threadName = String.format( + "%s-%s-persist-n-merge", schema.getDataSource(), new DateTime(entry.getKey()) + ); + persistExecutor.execute( + new ThreadRenamingRunnable(threadName) + { + @Override + public void doRun() + { + final Interval interval = sink.getInterval(); + + for (FireHydrant hydrant : sink) { + if (!hydrant.hasSwapped()) { + log.info("Hydrant[%s] hasn't swapped yet, swapping. Sink[%s]", hydrant, sink); + final int rowCount = persistHydrant(hydrant, schema, interval); + metrics.incrementRowOutputCount(rowCount); + } + } + + File mergedFile = null; + try { + List indexes = Lists.newArrayList(); + for (FireHydrant fireHydrant : sink) { + Segment segment = fireHydrant.getSegment(); + final QueryableIndex queryableIndex = segment.asQueryableIndex(); + log.info("Adding hydrant[%s]", fireHydrant); + indexes.add(queryableIndex); + } + + mergedFile = IndexMerger.mergeQueryableIndex( + indexes, + schema.getAggregators(), + new File(computePersistDir(schema, interval), "merged") + ); + + QueryableIndex index = IndexIO.loadIndex(mergedFile); + + DataSegment segment = dataSegmentPusher.push( + mergedFile, + sink.getSegment().withDimensions(Lists.newArrayList(index.getAvailableDimensions())) + ); + + metadataUpdater.publishSegment(segment); + } + catch (IOException e) { + log.makeAlert(e, "Failed to persist merged index[%s]", schema.getDataSource()) + .addData("interval", interval) + .emit(); + } + + + if (mergedFile != null) { + try { + if (mergedFile != null) { + log.info("Deleting Index File[%s]", mergedFile); + FileUtils.deleteDirectory(mergedFile); + } + } + catch (IOException e) { + log.warn(e, "Error deleting directory[%s]", mergedFile); + } + } + } + } + ); + } + + if (stopped) { + log.info("Stopping merge-n-push overseer thread"); + return ScheduledExecutors.Signal.STOP; + } else { + return ScheduledExecutors.Signal.REPEAT; + } + } + } + ); } }; } - private File computeBaseDir(Schema schema) + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes(value = { + @JsonSubTypes.Type(name = "intervalStart", value = IntervalStartVersioningPolicy.class) + }) + public static interface VersioningPolicy { - return new File(basePersistDirectory, schema.getDataSource()); + public String getVersion(Interval interval); } - private File computePersistDir(Schema schema, Interval interval) + public static class IntervalStartVersioningPolicy implements VersioningPolicy { - return new File(computeBaseDir(schema), interval.toString().replace("/", "_")); - } - - /** - * Persists the given hydrant and returns the number of rows persisted - * - * @param indexToPersist - * @param schema - * @param interval - * - * @return the number of rows persisted - */ - private int persistHydrant(FireHydrant indexToPersist, Schema schema, Interval interval) - { - log.info("DataSource[%s], Interval[%s], persisting Hydrant[%s]", schema.getDataSource(), interval, indexToPersist); - try { - int numRows = indexToPersist.getIndex().size(); - - File persistedFile = IndexMerger.persist( - indexToPersist.getIndex(), - new File(computePersistDir(schema, interval), String.valueOf(indexToPersist.getCount())) - ); - - indexToPersist.swapSegment(new QueryableIndexSegment(null, IndexIO.loadIndex(persistedFile))); - - return numRows; - } - catch (IOException e) { - log.makeAlert("dataSource[%s] -- incremental persist failed", schema.getDataSource()) - .addData("interval", interval) - .addData("count", indexToPersist.getCount()) - .emit(); - - throw Throwables.propagate(e); - } - } - - private void verifyState() - { - Preconditions.checkNotNull(conglomerate, "must specify a queryRunnerFactoryConglomerate to do this action."); - Preconditions.checkNotNull(dataSegmentPusher, "must specify a segmentPusher to do this action."); - Preconditions.checkNotNull(metadataUpdater, "must specify a metadataUpdater to do this action."); - Preconditions.checkNotNull(serverView, "must specify a serverView to do this action."); - Preconditions.checkNotNull(emitter, "must specify a serviceEmitter to do this action."); - } - - private void initializeExecutors() - { - if (persistExecutor == null) { - persistExecutor = Executors.newFixedThreadPool( - 1, - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("plumber_persist_%d") - .build() - ); - } - if (scheduledExecutor == null) { - scheduledExecutor = Executors.newScheduledThreadPool( - 1, - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("plumber_scheduled_%d") - .build() - ); + @Override + public String getVersion(Interval interval) + { + return interval.getStart().toString(); } } @@ -632,4 +662,57 @@ public class RealtimePlumberSchool implements PlumberSchool }; } } + + private File computeBaseDir(Schema schema) + { + return new File(basePersistDirectory, schema.getDataSource()); + } + + private File computePersistDir(Schema schema, Interval interval) + { + return new File(computeBaseDir(schema), interval.toString().replace("/", "_")); + } + + /** + * Persists the given hydrant and returns the number of rows persisted + * + * @param indexToPersist + * @param schema + * @param interval + * + * @return the number of rows persisted + */ + private int persistHydrant(FireHydrant indexToPersist, Schema schema, Interval interval) + { + log.info("DataSource[%s], Interval[%s], persisting Hydrant[%s]", schema.getDataSource(), interval, indexToPersist); + try { + int numRows = indexToPersist.getIndex().size(); + + File persistedFile = IndexMerger.persist( + indexToPersist.getIndex(), + new File(computePersistDir(schema, interval), String.valueOf(indexToPersist.getCount())) + ); + + indexToPersist.swapSegment(new QueryableIndexSegment(null, IndexIO.loadIndex(persistedFile))); + + return numRows; + } + catch (IOException e) { + log.makeAlert("dataSource[%s] -- incremental persist failed", schema.getDataSource()) + .addData("interval", interval) + .addData("count", indexToPersist.getCount()) + .emit(); + + throw Throwables.propagate(e); + } + } + + private void verifyState() + { + Preconditions.checkNotNull(conglomerate, "must specify a queryRunnerFactoryConglomerate to do this action."); + Preconditions.checkNotNull(dataSegmentPusher, "must specify a segmentPusher to do this action."); + Preconditions.checkNotNull(metadataUpdater, "must specify a metadataUpdater to do this action."); + Preconditions.checkNotNull(serverView, "must specify a serverView to do this action."); + Preconditions.checkNotNull(emitter, "must specify a serviceEmitter to do this action."); + } } diff --git a/realtime/src/main/java/com/metamx/druid/realtime/SegmentAnnouncer.java b/realtime/src/main/java/com/metamx/druid/realtime/SegmentAnnouncer.java new file mode 100644 index 00000000000..823a2e2a547 --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/SegmentAnnouncer.java @@ -0,0 +1,11 @@ +package com.metamx.druid.realtime; + +import com.metamx.druid.client.DataSegment; + +import java.io.IOException; + +public interface SegmentAnnouncer +{ + public void announceSegment(DataSegment segment) throws IOException; + public void unannounceSegment(DataSegment segment) throws IOException; +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/SegmentPublisher.java b/realtime/src/main/java/com/metamx/druid/realtime/SegmentPublisher.java new file mode 100644 index 00000000000..48315849921 --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/SegmentPublisher.java @@ -0,0 +1,10 @@ +package com.metamx.druid.realtime; + +import com.metamx.druid.client.DataSegment; + +import java.io.IOException; + +public interface SegmentPublisher +{ + public void publishSegment(DataSegment segment) throws IOException; +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/Sink.java b/realtime/src/main/java/com/metamx/druid/realtime/Sink.java index 42acc191b63..70e305b67e4 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/Sink.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/Sink.java @@ -50,16 +50,19 @@ public class Sink implements Iterable private final Interval interval; private final Schema schema; + private final String version; private final CopyOnWriteArrayList hydrants = new CopyOnWriteArrayList(); public Sink( Interval interval, - Schema schema + Schema schema, + String version ) { this.schema = schema; this.interval = interval; + this.version = version; makeNewCurrIndex(interval.getStartMillis(), schema); } @@ -67,11 +70,13 @@ public class Sink implements Iterable public Sink( Interval interval, Schema schema, + String version, List hydrants ) { this.schema = schema; this.interval = interval; + this.version = version; for (int i = 0; i < hydrants.size(); ++i) { final FireHydrant hydrant = hydrants.get(i); @@ -100,6 +105,13 @@ public class Sink implements Iterable } } + public boolean isEmpty() + { + synchronized (currIndex) { + return hydrants.size() == 1 && currIndex.getIndex().isEmpty(); + } + } + /** * If currIndex is A, creates a new index B, sets currIndex to B and returns A. * @@ -122,7 +134,7 @@ public class Sink implements Iterable return new DataSegment( schema.getDataSource(), interval, - interval.getStart().toString(), + version, ImmutableMap.of(), Lists.newArrayList(), Lists.transform( diff --git a/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java b/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java new file mode 100644 index 00000000000..624a7855363 --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java @@ -0,0 +1,104 @@ +package com.metamx.druid.realtime; + +import com.google.common.collect.ImmutableMap; +import com.metamx.common.lifecycle.LifecycleStart; +import com.metamx.common.lifecycle.LifecycleStop; +import com.metamx.common.logger.Logger; +import com.metamx.druid.client.DataSegment; +import com.metamx.phonebook.PhoneBook; +import org.joda.time.DateTime; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +public class ZkSegmentAnnouncer implements SegmentAnnouncer +{ + private static final Logger log = new Logger(ZkSegmentAnnouncer.class); + + private final Object lock = new Object(); + + private final MetadataUpdaterConfig config; + private final PhoneBook yp; + private final String servedSegmentsLocation; + + private volatile boolean started = false; + + public ZkSegmentAnnouncer( + MetadataUpdaterConfig config, + PhoneBook yp + ) + { + this.config = config; + this.yp = yp; + this.servedSegmentsLocation = yp.combineParts( + Arrays.asList( + config.getServedSegmentsLocation(), config.getServerName() + ) + ); + } + + public Map getStringProps() + { + return ImmutableMap.of( + "name", config.getServerName(), + "host", config.getHost(), + "maxSize", String.valueOf(config.getMaxSize()), + "type", "realtime" + ); + } + + @LifecycleStart + public void start() + { + synchronized (lock) { + if (started) { + return; + } + + log.info("Starting zkCoordinator for server[%s] with config[%s]", config.getServerName(), config); + if (yp.lookup(servedSegmentsLocation, Object.class) == null) { + yp.post( + config.getServedSegmentsLocation(), + config.getServerName(), + ImmutableMap.of("created", new DateTime().toString()) + ); + } + + yp.announce( + config.getAnnounceLocation(), + config.getServerName(), + getStringProps() + ); + + started = true; + } + } + + @LifecycleStop + public void stop() + { + synchronized (lock) { + if (!started) { + return; + } + + log.info("Stopping MetadataUpdater with config[%s]", config); + yp.unannounce(config.getAnnounceLocation(), config.getServerName()); + + started = false; + } + } + + public void announceSegment(DataSegment segment) throws IOException + { + log.info("Announcing realtime segment %s", segment.getIdentifier()); + yp.announce(servedSegmentsLocation, segment.getIdentifier(), segment); + } + + public void unannounceSegment(DataSegment segment) throws IOException + { + log.info("Unannouncing realtime segment %s", segment.getIdentifier()); + yp.unannounce(servedSegmentsLocation, segment.getIdentifier()); + } +} From 0e4db00d5448038274ad600748099b2ac28fd66e Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Mon, 11 Mar 2013 11:15:19 -0700 Subject: [PATCH 04/18] TaskQueue: Fix task ordering when bootstrapping --- .../com/metamx/druid/merger/coordinator/TaskQueue.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskQueue.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskQueue.java index e16912b4c6e..8dd44cb5131 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskQueue.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskQueue.java @@ -105,17 +105,17 @@ public class TaskQueue } // Sort locks by version - final Ordering byVersionOrdering = new Ordering() + final Ordering> byVersionOrdering = new Ordering>() { @Override - public int compare(TaskLock left, TaskLock right) + public int compare(Map.Entry left, Map.Entry right) { - return left.getVersion().compareTo(right.getVersion()); + return left.getKey().getVersion().compareTo(right.getKey().getVersion()); } }; // Acquire as many locks as possible, in version order - for(final Map.Entry taskAndLock : tasksByLock.entries()) { + for(final Map.Entry taskAndLock : byVersionOrdering.sortedCopy(tasksByLock.entries())) { final Task task = taskAndLock.getValue(); final TaskLock savedTaskLock = taskAndLock.getKey(); From 6245e389810b0f8c1a1bd89426b56993842bbc35 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Mon, 11 Mar 2013 11:22:36 -0700 Subject: [PATCH 05/18] RealtimeIndexTask-related stuff. - New task: RealtimeIndexTask - Add SegmentAnnouncer, ServerView, QueryRunnerFactoryConglomerate to TaskToolbox - Tasks can advertise ability to answer queries (through returning non-null from getQueryRunner) - WorkerTaskMonitor (the thing on a worker that tracks running tasks) is now a QuerySegmentWalker - LockAcquireAction is now blocking Assorted other changes. - TaskAction.perform throws IOException - TaskActions generally have better stringification - Renamed TaskMonitor -> WorkerTaskMonitor --- .../druid/merger/common/TaskToolbox.java | 27 ++ .../merger/common/TaskToolboxFactory.java | 15 + .../common/actions/LocalTaskActionClient.java | 4 +- .../common/actions/LockAcquireAction.java | 30 +- .../merger/common/actions/LockListAction.java | 12 +- .../common/actions/LockReleaseAction.java | 16 +- .../actions/RemoteTaskActionClient.java | 42 ++- .../common/actions/SegmentInsertAction.java | 58 ++-- .../actions/SegmentListUnusedAction.java | 23 +- .../common/actions/SegmentListUsedAction.java | 23 +- .../common/actions/SegmentNukeAction.java | 50 ++-- .../common/actions/SpawnTasksAction.java | 25 +- .../merger/common/actions/TaskAction.java | 4 +- .../common/actions/TaskActionClient.java | 4 +- .../common/index/YeOldePlumberSchool.java | 11 +- .../merger/common/task/AbstractTask.java | 8 + .../merger/common/task/MergeTaskBase.java | 64 +++-- .../merger/common/task/RealtimeIndexTask.java | 257 ++++++++++++++++++ .../metamx/druid/merger/common/task/Task.java | 9 + .../coordinator/MergerDBCoordinator.java | 54 ++-- .../druid/merger/coordinator/TaskLockbox.java | 26 ++ .../http/IndexerCoordinatorNode.java | 89 ++++-- .../http/IndexerCoordinatorResource.java | 16 +- ...askMonitor.java => WorkerTaskMonitor.java} | 72 ++++- .../druid/merger/worker/http/WorkerNode.java | 116 ++++++-- .../coordinator/RemoteTaskRunnerTest.java | 12 +- .../merger/coordinator/TaskLifecycleTest.java | 43 +-- .../merger/coordinator/TaskQueueTest.java | 6 + 28 files changed, 859 insertions(+), 257 deletions(-) create mode 100644 merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java rename merger/src/main/java/com/metamx/druid/merger/worker/{TaskMonitor.java => WorkerTaskMonitor.java} (74%) diff --git a/merger/src/main/java/com/metamx/druid/merger/common/TaskToolbox.java b/merger/src/main/java/com/metamx/druid/merger/common/TaskToolbox.java index e69b0f827e7..a09dacc1b39 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/TaskToolbox.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/TaskToolbox.java @@ -22,6 +22,7 @@ package com.metamx.druid.merger.common; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Maps; import com.metamx.druid.client.DataSegment; +import com.metamx.druid.client.MutableServerView; import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.loading.MMappedQueryableIndexFactory; import com.metamx.druid.loading.S3DataSegmentPuller; @@ -33,6 +34,8 @@ import com.metamx.druid.merger.common.actions.TaskActionClient; import com.metamx.druid.merger.common.actions.TaskActionClientFactory; import com.metamx.druid.merger.common.config.TaskConfig; import com.metamx.druid.merger.common.task.Task; +import com.metamx.druid.query.QueryRunnerFactoryConglomerate; +import com.metamx.druid.realtime.SegmentAnnouncer; import com.metamx.emitter.service.ServiceEmitter; import org.jets3t.service.impl.rest.httpclient.RestS3Service; @@ -52,6 +55,9 @@ public class TaskToolbox private final RestS3Service s3Client; private final DataSegmentPusher segmentPusher; private final DataSegmentKiller dataSegmentKiller; + private final SegmentAnnouncer segmentAnnouncer; + private final MutableServerView newSegmentServerView; + private final QueryRunnerFactoryConglomerate queryRunnerFactoryConglomerate; private final ObjectMapper objectMapper; public TaskToolbox( @@ -62,6 +68,9 @@ public class TaskToolbox RestS3Service s3Client, DataSegmentPusher segmentPusher, DataSegmentKiller dataSegmentKiller, + SegmentAnnouncer segmentAnnouncer, + MutableServerView newSegmentServerView, + QueryRunnerFactoryConglomerate queryRunnerFactoryConglomerate, ObjectMapper objectMapper ) { @@ -72,6 +81,9 @@ public class TaskToolbox this.s3Client = s3Client; this.segmentPusher = segmentPusher; this.dataSegmentKiller = dataSegmentKiller; + this.segmentAnnouncer = segmentAnnouncer; + this.newSegmentServerView = newSegmentServerView; + this.queryRunnerFactoryConglomerate = queryRunnerFactoryConglomerate; this.objectMapper = objectMapper; } @@ -100,6 +112,21 @@ public class TaskToolbox return dataSegmentKiller; } + public SegmentAnnouncer getSegmentAnnouncer() + { + return segmentAnnouncer; + } + + public MutableServerView getNewSegmentServerView() + { + return newSegmentServerView; + } + + public QueryRunnerFactoryConglomerate getQueryRunnerFactoryConglomerate() + { + return queryRunnerFactoryConglomerate; + } + public ObjectMapper getObjectMapper() { return objectMapper; diff --git a/merger/src/main/java/com/metamx/druid/merger/common/TaskToolboxFactory.java b/merger/src/main/java/com/metamx/druid/merger/common/TaskToolboxFactory.java index 2266860ea86..d7b85e3f141 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/TaskToolboxFactory.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/TaskToolboxFactory.java @@ -20,11 +20,14 @@ package com.metamx.druid.merger.common; import com.fasterxml.jackson.databind.ObjectMapper; +import com.metamx.druid.client.MutableServerView; import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.loading.DataSegmentKiller; import com.metamx.druid.merger.common.actions.TaskActionClientFactory; import com.metamx.druid.merger.common.config.TaskConfig; import com.metamx.druid.merger.common.task.Task; +import com.metamx.druid.query.QueryRunnerFactoryConglomerate; +import com.metamx.druid.realtime.SegmentAnnouncer; import com.metamx.emitter.service.ServiceEmitter; import org.jets3t.service.impl.rest.httpclient.RestS3Service; @@ -39,6 +42,9 @@ public class TaskToolboxFactory private final RestS3Service s3Client; private final DataSegmentPusher segmentPusher; private final DataSegmentKiller dataSegmentKiller; + private final SegmentAnnouncer segmentAnnouncer; + private final MutableServerView newSegmentServerView; + private final QueryRunnerFactoryConglomerate queryRunnerFactoryConglomerate; private final ObjectMapper objectMapper; public TaskToolboxFactory( @@ -48,6 +54,9 @@ public class TaskToolboxFactory RestS3Service s3Client, DataSegmentPusher segmentPusher, DataSegmentKiller dataSegmentKiller, + SegmentAnnouncer segmentAnnouncer, + MutableServerView newSegmentServerView, + QueryRunnerFactoryConglomerate queryRunnerFactoryConglomerate, ObjectMapper objectMapper ) { @@ -57,6 +66,9 @@ public class TaskToolboxFactory this.s3Client = s3Client; this.segmentPusher = segmentPusher; this.dataSegmentKiller = dataSegmentKiller; + this.segmentAnnouncer = segmentAnnouncer; + this.newSegmentServerView = newSegmentServerView; + this.queryRunnerFactoryConglomerate = queryRunnerFactoryConglomerate; this.objectMapper = objectMapper; } @@ -75,6 +87,9 @@ public class TaskToolboxFactory s3Client, segmentPusher, dataSegmentKiller, + segmentAnnouncer, + newSegmentServerView, + queryRunnerFactoryConglomerate, objectMapper ); } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java index e36dbf65a6c..e3ccc610741 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java @@ -4,6 +4,8 @@ import com.metamx.druid.merger.common.task.Task; import com.metamx.druid.merger.coordinator.TaskStorage; import com.metamx.emitter.EmittingLogger; +import java.io.IOException; + public class LocalTaskActionClient implements TaskActionClient { private final Task task; @@ -20,7 +22,7 @@ public class LocalTaskActionClient implements TaskActionClient } @Override - public RetType submit(TaskAction taskAction) + public RetType submit(TaskAction taskAction) throws IOException { final RetType ret = taskAction.perform(task, toolbox); diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockAcquireAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockAcquireAction.java index de325ba274f..0a353dc5024 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockAcquireAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockAcquireAction.java @@ -1,15 +1,14 @@ package com.metamx.druid.merger.common.actions; -import com.google.common.base.Optional; -import com.google.common.base.Throwables; -import com.metamx.druid.merger.common.TaskLock; -import com.metamx.druid.merger.common.task.Task; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.Throwables; +import com.metamx.druid.merger.common.TaskLock; +import com.metamx.druid.merger.common.task.Task; import org.joda.time.Interval; -public class LockAcquireAction implements TaskAction> +public class LockAcquireAction implements TaskAction { private final Interval interval; @@ -27,18 +26,29 @@ public class LockAcquireAction implements TaskAction> return interval; } - public TypeReference> getReturnTypeReference() + public TypeReference getReturnTypeReference() { - return new TypeReference>() {}; + return new TypeReference() + { + }; } @Override - public Optional perform(Task task, TaskActionToolbox toolbox) + public TaskLock perform(Task task, TaskActionToolbox toolbox) { try { - return toolbox.getTaskLockbox().tryLock(task, interval); - } catch (Exception e) { + return toolbox.getTaskLockbox().lock(task, interval); + } + catch (InterruptedException e) { throw Throwables.propagate(e); } } + + @Override + public String toString() + { + return "LockAcquireAction{" + + "interval=" + interval + + '}'; + } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockListAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockListAction.java index 06a2879ec47..2d58a883d93 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockListAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockListAction.java @@ -20,10 +20,12 @@ public class LockListAction implements TaskAction> @Override public List perform(Task task, TaskActionToolbox toolbox) { - try { - return toolbox.getTaskLockbox().findLocksForTask(task); - } catch (Exception e) { - throw Throwables.propagate(e); - } + return toolbox.getTaskLockbox().findLocksForTask(task); + } + + @Override + public String toString() + { + return "LockListAction{}"; } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java index b932e748ed1..3af6befb712 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java @@ -36,11 +36,15 @@ public class LockReleaseAction implements TaskAction @Override public Void perform(Task task, TaskActionToolbox toolbox) { - try { - toolbox.getTaskLockbox().unlock(task, interval); - return null; - } catch (Exception e) { - throw Throwables.propagate(e); - } + toolbox.getTaskLockbox().unlock(task, interval); + return null; + } + + @Override + public String toString() + { + return "LockReleaseAction{" + + "interval=" + interval + + '}'; } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java index 5cebc6ee1ec..6dd7246f9e1 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java @@ -11,9 +11,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.curator.x.discovery.ServiceInstance; import com.netflix.curator.x.discovery.ServiceProvider; +import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; import java.util.Map; +import java.util.concurrent.ExecutionException; public class RemoteTaskActionClient implements TaskActionClient { @@ -33,26 +34,37 @@ public class RemoteTaskActionClient implements TaskActionClient } @Override - public RetType submit(TaskAction taskAction) + public RetType submit(TaskAction taskAction) throws IOException { + byte[] dataToSend = jsonMapper.writeValueAsBytes(new TaskActionHolder(task, taskAction)); + + final URI serviceUri; try { - byte[] dataToSend = jsonMapper.writeValueAsBytes(new TaskActionHolder(task, taskAction)); - - final String response = httpClient.post(getServiceUri().toURL()) - .setContent("application/json", dataToSend) - .go(new ToStringResponseHandler(Charsets.UTF_8)) - .get(); - - final Map responseDict = jsonMapper.readValue( - response, - new TypeReference>() {} - ); - - return jsonMapper.convertValue(responseDict.get("result"), taskAction.getReturnTypeReference()); + serviceUri = getServiceUri(); } catch (Exception e) { + throw new IOException("Failed to locate service uri", e); + } + + final String response; + + try { + response = httpClient.post(serviceUri.toURL()) + .setContent("application/json", dataToSend) + .go(new ToStringResponseHandler(Charsets.UTF_8)) + .get(); + } + catch (Exception e) { + Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); throw Throwables.propagate(e); } + + final Map responseDict = jsonMapper.readValue( + response, + new TypeReference>() {} + ); + + return jsonMapper.convertValue(responseDict.get("result"), taskAction.getReturnTypeReference()); } private URI getServiceUri() throws Exception diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentInsertAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentInsertAction.java index 5354e14878c..b43d33a7f60 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentInsertAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentInsertAction.java @@ -1,22 +1,18 @@ package com.metamx.druid.merger.common.actions; -import com.google.common.base.Predicate; -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.metamx.common.ISE; -import com.metamx.druid.client.DataSegment; -import com.metamx.druid.merger.common.TaskLock; -import com.metamx.druid.merger.common.task.Task; -import com.metamx.emitter.service.ServiceMetricEvent; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.ImmutableSet; +import com.metamx.common.ISE; +import com.metamx.druid.client.DataSegment; +import com.metamx.druid.merger.common.task.Task; +import com.metamx.emitter.service.ServiceMetricEvent; -import java.util.List; +import java.io.IOException; import java.util.Set; -public class SegmentInsertAction implements TaskAction +public class SegmentInsertAction implements TaskAction> { private final Set segments; @@ -34,34 +30,38 @@ public class SegmentInsertAction implements TaskAction return segments; } - public TypeReference getReturnTypeReference() + public TypeReference> getReturnTypeReference() { - return new TypeReference() {}; + return new TypeReference>() {}; } @Override - public Void perform(Task task, TaskActionToolbox toolbox) + public Set perform(Task task, TaskActionToolbox toolbox) throws IOException { if(!toolbox.taskLockCoversSegments(task, segments, false)) { - throw new ISE("Segments not covered by locks for task: %s", task.getId()); + throw new ISE("Segments not covered by locks for task[%s]: %s", task.getId(), segments); } - try { - toolbox.getMergerDBCoordinator().announceHistoricalSegments(segments); + final Set retVal = toolbox.getMergerDBCoordinator().announceHistoricalSegments(segments); - // Emit metrics - final ServiceMetricEvent.Builder metricBuilder = new ServiceMetricEvent.Builder() - .setUser2(task.getDataSource()) - .setUser4(task.getType()); + // Emit metrics + final ServiceMetricEvent.Builder metricBuilder = new ServiceMetricEvent.Builder() + .setUser2(task.getDataSource()) + .setUser4(task.getType()); - for (DataSegment segment : segments) { - metricBuilder.setUser5(segment.getInterval().toString()); - toolbox.getEmitter().emit(metricBuilder.build("indexer/segment/bytes", segment.getSize())); - } - - return null; - } catch (Exception e) { - throw Throwables.propagate(e); + for (DataSegment segment : segments) { + metricBuilder.setUser5(segment.getInterval().toString()); + toolbox.getEmitter().emit(metricBuilder.build("indexer/segment/bytes", segment.getSize())); } + + return retVal; + } + + @Override + public String toString() + { + return "SegmentInsertAction{" + + "segments=" + segments + + '}'; } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUnusedAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUnusedAction.java index 56304533a68..c5cf8f306b5 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUnusedAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUnusedAction.java @@ -1,13 +1,13 @@ package com.metamx.druid.merger.common.actions; -import com.google.common.base.Throwables; -import com.metamx.druid.client.DataSegment; -import com.metamx.druid.merger.common.task.Task; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; +import com.metamx.druid.client.DataSegment; +import com.metamx.druid.merger.common.task.Task; import org.joda.time.Interval; +import java.io.IOException; import java.util.List; public class SegmentListUnusedAction implements TaskAction> @@ -43,12 +43,17 @@ public class SegmentListUnusedAction implements TaskAction> } @Override - public List perform(Task task, TaskActionToolbox toolbox) + public List perform(Task task, TaskActionToolbox toolbox) throws IOException { - try { - return toolbox.getMergerDBCoordinator().getUnusedSegmentsForInterval(dataSource, interval); - } catch (Exception e) { - throw Throwables.propagate(e); - } + return toolbox.getMergerDBCoordinator().getUnusedSegmentsForInterval(dataSource, interval); + } + + @Override + public String toString() + { + return "SegmentListUnusedAction{" + + "dataSource='" + dataSource + '\'' + + ", interval=" + interval + + '}'; } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUsedAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUsedAction.java index a776ed641cc..c2a3b8fbc3a 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUsedAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentListUsedAction.java @@ -1,13 +1,13 @@ package com.metamx.druid.merger.common.actions; -import com.google.common.base.Throwables; -import com.metamx.druid.client.DataSegment; -import com.metamx.druid.merger.common.task.Task; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; +import com.metamx.druid.client.DataSegment; +import com.metamx.druid.merger.common.task.Task; import org.joda.time.Interval; +import java.io.IOException; import java.util.List; public class SegmentListUsedAction implements TaskAction> @@ -43,12 +43,17 @@ public class SegmentListUsedAction implements TaskAction> } @Override - public List perform(Task task, TaskActionToolbox toolbox) + public List perform(Task task, TaskActionToolbox toolbox) throws IOException { - try { - return toolbox.getMergerDBCoordinator().getUsedSegmentsForInterval(dataSource, interval); - } catch (Exception e) { - throw Throwables.propagate(e); - } + return toolbox.getMergerDBCoordinator().getUsedSegmentsForInterval(dataSource, interval); + } + + @Override + public String toString() + { + return "SegmentListUsedAction{" + + "dataSource='" + dataSource + '\'' + + ", interval=" + interval + + '}'; } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentNukeAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentNukeAction.java index 2ebedec0daf..c4b2a2f7044 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentNukeAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/SegmentNukeAction.java @@ -1,19 +1,15 @@ package com.metamx.druid.merger.common.actions; -import com.google.common.base.Predicate; -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.metamx.common.ISE; -import com.metamx.druid.client.DataSegment; -import com.metamx.druid.merger.common.TaskLock; -import com.metamx.druid.merger.common.task.Task; -import com.metamx.emitter.service.ServiceMetricEvent; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.ImmutableSet; +import com.metamx.common.ISE; +import com.metamx.druid.client.DataSegment; +import com.metamx.druid.merger.common.task.Task; +import com.metamx.emitter.service.ServiceMetricEvent; -import java.util.List; +import java.io.IOException; import java.util.Set; public class SegmentNukeAction implements TaskAction @@ -40,28 +36,32 @@ public class SegmentNukeAction implements TaskAction } @Override - public Void perform(Task task, TaskActionToolbox toolbox) + public Void perform(Task task, TaskActionToolbox toolbox) throws IOException { if(!toolbox.taskLockCoversSegments(task, segments, true)) { throw new ISE("Segments not covered by locks for task: %s", task.getId()); } - try { - toolbox.getMergerDBCoordinator().deleteSegments(segments); + toolbox.getMergerDBCoordinator().deleteSegments(segments); - // Emit metrics - final ServiceMetricEvent.Builder metricBuilder = new ServiceMetricEvent.Builder() - .setUser2(task.getDataSource()) - .setUser4(task.getType()); + // Emit metrics + final ServiceMetricEvent.Builder metricBuilder = new ServiceMetricEvent.Builder() + .setUser2(task.getDataSource()) + .setUser4(task.getType()); - for (DataSegment segment : segments) { - metricBuilder.setUser5(segment.getInterval().toString()); - toolbox.getEmitter().emit(metricBuilder.build("indexer/segmentNuked/bytes", segment.getSize())); - } - - return null; - } catch (Exception e) { - throw Throwables.propagate(e); + for (DataSegment segment : segments) { + metricBuilder.setUser5(segment.getInterval().toString()); + toolbox.getEmitter().emit(metricBuilder.build("indexer/segmentNuked/bytes", segment.getSize())); } + + return null; + } + + @Override + public String toString() + { + return "SegmentNukeAction{" + + "segments=" + segments + + '}'; } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/SpawnTasksAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/SpawnTasksAction.java index ec48430c49a..6f0c7402640 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/SpawnTasksAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/SpawnTasksAction.java @@ -1,11 +1,10 @@ package com.metamx.druid.merger.common.actions; -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; -import com.metamx.druid.merger.common.task.Task; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.ImmutableList; +import com.metamx.druid.merger.common.task.Task; import java.util.List; @@ -35,14 +34,18 @@ public class SpawnTasksAction implements TaskAction @Override public Void perform(Task task, TaskActionToolbox toolbox) { - try { - for(final Task newTask : newTasks) { - toolbox.getTaskQueue().add(newTask); - } - - return null; - } catch (Exception e) { - throw Throwables.propagate(e); + for(final Task newTask : newTasks) { + toolbox.getTaskQueue().add(newTask); } + + return null; + } + + @Override + public String toString() + { + return "SpawnTasksAction{" + + "newTasks=" + newTasks + + '}'; } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskAction.java index 019b14a3b62..dac6fce597f 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskAction.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.type.TypeReference; +import java.io.IOException; + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes(value = { @JsonSubTypes.Type(name = "lockAcquire", value = LockAcquireAction.class), @@ -19,5 +21,5 @@ import com.fasterxml.jackson.core.type.TypeReference; public interface TaskAction { public TypeReference getReturnTypeReference(); // T_T - public RetType perform(Task task, TaskActionToolbox toolbox); + public RetType perform(Task task, TaskActionToolbox toolbox) throws IOException; } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskActionClient.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskActionClient.java index 7baa08fe788..1f0366c6a56 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskActionClient.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/TaskActionClient.java @@ -1,6 +1,8 @@ package com.metamx.druid.merger.common.actions; +import java.io.IOException; + public interface TaskActionClient { - public RetType submit(TaskAction taskAction); + public RetType submit(TaskAction taskAction) throws IOException; } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java b/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java index c26888c4485..777fc3dd378 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java @@ -84,16 +84,22 @@ public class YeOldePlumberSchool implements PlumberSchool public Plumber findPlumber(final Schema schema, final FireDepartmentMetrics metrics) { // There can be only one. - final Sink theSink = new Sink(interval, schema); + final Sink theSink = new Sink(interval, schema, version); // Temporary directory to hold spilled segments. - final File persistDir = new File(tmpSegmentDir, theSink.getSegment().withVersion(version).getIdentifier()); + final File persistDir = new File(tmpSegmentDir, theSink.getSegment().getIdentifier()); // Set of spilled segments. Will be merged at the end. final Set spilled = Sets.newHashSet(); return new Plumber() { + @Override + public void startJob() + { + + } + @Override public Sink getSink(long timestamp) { @@ -146,7 +152,6 @@ public class YeOldePlumberSchool implements PlumberSchool final DataSegment segmentToUpload = theSink.getSegment() .withDimensions(ImmutableList.copyOf(mappedSegment.getAvailableDimensions())) - .withVersion(version) .withBinaryVersion(IndexIO.getVersionFromDir(fileToUpload)); dataSegmentPusher.push(fileToUpload, segmentToUpload); diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/AbstractTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/AbstractTask.java index 518fb04ab37..eeec7d3651d 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/AbstractTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/AbstractTask.java @@ -24,9 +24,11 @@ import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.metamx.druid.Query; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.TaskToolbox; import com.metamx.druid.merger.common.actions.SegmentListUsedAction; +import com.metamx.druid.query.QueryRunner; import org.joda.time.Interval; public abstract class AbstractTask implements Task @@ -79,6 +81,12 @@ public abstract class AbstractTask implements Task return interval; } + @Override + public QueryRunner getQueryRunner(Query query) + { + return null; + } + @Override public TaskStatus preflight(TaskToolbox toolbox) throws Exception { diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/MergeTaskBase.java b/merger/src/main/java/com/metamx/druid/merger/common/task/MergeTaskBase.java index 4bda0363941..de4989436a1 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/MergeTaskBase.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/MergeTaskBase.java @@ -26,6 +26,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -48,6 +49,7 @@ import org.joda.time.Interval; import javax.annotation.Nullable; import java.io.File; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; @@ -181,38 +183,42 @@ public abstract class MergeTaskBase extends AbstractTask @Override public TaskStatus preflight(TaskToolbox toolbox) { - final Function toIdentifier = new Function() - { - @Override - public String apply(DataSegment dataSegment) + try { + final Function toIdentifier = new Function() { - return dataSegment.getIdentifier(); + @Override + public String apply(DataSegment dataSegment) + { + return dataSegment.getIdentifier(); + } + }; + + final Set current = ImmutableSet.copyOf( + Iterables.transform(toolbox.getTaskActionClient().submit(defaultListUsedAction()), toIdentifier) + ); + final Set requested = ImmutableSet.copyOf(Iterables.transform(segments, toIdentifier)); + + final Set missingFromRequested = Sets.difference(current, requested); + if (!missingFromRequested.isEmpty()) { + throw new ISE( + "Merge is invalid: current segment(s) are not in the requested set: %s", + Joiner.on(", ").join(missingFromRequested) + ); } - }; - final Set current = ImmutableSet.copyOf( - Iterables.transform(toolbox.getTaskActionClient().submit(defaultListUsedAction()), toIdentifier) - ); - final Set requested = ImmutableSet.copyOf(Iterables.transform(segments, toIdentifier)); + final Set missingFromCurrent = Sets.difference(requested, current); + if (!missingFromCurrent.isEmpty()) { + throw new ISE( + "Merge is invalid: requested segment(s) are not in the current set: %s", + Joiner.on(", ").join(missingFromCurrent) + ); + } - final Set missingFromRequested = Sets.difference(current, requested); - if (!missingFromRequested.isEmpty()) { - throw new ISE( - "Merge is invalid: current segment(s) are not in the requested set: %s", - Joiner.on(", ").join(missingFromRequested) - ); + return TaskStatus.running(getId()); } - - final Set missingFromCurrent = Sets.difference(requested, current); - if (!missingFromCurrent.isEmpty()) { - throw new ISE( - "Merge is invalid: requested segment(s) are not in the current set: %s", - Joiner.on(", ").join(missingFromCurrent) - ); + catch (IOException e) { + throw Throwables.propagate(e); } - - return TaskStatus.running(getId()); - } protected abstract File merge(Map segments, File outDir) @@ -270,12 +276,12 @@ public abstract class MergeTaskBase extends AbstractTask DateTime start = null; DateTime end = null; - for(final DataSegment segment : segments) { - if(start == null || segment.getInterval().getStart().isBefore(start)) { + for (final DataSegment segment : segments) { + if (start == null || segment.getInterval().getStart().isBefore(start)) { start = segment.getInterval().getStart(); } - if(end == null || segment.getInterval().getEnd().isAfter(end)) { + if (end == null || segment.getInterval().getEnd().isAfter(end)) { end = segment.getInterval().getEnd(); } } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java new file mode 100644 index 00000000000..0a7696d0802 --- /dev/null +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java @@ -0,0 +1,257 @@ +package com.metamx.druid.merger.common.task; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Closeables; +import com.metamx.common.exception.FormattedException; +import com.metamx.druid.Query; +import com.metamx.druid.client.DataSegment; +import com.metamx.druid.index.v1.IndexGranularity; +import com.metamx.druid.input.InputRow; +import com.metamx.druid.merger.common.TaskLock; +import com.metamx.druid.merger.common.TaskStatus; +import com.metamx.druid.merger.common.TaskToolbox; +import com.metamx.druid.merger.common.actions.LockAcquireAction; +import com.metamx.druid.merger.common.actions.LockReleaseAction; +import com.metamx.druid.merger.common.actions.SegmentInsertAction; +import com.metamx.druid.query.QueryRunner; +import com.metamx.druid.realtime.FireDepartmentConfig; +import com.metamx.druid.realtime.FireDepartmentMetrics; +import com.metamx.druid.realtime.Firehose; +import com.metamx.druid.realtime.FirehoseFactory; +import com.metamx.druid.realtime.MetadataUpdater; +import com.metamx.druid.realtime.Plumber; +import com.metamx.druid.realtime.RealtimePlumberSchool; +import com.metamx.druid.realtime.Schema; +import com.metamx.druid.realtime.SegmentAnnouncer; +import com.metamx.druid.realtime.SegmentPublisher; +import com.metamx.druid.realtime.Sink; +import com.metamx.emitter.EmittingLogger; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import java.io.File; +import java.io.IOException; + +public class RealtimeIndexTask extends AbstractTask +{ + @JsonProperty + final Schema schema; + + @JsonProperty + final FirehoseFactory firehoseFactory; + + @JsonProperty + final FireDepartmentConfig fireDepartmentConfig; + + @JsonProperty + final Period windowPeriod; + + @JsonProperty + final IndexGranularity segmentGranularity; + + private volatile Plumber plumber = null; + + private static final EmittingLogger log = new EmittingLogger(RealtimeIndexTask.class); + + @JsonCreator + public RealtimeIndexTask( + @JsonProperty("schema") Schema schema, + @JsonProperty("firehose") FirehoseFactory firehoseFactory, + @JsonProperty("fireDepartmentConfig") FireDepartmentConfig fireDepartmentConfig, // TODO rename? + @JsonProperty("windowPeriod") Period windowPeriod, + @JsonProperty("segmentGranularity") IndexGranularity segmentGranularity + ) + { + super( + String.format( + "index_realtime_%s_%d_%s", + schema.getDataSource(), schema.getShardSpec().getPartitionNum(), new DateTime() + ), + String.format( + "index_realtime_%s", + schema.getDataSource() + ), + schema.getDataSource(), + null + ); + + this.schema = schema; + this.firehoseFactory = firehoseFactory; + this.fireDepartmentConfig = fireDepartmentConfig; + this.windowPeriod = windowPeriod; + this.segmentGranularity = segmentGranularity; + } + + @Override + public String getType() + { + return "index_realtime"; + } + + @Override + public QueryRunner getQueryRunner(Query query) + { + if (plumber != null) { + return plumber.getQueryRunner(query); + } else { + return null; + } + } + + @Override + public TaskStatus run(final TaskToolbox toolbox) throws Exception + { + if (this.plumber != null) { + throw new IllegalStateException("WTF?!? run with non-null plumber??!"); + } + + boolean normalExit = true; + + final FireDepartmentMetrics metrics = new FireDepartmentMetrics(); + final Period intermediatePersistPeriod = fireDepartmentConfig.getIntermediatePersistPeriod(); + final Firehose firehose = firehoseFactory.connect(); + + // TODO Take PlumberSchool in constructor (although that will need jackson injectables for stuff like + // TODO the ServerView, which seems kind of odd?) + final RealtimePlumberSchool realtimePlumberSchool = new RealtimePlumberSchool( + windowPeriod, + new File(toolbox.getTaskDir(), "persist"), + segmentGranularity + ); + + final SegmentPublisher segmentPublisher = new TaskActionSegmentPublisher(this, toolbox); + + // Wrap default SegmentAnnouncer such that we unlock intervals as we unannounce segments + final SegmentAnnouncer lockingSegmentAnnouncer = new SegmentAnnouncer() + { + @Override + public void announceSegment(final DataSegment segment) throws IOException + { + toolbox.getSegmentAnnouncer().announceSegment(segment); + } + + @Override + public void unannounceSegment(final DataSegment segment) throws IOException + { + try { + toolbox.getSegmentAnnouncer().unannounceSegment(segment); + } finally { + toolbox.getTaskActionClient().submit(new LockReleaseAction(segment.getInterval())); + } + } + }; + + // TODO -- This can block if there is lock contention, which will block plumber.getSink (and thus the firehose) + // TODO -- Shouldn't usually be bad, since we don't expect people to submit tasks that intersect with the + // TODO -- realtime window, but if they do it can be problematic + final RealtimePlumberSchool.VersioningPolicy versioningPolicy = new RealtimePlumberSchool.VersioningPolicy() + { + @Override + public String getVersion(final Interval interval) + { + try { + // NOTE: Side effect: Calling getVersion causes a lock to be acquired + final TaskLock myLock = toolbox.getTaskActionClient() + .submit(new LockAcquireAction(interval)); + + return myLock.getVersion(); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + }; + + // TODO - Might need to have task id in segmentPusher path or some other way of making redundant realtime + // TODO - workers not step on each other's toes when pushing segments to S3 + realtimePlumberSchool.setDataSegmentPusher(toolbox.getSegmentPusher()); + realtimePlumberSchool.setConglomerate(toolbox.getQueryRunnerFactoryConglomerate()); + realtimePlumberSchool.setVersioningPolicy(versioningPolicy); + realtimePlumberSchool.setMetadataUpdater(new MetadataUpdater(lockingSegmentAnnouncer, segmentPublisher)); + realtimePlumberSchool.setServerView(toolbox.getNewSegmentServerView()); + realtimePlumberSchool.setServiceEmitter(toolbox.getEmitter()); + + this.plumber = realtimePlumberSchool.findPlumber(schema, metrics); + + try { + plumber.startJob(); + + long nextFlush = new DateTime().plus(intermediatePersistPeriod).getMillis(); + while (firehose.hasMore()) { + final InputRow inputRow; + try { + inputRow = firehose.nextRow(); + + final Sink sink = plumber.getSink(inputRow.getTimestampFromEpoch()); + if (sink == null) { + metrics.incrementThrownAway(); + log.debug("Throwing away event[%s]", inputRow); + + if (System.currentTimeMillis() > nextFlush) { + plumber.persist(firehose.commit()); + nextFlush = new DateTime().plus(intermediatePersistPeriod).getMillis(); + } + + continue; + } + + if (sink.isEmpty()) { + log.info("Task %s: New sink: %s", getId(), sink); + } + + int currCount = sink.add(inputRow); + metrics.incrementProcessed(); + if (currCount >= fireDepartmentConfig.getMaxRowsInMemory() || System.currentTimeMillis() > nextFlush) { + plumber.persist(firehose.commit()); + nextFlush = new DateTime().plus(intermediatePersistPeriod).getMillis(); + } + } + catch (FormattedException e) { + log.warn(e, "unparseable line"); + metrics.incrementUnparseable(); + } + } + } + catch (Exception e) { + log.makeAlert(e, "Exception aborted realtime processing[%s]", schema.getDataSource()) + .emit(); + normalExit = false; + throw Throwables.propagate(e); + } + finally { + Closeables.closeQuietly(firehose); + + if (normalExit) { + try { + plumber.persist(firehose.commit()); + plumber.finishJob(); + } catch(Exception e) { + log.makeAlert(e, "Failed to finish realtime task").emit(); + } + } + } + + return TaskStatus.success(getId()); + } + + public static class TaskActionSegmentPublisher implements SegmentPublisher + { + final Task task; + final TaskToolbox taskToolbox; + + public TaskActionSegmentPublisher(Task task, TaskToolbox taskToolbox) + { + this.task = task; + this.taskToolbox = taskToolbox; + } + + @Override + public void publishSegment(DataSegment segment) throws IOException + { + taskToolbox.getTaskActionClient().submit(new SegmentInsertAction(ImmutableSet.of(segment))); + } + } +} diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/Task.java b/merger/src/main/java/com/metamx/druid/merger/common/task/Task.java index 5f288be99dc..e6922680fa7 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/Task.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/Task.java @@ -22,8 +22,10 @@ package com.metamx.druid.merger.common.task; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.base.Optional; +import com.metamx.druid.Query; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.TaskToolbox; +import com.metamx.druid.query.QueryRunner; import org.joda.time.Interval; /** @@ -51,6 +53,7 @@ import org.joda.time.Interval; @JsonSubTypes.Type(name = "index_partitions", value = IndexDeterminePartitionsTask.class), @JsonSubTypes.Type(name = "index_generator", value = IndexGeneratorTask.class), @JsonSubTypes.Type(name = "index_hadoop", value = HadoopIndexTask.class), + @JsonSubTypes.Type(name = "index_realtime", value = RealtimeIndexTask.class), @JsonSubTypes.Type(name = "version_converter", value = VersionConverterTask.class), @JsonSubTypes.Type(name = "version_converter_sub", value = VersionConverterTask.SubTask.class) }) @@ -83,6 +86,12 @@ public interface Task */ public Optional getImplicitLockInterval(); + /** + * Returns query runners for this task. If this task is not meant to answer queries over its datasource, this method + * should return null. + */ + public QueryRunner getQueryRunner(Query query); + /** * Execute preflight checks for a task. This typically runs on the coordinator, and will be run while * holding a lock on our dataSource and implicit lock interval (if any). If this method throws an exception, the diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/MergerDBCoordinator.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/MergerDBCoordinator.java index 9338bc930c9..d2a63bad26d 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/MergerDBCoordinator.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/MergerDBCoordinator.java @@ -22,8 +22,10 @@ package com.metamx.druid.merger.coordinator; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; +import com.google.common.collect.Sets; import com.metamx.common.logger.Logger; import com.metamx.druid.TimelineObjectHolder; import com.metamx.druid.VersionedIntervalTimeline; @@ -71,13 +73,11 @@ public class MergerDBCoordinator public List getUsedSegmentsForInterval(final String dataSource, final Interval interval) throws IOException { - // XXX Could be reading from a cache if we can assume we're the only one editing the DB - final VersionedIntervalTimeline timeline = dbi.withHandle( new HandleCallback>() { @Override - public VersionedIntervalTimeline withHandle(Handle handle) throws Exception + public VersionedIntervalTimeline withHandle(Handle handle) throws IOException { final VersionedIntervalTimeline timeline = new VersionedIntervalTimeline( Ordering.natural() @@ -129,31 +129,47 @@ public class MergerDBCoordinator return segments; } - public void announceHistoricalSegments(final Set segments) throws Exception + /** + * Attempts to insert a set of segments to the database. Returns the set of segments actually added (segments + * with identifiers already in the database will not be added). + * + * @param segments set of segments to add + * @return set of segments actually added + */ + public Set announceHistoricalSegments(final Set segments) throws IOException { - dbi.inTransaction( - new TransactionCallback() + return dbi.inTransaction( + new TransactionCallback>() { @Override - public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + public Set inTransaction(Handle handle, TransactionStatus transactionStatus) throws IOException { - for(final DataSegment segment : segments) { - announceHistoricalSegment(handle, segment); + final Set inserted = Sets.newHashSet(); + + for (final DataSegment segment : segments) { + if (announceHistoricalSegment(handle, segment)) { + inserted.add(segment); + } } - return null; + return ImmutableSet.copyOf(inserted); } } ); } - - private void announceHistoricalSegment(final Handle handle, final DataSegment segment) throws Exception + /** + * Attempts to insert a single segment to the database. If the segment already exists, will do nothing. Meant + * to be called from within a transaction. + * + * @return true if the segment was added, false otherwise + */ + private boolean announceHistoricalSegment(final Handle handle, final DataSegment segment) throws IOException { try { final List> exists = handle.createQuery( String.format( - "SELECT id FROM %s WHERE id = ':identifier'", + "SELECT id FROM %s WHERE id = :identifier", dbConnectorConfig.getSegmentTable() ) ).bind( @@ -163,7 +179,7 @@ public class MergerDBCoordinator if (!exists.isEmpty()) { log.info("Found [%s] in DB, not updating DB", segment.getIdentifier()); - return; + return false; } handle.createStatement( @@ -185,19 +201,21 @@ public class MergerDBCoordinator log.info("Published segment [%s] to DB", segment.getIdentifier()); } - catch (Exception e) { + catch (IOException e) { log.error(e, "Exception inserting into DB"); throw e; } + + return true; } - public void deleteSegments(final Set segments) throws Exception + public void deleteSegments(final Set segments) throws IOException { dbi.inTransaction( new TransactionCallback() { @Override - public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws IOException { for(final DataSegment segment : segments) { deleteSegment(handle, segment); @@ -223,7 +241,7 @@ public class MergerDBCoordinator new HandleCallback>() { @Override - public List withHandle(Handle handle) throws Exception + public List withHandle(Handle handle) throws IOException { return handle.createQuery( String.format( diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskLockbox.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskLockbox.java index 0a4bd925d4d..811429b0a05 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskLockbox.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskLockbox.java @@ -45,6 +45,7 @@ import java.util.NavigableMap; import java.util.NavigableSet; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** @@ -58,6 +59,7 @@ public class TaskLockbox private final Map> running = Maps.newHashMap(); private final TaskStorage taskStorage; private final ReentrantLock giant = new ReentrantLock(); + private final Condition lockReleaseCondition = giant.newCondition(); private static final EmittingLogger log = new EmittingLogger(TaskLockbox.class); @@ -66,6 +68,27 @@ public class TaskLockbox this.taskStorage = taskStorage; } + /** + * Locks a task without removing it from the queue. Blocks until the lock is acquired. Throws an exception + * if the lock cannot be acquired. + */ + public TaskLock lock(final Task task, final Interval interval) throws InterruptedException + { + giant.lock(); + + try { + Optional taskLock; + + while (!(taskLock = tryLock(task, interval)).isPresent()) { + lockReleaseCondition.await(); + } + + return taskLock.get(); + } finally { + giant.unlock(); + } + } + /** * Attempt to lock a task, without removing it from the queue. Equivalent to the long form of {@code tryLock} * with no preferred version. @@ -241,6 +264,9 @@ public class TaskLockbox running.remove(dataSource); } + // Wake up blocking-lock waiters + lockReleaseCondition.signalAll(); + // Best effort to remove lock from storage try { taskStorage.removeLock(task.getId(), taskLock); diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java index a83f0713075..17808363311 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java @@ -23,6 +23,7 @@ import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.ec2.AmazonEC2Client; import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.google.common.base.Charsets; import com.google.common.base.Throwables; import com.google.common.collect.Lists; @@ -38,14 +39,18 @@ import com.metamx.common.lifecycle.Lifecycle; import com.metamx.common.lifecycle.LifecycleStart; import com.metamx.common.lifecycle.LifecycleStop; import com.metamx.common.logger.Logger; +import com.metamx.druid.BaseServerNode; import com.metamx.druid.RegisteringNode; +import com.metamx.druid.client.ClientConfig; +import com.metamx.druid.client.ClientInventoryManager; +import com.metamx.druid.client.MutableServerView; +import com.metamx.druid.client.OnlyNewSegmentWatcherServerView; import com.metamx.druid.config.ConfigManager; import com.metamx.druid.config.ConfigManagerConfig; import com.metamx.druid.config.JacksonConfigManager; import com.metamx.druid.db.DbConnector; import com.metamx.druid.db.DbConnectorConfig; import com.metamx.druid.http.GuiceServletConfig; -import com.metamx.druid.http.MasterMain; import com.metamx.druid.http.RedirectFilter; import com.metamx.druid.http.RedirectInfo; import com.metamx.druid.http.StatusServlet; @@ -90,6 +95,10 @@ import com.metamx.druid.merger.coordinator.scaling.ResourceManagementSchedulerFa import com.metamx.druid.merger.coordinator.scaling.SimpleResourceManagementStrategy; import com.metamx.druid.merger.coordinator.scaling.SimpleResourceManagmentConfig; import com.metamx.druid.merger.coordinator.setup.WorkerSetupData; +import com.metamx.druid.merger.worker.http.WorkerNode; +import com.metamx.druid.realtime.MetadataUpdaterConfig; +import com.metamx.druid.realtime.SegmentAnnouncer; +import com.metamx.druid.realtime.ZkSegmentAnnouncer; import com.metamx.druid.utils.PropUtils; import com.metamx.emitter.EmittingLogger; import com.metamx.emitter.core.Emitters; @@ -128,7 +137,7 @@ import java.util.concurrent.atomic.AtomicReference; /** */ -public class IndexerCoordinatorNode extends RegisteringNode +public class IndexerCoordinatorNode extends BaseServerNode { private static final Logger log = new Logger(IndexerCoordinatorNode.class); @@ -137,7 +146,6 @@ public class IndexerCoordinatorNode extends RegisteringNode return new Builder(); } - private final ObjectMapper jsonMapper; private final Lifecycle lifecycle; private final Properties props; private final ConfigurationObjectFactory configFactory; @@ -161,20 +169,21 @@ public class IndexerCoordinatorNode extends RegisteringNode private TaskRunnerFactory taskRunnerFactory = null; private ResourceManagementSchedulerFactory resourceManagementSchedulerFactory = null; private TaskMasterLifecycle taskMasterLifecycle = null; + private MutableServerView newSegmentServerView = null; private Server server = null; private boolean initialized = false; public IndexerCoordinatorNode( - ObjectMapper jsonMapper, - Lifecycle lifecycle, Properties props, + Lifecycle lifecycle, + ObjectMapper jsonMapper, + ObjectMapper smileMapper, ConfigurationObjectFactory configFactory ) { - super(Arrays.asList(jsonMapper)); + super(log, props, lifecycle, jsonMapper, smileMapper, configFactory); - this.jsonMapper = jsonMapper; this.lifecycle = lifecycle; this.props = props; this.configFactory = configFactory; @@ -198,6 +207,12 @@ public class IndexerCoordinatorNode extends RegisteringNode return this; } + public IndexerCoordinatorNode setNewSegmentServerView(MutableServerView newSegmentServerView) + { + this.newSegmentServerView = newSegmentServerView; + return this; + } + public IndexerCoordinatorNode setS3Service(RestS3Service s3Service) { this.s3Service = s3Service; @@ -240,7 +255,7 @@ public class IndexerCoordinatorNode extends RegisteringNode return this; } - public void init() throws Exception + public void doInit() throws Exception { scheduledExecutorFactory = ScheduledExecutors.createFactory(lifecycle); initializeDB(); @@ -254,7 +269,7 @@ public class IndexerCoordinatorNode extends RegisteringNode dbi, managerConfig ) - ), jsonMapper + ), getJsonMapper() ); initializeEmitter(); @@ -263,6 +278,7 @@ public class IndexerCoordinatorNode extends RegisteringNode initializeTaskConfig(); initializeS3Service(); initializeMergeDBCoordinator(); + initializeNewSegmentServerView(); initializeTaskStorage(); initializeTaskLockbox(); initializeTaskQueue(); @@ -288,7 +304,7 @@ public class IndexerCoordinatorNode extends RegisteringNode final Injector injector = Guice.createInjector( new IndexerCoordinatorServletModule( - jsonMapper, + getJsonMapper(), config, emitter, taskMasterLifecycle, @@ -306,6 +322,9 @@ public class IndexerCoordinatorNode extends RegisteringNode }); staticContext.setBaseResource(resourceCollection); + // TODO -- Need a QueryServlet and some kind of QuerySegmentWalker if we want to support querying tasks + // TODO -- (e.g. for realtime) in local mode + final Context root = new Context(server, "/", Context.SESSIONS); root.addServlet(new ServletHolder(new StatusServlet()), "/status"); root.addServlet(new ServletHolder(new DefaultServlet()), "/mmx/*"); @@ -419,12 +438,12 @@ public class IndexerCoordinatorNode extends RegisteringNode injectables.addValue("s3Client", s3Service) .addValue("segmentPusher", segmentPusher); - jsonMapper.setInjectableValues(injectables); + getJsonMapper().setInjectableValues(injectables); } private void initializeJacksonSubtypes() { - jsonMapper.registerSubtypes(StaticS3FirehoseFactory.class); + getJsonMapper().registerSubtypes(StaticS3FirehoseFactory.class); } private void initializeEmitter() @@ -437,7 +456,7 @@ public class IndexerCoordinatorNode extends RegisteringNode emitter = new ServiceEmitter( PropUtils.getProperty(props, "druid.service"), PropUtils.getProperty(props, "druid.host"), - Emitters.create(props, httpClient, jsonMapper, lifecycle) + Emitters.create(props, httpClient, getJsonMapper(), lifecycle) ); } EmittingLogger.registerEmitter(emitter); @@ -476,6 +495,21 @@ public class IndexerCoordinatorNode extends RegisteringNode } } + private void initializeNewSegmentServerView() + { + if (newSegmentServerView == null) { + final MutableServerView view = new OnlyNewSegmentWatcherServerView(); + final ClientInventoryManager clientInventoryManager = new ClientInventoryManager( + getConfigFactory().build(ClientConfig.class), + getPhoneBook(), + view + ); + lifecycle.addManagedInstance(clientInventoryManager); + + this.newSegmentServerView = view; + } + } + public void initializeS3Service() throws S3ServiceException { this.s3Service = new RestS3Service( @@ -489,13 +523,17 @@ public class IndexerCoordinatorNode extends RegisteringNode public void initializeDataSegmentPusher() { if (segmentPusher == null) { - segmentPusher = ServerInit.getSegmentPusher(props, configFactory, jsonMapper); + segmentPusher = ServerInit.getSegmentPusher(props, configFactory, getJsonMapper()); } } public void initializeTaskToolbox() { if (taskToolboxFactory == null) { + final SegmentAnnouncer segmentAnnouncer = new ZkSegmentAnnouncer( + configFactory.build(MetadataUpdaterConfig.class), + getPhoneBook() + ); final DataSegmentKiller dataSegmentKiller = new S3DataSegmentKiller(s3Service); taskToolboxFactory = new TaskToolboxFactory( taskConfig, @@ -507,7 +545,10 @@ public class IndexerCoordinatorNode extends RegisteringNode s3Service, segmentPusher, dataSegmentKiller, - jsonMapper + segmentAnnouncer, + newSegmentServerView, + getConglomerate(), + getJsonMapper() ); } } @@ -516,7 +557,7 @@ public class IndexerCoordinatorNode extends RegisteringNode { if (mergerDBCoordinator == null) { mergerDBCoordinator = new MergerDBCoordinator( - jsonMapper, + getJsonMapper(), dbConnectorConfig, dbi ); @@ -563,7 +604,7 @@ public class IndexerCoordinatorNode extends RegisteringNode taskStorage = new HeapMemoryTaskStorage(); } else if (config.getStorageImpl().equals("db")) { final IndexerDbConnectorConfig dbConnectorConfig = configFactory.build(IndexerDbConnectorConfig.class); - taskStorage = new DbTaskStorage(jsonMapper, dbConnectorConfig, new DbConnector(dbConnectorConfig).getDBI()); + taskStorage = new DbTaskStorage(getJsonMapper(), dbConnectorConfig, new DbConnector(dbConnectorConfig).getDBI()); } else { throw new ISE("Invalid storage implementation: %s", config.getStorageImpl()); } @@ -590,7 +631,7 @@ public class IndexerCoordinatorNode extends RegisteringNode ); RemoteTaskRunner remoteTaskRunner = new RemoteTaskRunner( - jsonMapper, + getJsonMapper(), configFactory.build(RemoteTaskRunnerConfig.class), curatorFramework, new PathChildrenCache(curatorFramework, indexerZkConfig.getAnnouncementPath(), true), @@ -641,7 +682,7 @@ public class IndexerCoordinatorNode extends RegisteringNode AutoScalingStrategy strategy; if (config.getStrategyImpl().equalsIgnoreCase("ec2")) { strategy = new EC2AutoScalingStrategy( - jsonMapper, + getJsonMapper(), new AmazonEC2Client( new BasicAWSCredentials( PropUtils.getProperty(props, "com.metamx.aws.accessKey"), @@ -675,6 +716,7 @@ public class IndexerCoordinatorNode extends RegisteringNode public static class Builder { private ObjectMapper jsonMapper = null; + private ObjectMapper smileMapper = null; private Lifecycle lifecycle = null; private Properties props = null; private ConfigurationObjectFactory configFactory = null; @@ -705,8 +747,13 @@ public class IndexerCoordinatorNode extends RegisteringNode public IndexerCoordinatorNode build() { - if (jsonMapper == null) { + if (jsonMapper == null && smileMapper == null) { jsonMapper = new DefaultObjectMapper(); + smileMapper = new DefaultObjectMapper(new SmileFactory()); + smileMapper.getJsonFactory().setCodec(smileMapper); + } + else if (jsonMapper == null || smileMapper == null) { + throw new ISE("Only jsonMapper[%s] or smileMapper[%s] was set, must set neither or both.", jsonMapper, smileMapper); } if (lifecycle == null) { @@ -721,7 +768,7 @@ public class IndexerCoordinatorNode extends RegisteringNode configFactory = Config.createFactory(props); } - return new IndexerCoordinatorNode(jsonMapper, lifecycle, props, configFactory); + return new IndexerCoordinatorNode(props, lifecycle, jsonMapper, smileMapper, configFactory); } } } diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorResource.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorResource.java index 21adae8b09b..b6fce645624 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorResource.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorResource.java @@ -44,6 +44,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.io.IOException; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -184,12 +185,17 @@ public class IndexerCoordinatorResource @Produces("application/json") public Response doAction(final TaskActionHolder holder) { - final T ret = taskMasterLifecycle.getTaskToolbox(holder.getTask()) - .getTaskActionClient() - .submit(holder.getAction()); + final Map retMap; - final Map retMap = Maps.newHashMap(); - retMap.put("result", ret); + try { + final T ret = taskMasterLifecycle.getTaskToolbox(holder.getTask()) + .getTaskActionClient() + .submit(holder.getAction()); + retMap = Maps.newHashMap(); + retMap.put("result", ret); + } catch(IOException e) { + return Response.serverError().build(); + } return Response.ok().entity(retMap).build(); } diff --git a/merger/src/main/java/com/metamx/druid/merger/worker/TaskMonitor.java b/merger/src/main/java/com/metamx/druid/merger/worker/WorkerTaskMonitor.java similarity index 74% rename from merger/src/main/java/com/metamx/druid/merger/worker/TaskMonitor.java rename to merger/src/main/java/com/metamx/druid/merger/worker/WorkerTaskMonitor.java index 867b8dd9cde..400abec76fe 100644 --- a/merger/src/main/java/com/metamx/druid/merger/worker/TaskMonitor.java +++ b/merger/src/main/java/com/metamx/druid/merger/worker/WorkerTaskMonitor.java @@ -21,35 +21,47 @@ package com.metamx.druid.merger.worker; import com.metamx.common.lifecycle.LifecycleStart; import com.metamx.common.lifecycle.LifecycleStop; +import com.metamx.druid.Query; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.TaskToolbox; import com.metamx.druid.merger.common.TaskToolboxFactory; import com.metamx.druid.merger.common.task.Task; +import com.metamx.druid.query.NoopQueryRunner; +import com.metamx.druid.query.QueryRunner; +import com.metamx.druid.query.segment.QuerySegmentWalker; +import com.metamx.druid.query.segment.SegmentDescriptor; import com.metamx.emitter.EmittingLogger; import com.netflix.curator.framework.CuratorFramework; import com.netflix.curator.framework.recipes.cache.PathChildrenCache; import com.netflix.curator.framework.recipes.cache.PathChildrenCacheEvent; import com.netflix.curator.framework.recipes.cache.PathChildrenCacheListener; import org.apache.commons.io.FileUtils; +import org.joda.time.Interval; import java.io.File; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; /** * The monitor watches ZK at a specified path for new tasks to appear. Upon starting the monitor, a listener will be * created that waits for new tasks. Tasks are executed as soon as they are seen. + * + * The monitor implements {@link QuerySegmentWalker} so tasks can offer up queryable data. This is useful for + * realtime index tasks. */ -public class TaskMonitor +public class WorkerTaskMonitor implements QuerySegmentWalker { - private static final EmittingLogger log = new EmittingLogger(TaskMonitor.class); + private static final EmittingLogger log = new EmittingLogger(WorkerTaskMonitor.class); private final PathChildrenCache pathChildrenCache; private final CuratorFramework cf; private final WorkerCuratorCoordinator workerCuratorCoordinator; private final TaskToolboxFactory toolboxFactory; private final ExecutorService exec; + private final List running = new CopyOnWriteArrayList(); - public TaskMonitor( + public WorkerTaskMonitor( PathChildrenCache pathChildrenCache, CuratorFramework cf, WorkerCuratorCoordinator workerCuratorCoordinator, @@ -88,7 +100,7 @@ public class TaskMonitor ); final TaskToolbox toolbox = toolboxFactory.build(task); - if (workerCuratorCoordinator.statusExists(task.getId())) { + if (isTaskRunning(task)) { log.warn("Got task %s that I am already running...", task.getId()); workerCuratorCoordinator.unannounceTask(task.getId()); return; @@ -104,6 +116,7 @@ public class TaskMonitor final File taskDir = toolbox.getTaskDir(); log.info("Running task [%s]", task.getId()); + running.add(task); TaskStatus taskStatus; try { @@ -116,6 +129,8 @@ public class TaskMonitor .addData("task", task.getId()) .emit(); taskStatus = TaskStatus.failure(task.getId()); + } finally { + running.remove(task); } taskStatus = taskStatus.withDuration(System.currentTimeMillis() - startTime); @@ -151,12 +166,23 @@ public class TaskMonitor ); } catch (Exception e) { - log.makeAlert(e, "Exception starting TaskMonitor") + log.makeAlert(e, "Exception starting WorkerTaskMonitor") .addData("exception", e.toString()) .emit(); } } + private boolean isTaskRunning(final Task task) + { + for (final Task runningTask : running) { + if (runningTask.equals(task.getId())) { + return true; + } + } + + return false; + } + @LifecycleStop public void stop() { @@ -165,9 +191,43 @@ public class TaskMonitor exec.shutdown(); } catch (Exception e) { - log.makeAlert(e, "Exception stopping TaskMonitor") + log.makeAlert(e, "Exception stopping WorkerTaskMonitor") .addData("exception", e.toString()) .emit(); } } + + @Override + public QueryRunner getQueryRunnerForIntervals(Query query, Iterable intervals) + { + return getQueryRunnerImpl(query); + } + + @Override + public QueryRunner getQueryRunnerForSegments(Query query, Iterable specs) + { + return getQueryRunnerImpl(query); + } + + private QueryRunner getQueryRunnerImpl(Query query) { + QueryRunner queryRunner = null; + + for (final Task task : running) { + if (task.getDataSource().equals(query.getDataSource())) { + final QueryRunner taskQueryRunner = task.getQueryRunner(query); + + if (taskQueryRunner != null) { + if (queryRunner == null) { + queryRunner = taskQueryRunner; + } else { + log.makeAlert("Found too many query runners for datasource") + .addData("dataSource", query.getDataSource()) + .emit(); + } + } + } + } + + return queryRunner == null ? new NoopQueryRunner() : queryRunner; + } } diff --git a/merger/src/main/java/com/metamx/druid/merger/worker/http/WorkerNode.java b/merger/src/main/java/com/metamx/druid/merger/worker/http/WorkerNode.java index caef5bd2935..f42c9b96e4d 100644 --- a/merger/src/main/java/com/metamx/druid/merger/worker/http/WorkerNode.java +++ b/merger/src/main/java/com/metamx/druid/merger/worker/http/WorkerNode.java @@ -21,16 +21,23 @@ package com.metamx.druid.merger.worker.http; import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.google.common.collect.Lists; import com.google.inject.servlet.GuiceFilter; +import com.metamx.common.ISE; import com.metamx.common.concurrent.ScheduledExecutorFactory; import com.metamx.common.concurrent.ScheduledExecutors; import com.metamx.common.config.Config; import com.metamx.common.lifecycle.Lifecycle; import com.metamx.common.lifecycle.LifecycleStart; import com.metamx.common.lifecycle.LifecycleStop; -import com.metamx.common.logger.Logger; -import com.metamx.druid.RegisteringNode; +import com.metamx.druid.BaseServerNode; +import com.metamx.druid.Query; +import com.metamx.druid.client.ClientConfig; +import com.metamx.druid.client.ClientInventoryManager; +import com.metamx.druid.client.MutableServerView; +import com.metamx.druid.client.OnlyNewSegmentWatcherServerView; +import com.metamx.druid.http.QueryServlet; import com.metamx.druid.http.StatusServlet; import com.metamx.druid.initialization.CuratorConfig; import com.metamx.druid.initialization.Initialization; @@ -46,10 +53,18 @@ import com.metamx.druid.merger.common.actions.RemoteTaskActionClientFactory; import com.metamx.druid.merger.common.config.IndexerZkConfig; import com.metamx.druid.merger.common.config.TaskConfig; import com.metamx.druid.merger.common.index.StaticS3FirehoseFactory; -import com.metamx.druid.merger.worker.TaskMonitor; +import com.metamx.druid.merger.common.task.Task; +import com.metamx.druid.merger.worker.WorkerTaskMonitor; import com.metamx.druid.merger.worker.Worker; import com.metamx.druid.merger.worker.WorkerCuratorCoordinator; import com.metamx.druid.merger.worker.config.WorkerConfig; +import com.metamx.druid.query.NoopQueryRunner; +import com.metamx.druid.query.QueryRunner; +import com.metamx.druid.query.segment.QuerySegmentWalker; +import com.metamx.druid.query.segment.SegmentDescriptor; +import com.metamx.druid.realtime.MetadataUpdaterConfig; +import com.metamx.druid.realtime.SegmentAnnouncer; +import com.metamx.druid.realtime.ZkSegmentAnnouncer; import com.metamx.druid.utils.PropUtils; import com.metamx.emitter.EmittingLogger; import com.metamx.emitter.core.Emitters; @@ -69,6 +84,7 @@ import com.netflix.curator.x.discovery.ServiceProvider; import org.jets3t.service.S3ServiceException; import org.jets3t.service.impl.rest.httpclient.RestS3Service; import org.jets3t.service.security.AWSCredentials; +import org.joda.time.Interval; import org.mortbay.jetty.Server; import org.mortbay.jetty.servlet.Context; import org.mortbay.jetty.servlet.DefaultServlet; @@ -76,7 +92,6 @@ import org.mortbay.jetty.servlet.ServletHolder; import org.skife.config.ConfigurationObjectFactory; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.concurrent.ExecutorService; @@ -85,16 +100,15 @@ import java.util.concurrent.ScheduledExecutorService; /** */ -public class WorkerNode extends RegisteringNode +public class WorkerNode extends BaseServerNode { - private static final Logger log = new Logger(WorkerNode.class); + private static final EmittingLogger log = new EmittingLogger(WorkerNode.class); public static Builder builder() { return new Builder(); } - private final ObjectMapper jsonMapper; private final Lifecycle lifecycle; private final Properties props; private final ConfigurationObjectFactory configFactory; @@ -111,21 +125,22 @@ public class WorkerNode extends RegisteringNode private ServiceDiscovery serviceDiscovery = null; private ServiceProvider coordinatorServiceProvider = null; private WorkerCuratorCoordinator workerCuratorCoordinator = null; - private TaskMonitor taskMonitor = null; + private WorkerTaskMonitor workerTaskMonitor = null; + private MutableServerView newSegmentServerView = null; private Server server = null; private boolean initialized = false; public WorkerNode( - ObjectMapper jsonMapper, - Lifecycle lifecycle, Properties props, + Lifecycle lifecycle, + ObjectMapper jsonMapper, + ObjectMapper smileMapper, ConfigurationObjectFactory configFactory ) { - super(Arrays.asList(jsonMapper)); + super(log, props, lifecycle, jsonMapper, smileMapper, configFactory); - this.jsonMapper = jsonMapper; this.lifecycle = lifecycle; this.props = props; this.configFactory = configFactory; @@ -185,13 +200,20 @@ public class WorkerNode extends RegisteringNode return this; } - public WorkerNode setTaskMonitor(TaskMonitor taskMonitor) + public WorkerNode setNewSegmentServerView(MutableServerView newSegmentServerView) { - this.taskMonitor = taskMonitor; + this.newSegmentServerView = newSegmentServerView; return this; } - public void init() throws Exception + public WorkerNode setWorkerTaskMonitor(WorkerTaskMonitor workerTaskMonitor) + { + this.workerTaskMonitor = workerTaskMonitor; + return this; + } + + @Override + public void doInit() throws Exception { initializeHttpClient(); initializeEmitter(); @@ -201,12 +223,13 @@ public class WorkerNode extends RegisteringNode initializeCuratorFramework(); initializeServiceDiscovery(); initializeCoordinatorServiceProvider(); + initializeNewSegmentServerView(); initializeDataSegmentPusher(); initializeTaskToolbox(); initializeJacksonInjections(); initializeJacksonSubtypes(); initializeCuratorCoordinator(); - initializeTaskMonitor(); + initializeWorkerTaskMonitor(); initializeServer(); final ScheduledExecutorFactory scheduledExecutorFactory = ScheduledExecutors.createFactory(lifecycle); @@ -223,6 +246,12 @@ public class WorkerNode extends RegisteringNode root.addServlet(new ServletHolder(new StatusServlet()), "/status"); root.addServlet(new ServletHolder(new DefaultServlet()), "/mmx/*"); + root.addServlet( + new ServletHolder( + new QueryServlet(getJsonMapper(), getSmileMapper(), workerTaskMonitor, emitter, getRequestLogger()) + ), + "/druid/v2/*" + ); root.addFilter(GuiceFilter.class, "/mmx/indexer/worker/v1/*", 0); } @@ -280,12 +309,12 @@ public class WorkerNode extends RegisteringNode injectables.addValue("s3Client", s3Service) .addValue("segmentPusher", segmentPusher); - jsonMapper.setInjectableValues(injectables); + getJsonMapper().setInjectableValues(injectables); } private void initializeJacksonSubtypes() { - jsonMapper.registerSubtypes(StaticS3FirehoseFactory.class); + getJsonMapper().registerSubtypes(StaticS3FirehoseFactory.class); } private void initializeHttpClient() @@ -303,7 +332,7 @@ public class WorkerNode extends RegisteringNode emitter = new ServiceEmitter( PropUtils.getProperty(props, "druid.service"), PropUtils.getProperty(props, "druid.host"), - Emitters.create(props, httpClient, jsonMapper, lifecycle) + Emitters.create(props, httpClient, getJsonMapper(), lifecycle) ); } EmittingLogger.registerEmitter(emitter); @@ -344,7 +373,7 @@ public class WorkerNode extends RegisteringNode public void initializeDataSegmentPusher() { if (segmentPusher == null) { - segmentPusher = ServerInit.getSegmentPusher(props, configFactory, jsonMapper); + segmentPusher = ServerInit.getSegmentPusher(props, configFactory, getJsonMapper()); } } @@ -352,14 +381,22 @@ public class WorkerNode extends RegisteringNode { if (taskToolboxFactory == null) { final DataSegmentKiller dataSegmentKiller = new S3DataSegmentKiller(s3Service); + final SegmentAnnouncer segmentAnnouncer = new ZkSegmentAnnouncer( + configFactory.build(MetadataUpdaterConfig.class), + getPhoneBook() + ); + lifecycle.addManagedInstance(segmentAnnouncer); taskToolboxFactory = new TaskToolboxFactory( taskConfig, - new RemoteTaskActionClientFactory(httpClient, coordinatorServiceProvider, jsonMapper), + new RemoteTaskActionClientFactory(httpClient, coordinatorServiceProvider, getJsonMapper()), emitter, s3Service, segmentPusher, dataSegmentKiller, - jsonMapper + segmentAnnouncer, + newSegmentServerView, + getConglomerate(), + getJsonMapper() ); } } @@ -402,7 +439,7 @@ public class WorkerNode extends RegisteringNode { if (workerCuratorCoordinator == null) { workerCuratorCoordinator = new WorkerCuratorCoordinator( - jsonMapper, + getJsonMapper(), configFactory.build(IndexerZkConfig.class), curatorFramework, new Worker(workerConfig) @@ -411,29 +448,45 @@ public class WorkerNode extends RegisteringNode } } - public void initializeTaskMonitor() + private void initializeNewSegmentServerView() { - if (taskMonitor == null) { + if (newSegmentServerView == null) { + final MutableServerView view = new OnlyNewSegmentWatcherServerView(); + final ClientInventoryManager clientInventoryManager = new ClientInventoryManager( + getConfigFactory().build(ClientConfig.class), + getPhoneBook(), + view + ); + lifecycle.addManagedInstance(clientInventoryManager); + + this.newSegmentServerView = view; + } + } + + public void initializeWorkerTaskMonitor() + { + if (workerTaskMonitor == null) { final ExecutorService workerExec = Executors.newFixedThreadPool(workerConfig.getNumThreads()); final PathChildrenCache pathChildrenCache = new PathChildrenCache( curatorFramework, workerCuratorCoordinator.getTaskPathForWorker(), false ); - taskMonitor = new TaskMonitor( + workerTaskMonitor = new WorkerTaskMonitor( pathChildrenCache, curatorFramework, workerCuratorCoordinator, taskToolboxFactory, workerExec ); - lifecycle.addManagedInstance(taskMonitor); + lifecycle.addManagedInstance(workerTaskMonitor); } } public static class Builder { private ObjectMapper jsonMapper = null; + private ObjectMapper smileMapper = null; private Lifecycle lifecycle = null; private Properties props = null; private ConfigurationObjectFactory configFactory = null; @@ -464,8 +517,13 @@ public class WorkerNode extends RegisteringNode public WorkerNode build() { - if (jsonMapper == null) { + if (jsonMapper == null && smileMapper == null) { jsonMapper = new DefaultObjectMapper(); + smileMapper = new DefaultObjectMapper(new SmileFactory()); + smileMapper.getJsonFactory().setCodec(smileMapper); + } + else if (jsonMapper == null || smileMapper == null) { + throw new ISE("Only jsonMapper[%s] or smileMapper[%s] was set, must set neither or both.", jsonMapper, smileMapper); } if (lifecycle == null) { @@ -480,7 +538,7 @@ public class WorkerNode extends RegisteringNode configFactory = Config.createFactory(props); } - return new WorkerNode(jsonMapper, lifecycle, props, configFactory); + return new WorkerNode(props, lifecycle, jsonMapper, smileMapper, configFactory); } } } diff --git a/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java b/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java index d88ac044aed..9ccc3845555 100644 --- a/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java +++ b/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java @@ -17,9 +17,9 @@ import com.metamx.druid.merger.common.config.TaskConfig; import com.metamx.druid.merger.coordinator.config.RemoteTaskRunnerConfig; import com.metamx.druid.merger.coordinator.config.RetryPolicyConfig; import com.metamx.druid.merger.coordinator.setup.WorkerSetupData; -import com.metamx.druid.merger.worker.TaskMonitor; import com.metamx.druid.merger.worker.Worker; import com.metamx.druid.merger.worker.WorkerCuratorCoordinator; +import com.metamx.druid.merger.worker.WorkerTaskMonitor; import com.metamx.emitter.EmittingLogger; import com.metamx.emitter.service.ServiceEmitter; import com.netflix.curator.framework.CuratorFramework; @@ -59,7 +59,7 @@ public class RemoteTaskRunnerTest private CuratorFramework cf; private PathChildrenCache pathChildrenCache; private RemoteTaskRunner remoteTaskRunner; - private TaskMonitor taskMonitor; + private WorkerTaskMonitor workerTaskMonitor; private ScheduledExecutorService scheduledExec; @@ -123,7 +123,7 @@ public class RemoteTaskRunnerTest { testingCluster.stop(); remoteTaskRunner.stop(); - taskMonitor.stop(); + workerTaskMonitor.stop(); } @Test @@ -275,7 +275,7 @@ public class RemoteTaskRunnerTest ); workerCuratorCoordinator.start(); - taskMonitor = new TaskMonitor( + workerTaskMonitor = new WorkerTaskMonitor( new PathChildrenCache(cf, String.format("%s/worker1", tasksPath), true), cf, workerCuratorCoordinator, @@ -304,12 +304,12 @@ public class RemoteTaskRunnerTest { return null; } - }, null, null, null, null, null, jsonMapper + }, null, null, null, null, null, null, null, null, jsonMapper ), Executors.newSingleThreadExecutor() ); jsonMapper.registerSubtypes(new NamedType(TestTask.class, "test")); - taskMonitor.start(); + workerTaskMonitor.start(); } private void makeRemoteTaskRunner() throws Exception diff --git a/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskLifecycleTest.java b/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskLifecycleTest.java index c94369726e9..5c0fdf1b81a 100644 --- a/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskLifecycleTest.java +++ b/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskLifecycleTest.java @@ -19,7 +19,6 @@ package com.metamx.druid.merger.coordinator; -import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -153,6 +152,9 @@ public class TaskLifecycleTest } }, + null, // segment announcer + null, // new segment server view + null, // query runner factory conglomerate corporation unionized collective new DefaultObjectMapper() ); @@ -282,22 +284,20 @@ public class TaskLifecycleTest // Sort of similar to what realtime tasks do: // Acquire lock for first interval - final Optional lock1 = toolbox.getTaskActionClient().submit(new LockAcquireAction(interval1)); + final TaskLock lock1 = toolbox.getTaskActionClient().submit(new LockAcquireAction(interval1)); final List locks1 = toolbox.getTaskActionClient().submit(new LockListAction()); // (Confirm lock sanity) - Assert.assertTrue("lock1 present", lock1.isPresent()); - Assert.assertEquals("lock1 interval", interval1, lock1.get().getInterval()); - Assert.assertEquals("locks1", ImmutableList.of(lock1.get()), locks1); + Assert.assertEquals("lock1 interval", interval1, lock1.getInterval()); + Assert.assertEquals("locks1", ImmutableList.of(lock1), locks1); // Acquire lock for second interval - final Optional lock2 = toolbox.getTaskActionClient().submit(new LockAcquireAction(interval2)); + final TaskLock lock2 = toolbox.getTaskActionClient().submit(new LockAcquireAction(interval2)); final List locks2 = toolbox.getTaskActionClient().submit(new LockListAction()); // (Confirm lock sanity) - Assert.assertTrue("lock2 present", lock2.isPresent()); - Assert.assertEquals("lock2 interval", interval2, lock2.get().getInterval()); - Assert.assertEquals("locks2", ImmutableList.of(lock1.get(), lock2.get()), locks2); + Assert.assertEquals("lock2 interval", interval2, lock2.getInterval()); + Assert.assertEquals("locks2", ImmutableList.of(lock1, lock2), locks2); // Push first segment toolbox.getTaskActionClient() @@ -307,7 +307,7 @@ public class TaskLifecycleTest DataSegment.builder() .dataSource("foo") .interval(interval1) - .version(lock1.get().getVersion()) + .version(lock1.getVersion()) .build() ) ) @@ -318,7 +318,7 @@ public class TaskLifecycleTest final List locks3 = toolbox.getTaskActionClient().submit(new LockListAction()); // (Confirm lock sanity) - Assert.assertEquals("locks3", ImmutableList.of(lock2.get()), locks3); + Assert.assertEquals("locks3", ImmutableList.of(lock2), locks3); // Push second segment toolbox.getTaskActionClient() @@ -328,7 +328,7 @@ public class TaskLifecycleTest DataSegment.builder() .dataSource("foo") .interval(interval2) - .version(lock2.get().getVersion()) + .version(lock2.getVersion()) .build() ) ) @@ -392,7 +392,7 @@ public class TaskLifecycleTest } @Test - public void testBadVersion() throws Exception + public void testBadInterval() throws Exception { final Task task = new AbstractTask("id1", "id1", "ds", new Interval("2012-01-01/P1D")) { @@ -426,7 +426,7 @@ public class TaskLifecycleTest } @Test - public void testBadInterval() throws Exception + public void testBadVersion() throws Exception { final Task task = new AbstractTask("id1", "id1", "ds", new Interval("2012-01-01/P1D")) { @@ -506,15 +506,22 @@ public class TaskLifecycleTest } @Override - public void announceHistoricalSegments(Set segment) + public Set announceHistoricalSegments(Set segments) { - published.addAll(segment); + Set added = Sets.newHashSet(); + for(final DataSegment segment : segments) { + if(published.add(segment)) { + added.add(segment); + } + } + + return ImmutableSet.copyOf(added); } @Override - public void deleteSegments(Set segment) + public void deleteSegments(Set segments) { - nuked.addAll(segment); + nuked.addAll(segments); } public Set getPublished() diff --git a/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskQueueTest.java b/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskQueueTest.java index 939dc9b6b21..0a1968546c9 100644 --- a/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskQueueTest.java +++ b/merger/src/test/java/com/metamx/druid/merger/coordinator/TaskQueueTest.java @@ -165,6 +165,9 @@ public class TaskQueueTest null, null, null, + null, + null, + null, null ); @@ -222,6 +225,9 @@ public class TaskQueueTest null, null, null, + null, + null, + null, null ); From 34d6b3a7f05b70127934ee4709f08dbb89ae26b4 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Mon, 11 Mar 2013 14:08:05 -0700 Subject: [PATCH 06/18] Merger: - RealtimeIndexTask: Drop locks on startup - RealtimeIndexTask: Acquire lock before announcing a new segment - Retry failed RemoteTaskActionClient submissions using a RetryPolicy - Add comments to RealtimeIndexTask --- .../{coordinator => common}/RetryPolicy.java | 4 +- .../RetryPolicyFactory.java | 4 +- .../common/actions/LocalTaskActionClient.java | 2 + .../common/actions/LockReleaseAction.java | 6 +- .../actions/RemoteTaskActionClient.java | 79 +++++++++++++------ .../RemoteTaskActionClientFactory.java | 12 ++- .../config/RetryPolicyConfig.java | 8 +- .../merger/common/task/RealtimeIndexTask.java | 24 +++++- .../merger/coordinator/RemoteTaskRunner.java | 31 +++++--- .../coordinator/TaskRunnerWorkItem.java | 1 + .../http/IndexerCoordinatorNode.java | 15 ++-- .../druid/merger/worker/http/WorkerNode.java | 26 +++--- .../coordinator/RemoteTaskRunnerTest.java | 3 +- .../merger/coordinator/RetryPolicyTest.java | 3 +- 14 files changed, 143 insertions(+), 75 deletions(-) rename merger/src/main/java/com/metamx/druid/merger/{coordinator => common}/RetryPolicy.java (94%) rename merger/src/main/java/com/metamx/druid/merger/{coordinator => common}/RetryPolicyFactory.java (90%) rename merger/src/main/java/com/metamx/druid/merger/{coordinator => common}/config/RetryPolicyConfig.java (86%) diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/RetryPolicy.java b/merger/src/main/java/com/metamx/druid/merger/common/RetryPolicy.java similarity index 94% rename from merger/src/main/java/com/metamx/druid/merger/coordinator/RetryPolicy.java rename to merger/src/main/java/com/metamx/druid/merger/common/RetryPolicy.java index 632a1fcc985..19d66ee4522 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/RetryPolicy.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/RetryPolicy.java @@ -17,9 +17,9 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package com.metamx.druid.merger.coordinator; +package com.metamx.druid.merger.common; -import com.metamx.druid.merger.coordinator.config.RetryPolicyConfig; +import com.metamx.druid.merger.common.config.RetryPolicyConfig; import com.metamx.emitter.EmittingLogger; import org.joda.time.Duration; diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/RetryPolicyFactory.java b/merger/src/main/java/com/metamx/druid/merger/common/RetryPolicyFactory.java similarity index 90% rename from merger/src/main/java/com/metamx/druid/merger/coordinator/RetryPolicyFactory.java rename to merger/src/main/java/com/metamx/druid/merger/common/RetryPolicyFactory.java index c9bdcb411ea..ab6a30d5a86 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/RetryPolicyFactory.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/RetryPolicyFactory.java @@ -17,9 +17,9 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package com.metamx.druid.merger.coordinator; +package com.metamx.druid.merger.common; -import com.metamx.druid.merger.coordinator.config.RetryPolicyConfig; +import com.metamx.druid.merger.common.config.RetryPolicyConfig; /** */ diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java index e3ccc610741..4dd0cc8fe2d 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/LocalTaskActionClient.java @@ -24,6 +24,8 @@ public class LocalTaskActionClient implements TaskActionClient @Override public RetType submit(TaskAction taskAction) throws IOException { + log.info("Performing action for task[%s]: %s", task.getId(), taskAction); + final RetType ret = taskAction.perform(task, toolbox); // Add audit log diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java index 3af6befb712..42a6bbb40c9 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/LockReleaseAction.java @@ -1,15 +1,11 @@ package com.metamx.druid.merger.common.actions; -import com.google.common.base.Throwables; -import com.metamx.druid.merger.common.TaskLock; -import com.metamx.druid.merger.common.task.Task; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; +import com.metamx.druid.merger.common.task.Task; import org.joda.time.Interval; -import java.util.List; - public class LockReleaseAction implements TaskAction { private final Interval interval; diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java index 6dd7246f9e1..8059c4d4ea3 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.base.Charsets; import com.google.common.base.Throwables; import com.metamx.common.logger.Logger; +import com.metamx.druid.merger.common.RetryPolicy; +import com.metamx.druid.merger.common.RetryPolicyFactory; import com.metamx.druid.merger.common.task.Task; import com.metamx.http.client.HttpClient; import com.metamx.http.client.response.ToStringResponseHandler; @@ -21,50 +23,77 @@ public class RemoteTaskActionClient implements TaskActionClient private final Task task; private final HttpClient httpClient; private final ServiceProvider serviceProvider; + private final RetryPolicyFactory retryPolicyFactory; private final ObjectMapper jsonMapper; private static final Logger log = new Logger(RemoteTaskActionClient.class); - public RemoteTaskActionClient(Task task, HttpClient httpClient, ServiceProvider serviceProvider, ObjectMapper jsonMapper) + public RemoteTaskActionClient( + Task task, + HttpClient httpClient, + ServiceProvider serviceProvider, + RetryPolicyFactory retryPolicyFactory, + ObjectMapper jsonMapper + ) { this.task = task; this.httpClient = httpClient; this.serviceProvider = serviceProvider; + this.retryPolicyFactory = retryPolicyFactory; this.jsonMapper = jsonMapper; } @Override public RetType submit(TaskAction taskAction) throws IOException { + log.info("Performing action for task[%s]: %s", task.getId(), taskAction); + byte[] dataToSend = jsonMapper.writeValueAsBytes(new TaskActionHolder(task, taskAction)); - final URI serviceUri; - try { - serviceUri = getServiceUri(); - } - catch (Exception e) { - throw new IOException("Failed to locate service uri", e); - } + final RetryPolicy retryPolicy = retryPolicyFactory.makeRetryPolicy(); - final String response; + while (true) { + try { + final URI serviceUri; + try { + serviceUri = getServiceUri(); + } + catch (Exception e) { + throw new IOException("Failed to locate service uri", e); + } - try { - response = httpClient.post(serviceUri.toURL()) - .setContent("application/json", dataToSend) - .go(new ToStringResponseHandler(Charsets.UTF_8)) - .get(); + final String response; + + try { + response = httpClient.post(serviceUri.toURL()) + .setContent("application/json", dataToSend) + .go(new ToStringResponseHandler(Charsets.UTF_8)) + .get(); + } + catch (Exception e) { + Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); + throw Throwables.propagate(e); + } + + final Map responseDict = jsonMapper.readValue( + response, + new TypeReference>() {} + ); + + return jsonMapper.convertValue(responseDict.get("result"), taskAction.getReturnTypeReference()); + } catch(IOException e) { + if (retryPolicy.hasExceededRetryThreshold()) { + throw e; + } else { + try { + Thread.sleep(retryPolicy.getAndIncrementRetryDelay().getMillis()); + } + catch (InterruptedException e2) { + throw Throwables.propagate(e2); + } + } + } } - catch (Exception e) { - Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); - throw Throwables.propagate(e); - } - - final Map responseDict = jsonMapper.readValue( - response, - new TypeReference>() {} - ); - - return jsonMapper.convertValue(responseDict.get("result"), taskAction.getReturnTypeReference()); } private URI getServiceUri() throws Exception diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClientFactory.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClientFactory.java index 659042bb592..f6d1e9b04f1 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClientFactory.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClientFactory.java @@ -21,6 +21,7 @@ package com.metamx.druid.merger.common.actions; import com.fasterxml.jackson.databind.ObjectMapper; import com.metamx.druid.merger.common.task.Task; +import com.metamx.druid.merger.common.RetryPolicyFactory; import com.metamx.http.client.HttpClient; import com.netflix.curator.x.discovery.ServiceProvider; @@ -30,18 +31,25 @@ public class RemoteTaskActionClientFactory implements TaskActionClientFactory { private final HttpClient httpClient; private final ServiceProvider serviceProvider; + private final RetryPolicyFactory retryPolicyFactory; private final ObjectMapper jsonMapper; - public RemoteTaskActionClientFactory(HttpClient httpClient, ServiceProvider serviceProvider, ObjectMapper jsonMapper) + public RemoteTaskActionClientFactory( + HttpClient httpClient, + ServiceProvider serviceProvider, + RetryPolicyFactory retryPolicyFactory, + ObjectMapper jsonMapper + ) { this.httpClient = httpClient; this.serviceProvider = serviceProvider; + this.retryPolicyFactory = retryPolicyFactory; this.jsonMapper = jsonMapper; } @Override public TaskActionClient create(Task task) { - return new RemoteTaskActionClient(task, httpClient, serviceProvider, jsonMapper); + return new RemoteTaskActionClient(task, httpClient, serviceProvider, retryPolicyFactory, jsonMapper); } } diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/config/RetryPolicyConfig.java b/merger/src/main/java/com/metamx/druid/merger/common/config/RetryPolicyConfig.java similarity index 86% rename from merger/src/main/java/com/metamx/druid/merger/coordinator/config/RetryPolicyConfig.java rename to merger/src/main/java/com/metamx/druid/merger/common/config/RetryPolicyConfig.java index 47c8eaf4d1a..1086c5ec2cd 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/config/RetryPolicyConfig.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/config/RetryPolicyConfig.java @@ -17,7 +17,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package com.metamx.druid.merger.coordinator.config; +package com.metamx.druid.merger.common.config; import org.joda.time.Duration; import org.skife.config.Config; @@ -27,15 +27,15 @@ import org.skife.config.Default; */ public abstract class RetryPolicyConfig { - @Config("druid.indexer.retry.minWaitMillis") + @Config("${base_path}.retry.minWaitMillis") @Default("PT1M") // 1 minute public abstract Duration getRetryMinDuration(); - @Config("druid.indexer.retry.maxWaitMillis") + @Config("${base_path}.retry.maxWaitMillis") @Default("PT10M") // 10 minutes public abstract Duration getRetryMaxDuration(); - @Config("druid.indexer.retry.maxRetryCount") + @Config("${base_path}.retry.maxRetryCount") @Default("10") public abstract long getMaxRetryCount(); } diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java index 0a7696d0802..22bc8de52aa 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java @@ -14,6 +14,7 @@ import com.metamx.druid.merger.common.TaskLock; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.TaskToolbox; import com.metamx.druid.merger.common.actions.LockAcquireAction; +import com.metamx.druid.merger.common.actions.LockListAction; import com.metamx.druid.merger.common.actions.LockReleaseAction; import com.metamx.druid.merger.common.actions.SegmentInsertAction; import com.metamx.druid.query.QueryRunner; @@ -109,14 +110,20 @@ public class RealtimeIndexTask extends AbstractTask throw new IllegalStateException("WTF?!? run with non-null plumber??!"); } + // Shed any locks we might have (e.g. if we were uncleanly killed and restarted) since we'll reacquire + // them if we actually need them + for (final TaskLock taskLock : toolbox.getTaskActionClient().submit(new LockListAction())) { + toolbox.getTaskActionClient().submit(new LockReleaseAction(taskLock.getInterval())); + } + boolean normalExit = true; final FireDepartmentMetrics metrics = new FireDepartmentMetrics(); final Period intermediatePersistPeriod = fireDepartmentConfig.getIntermediatePersistPeriod(); final Firehose firehose = firehoseFactory.connect(); - // TODO Take PlumberSchool in constructor (although that will need jackson injectables for stuff like - // TODO the ServerView, which seems kind of odd?) + // TODO -- Take PlumberSchool in constructor (although that will need jackson injectables for stuff like + // TODO -- the ServerView, which seems kind of odd?) final RealtimePlumberSchool realtimePlumberSchool = new RealtimePlumberSchool( windowPeriod, new File(toolbox.getTaskDir(), "persist"), @@ -125,12 +132,19 @@ public class RealtimeIndexTask extends AbstractTask final SegmentPublisher segmentPublisher = new TaskActionSegmentPublisher(this, toolbox); + // TODO -- We're adding stuff to talk to the coordinator in various places in the plumber, and may + // TODO -- want to be more robust to coordinator downtime (currently we'll block/throw in whatever + // TODO -- thread triggered the coordinator behavior, which will typically be either the main + // TODO -- data processing loop or the persist thread) + // Wrap default SegmentAnnouncer such that we unlock intervals as we unannounce segments final SegmentAnnouncer lockingSegmentAnnouncer = new SegmentAnnouncer() { @Override public void announceSegment(final DataSegment segment) throws IOException { + // NOTE: Side effect: Calling announceSegment causes a lock to be acquired + toolbox.getTaskActionClient().submit(new LockAcquireAction(segment.getInterval())); toolbox.getSegmentAnnouncer().announceSegment(segment); } @@ -146,8 +160,10 @@ public class RealtimeIndexTask extends AbstractTask }; // TODO -- This can block if there is lock contention, which will block plumber.getSink (and thus the firehose) - // TODO -- Shouldn't usually be bad, since we don't expect people to submit tasks that intersect with the - // TODO -- realtime window, but if they do it can be problematic + // TODO -- Shouldn't usually happen, since we don't expect people to submit tasks that intersect with the + // TODO -- realtime window, but if they do it can be problematic. + // TODO -- If we decide to care, we can use more threads in the plumber such that waiting for the coordinator + // TODO -- doesn't block data processing. final RealtimePlumberSchool.VersioningPolicy versioningPolicy = new RealtimePlumberSchool.VersioningPolicy() { @Override diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/RemoteTaskRunner.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/RemoteTaskRunner.java index b1ed92087bc..57cb3b26059 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/RemoteTaskRunner.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/RemoteTaskRunner.java @@ -31,6 +31,7 @@ import com.metamx.common.ISE; import com.metamx.common.guava.FunctionalIterable; import com.metamx.common.lifecycle.LifecycleStart; import com.metamx.common.lifecycle.LifecycleStop; +import com.metamx.druid.merger.common.RetryPolicyFactory; import com.metamx.druid.merger.common.TaskCallback; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.task.Task; @@ -274,20 +275,24 @@ public class RemoteTaskRunner implements TaskRunner private void retryTask(final TaskRunnerWorkItem taskRunnerWorkItem, final String workerId) { final String taskId = taskRunnerWorkItem.getTask().getId(); - log.info("Retry scheduled in %s for %s", taskRunnerWorkItem.getRetryPolicy().getRetryDelay(), taskId); - scheduledExec.schedule( - new Runnable() - { - @Override - public void run() + if (!taskRunnerWorkItem.getRetryPolicy().hasExceededRetryThreshold()) { + log.info("Retry scheduled in %s for %s", taskRunnerWorkItem.getRetryPolicy().getRetryDelay(), taskId); + scheduledExec.schedule( + new Runnable() { - cleanup(workerId, taskId); - addPendingTask(taskRunnerWorkItem); - } - }, - taskRunnerWorkItem.getRetryPolicy().getAndIncrementRetryDelay().getMillis(), - TimeUnit.MILLISECONDS - ); + @Override + public void run() + { + cleanup(workerId, taskId); + addPendingTask(taskRunnerWorkItem); + } + }, + taskRunnerWorkItem.getRetryPolicy().getAndIncrementRetryDelay().getMillis(), + TimeUnit.MILLISECONDS + ); + } else { + log.makeAlert("Task exceeded retry threshold").addData("task", taskId).emit(); + } } /** diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskRunnerWorkItem.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskRunnerWorkItem.java index 4526421f0dd..d850c93d119 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskRunnerWorkItem.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/TaskRunnerWorkItem.java @@ -20,6 +20,7 @@ package com.metamx.druid.merger.coordinator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.metamx.druid.merger.common.RetryPolicy; import com.metamx.druid.merger.common.TaskCallback; import com.metamx.druid.merger.common.task.Task; import org.joda.time.DateTime; diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java index 17808363311..783555d3e37 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.google.common.base.Charsets; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Guice; @@ -40,7 +41,6 @@ import com.metamx.common.lifecycle.LifecycleStart; import com.metamx.common.lifecycle.LifecycleStop; import com.metamx.common.logger.Logger; import com.metamx.druid.BaseServerNode; -import com.metamx.druid.RegisteringNode; import com.metamx.druid.client.ClientConfig; import com.metamx.druid.client.ClientInventoryManager; import com.metamx.druid.client.MutableServerView; @@ -73,7 +73,7 @@ import com.metamx.druid.merger.coordinator.HeapMemoryTaskStorage; import com.metamx.druid.merger.coordinator.LocalTaskRunner; import com.metamx.druid.merger.coordinator.MergerDBCoordinator; import com.metamx.druid.merger.coordinator.RemoteTaskRunner; -import com.metamx.druid.merger.coordinator.RetryPolicyFactory; +import com.metamx.druid.merger.common.RetryPolicyFactory; import com.metamx.druid.merger.coordinator.TaskLockbox; import com.metamx.druid.merger.coordinator.TaskMasterLifecycle; import com.metamx.druid.merger.coordinator.TaskQueue; @@ -85,7 +85,7 @@ import com.metamx.druid.merger.coordinator.config.EC2AutoScalingStrategyConfig; import com.metamx.druid.merger.coordinator.config.IndexerCoordinatorConfig; import com.metamx.druid.merger.coordinator.config.IndexerDbConnectorConfig; import com.metamx.druid.merger.coordinator.config.RemoteTaskRunnerConfig; -import com.metamx.druid.merger.coordinator.config.RetryPolicyConfig; +import com.metamx.druid.merger.common.config.RetryPolicyConfig; import com.metamx.druid.merger.coordinator.scaling.AutoScalingStrategy; import com.metamx.druid.merger.coordinator.scaling.EC2AutoScalingStrategy; import com.metamx.druid.merger.coordinator.scaling.NoopAutoScalingStrategy; @@ -95,7 +95,6 @@ import com.metamx.druid.merger.coordinator.scaling.ResourceManagementSchedulerFa import com.metamx.druid.merger.coordinator.scaling.SimpleResourceManagementStrategy; import com.metamx.druid.merger.coordinator.scaling.SimpleResourceManagmentConfig; import com.metamx.druid.merger.coordinator.setup.WorkerSetupData; -import com.metamx.druid.merger.worker.http.WorkerNode; import com.metamx.druid.realtime.MetadataUpdaterConfig; import com.metamx.druid.realtime.SegmentAnnouncer; import com.metamx.druid.realtime.ZkSegmentAnnouncer; @@ -127,7 +126,6 @@ import org.skife.config.ConfigurationObjectFactory; import org.skife.jdbi.v2.DBI; import java.net.URL; -import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.concurrent.ExecutorService; @@ -636,7 +634,12 @@ public class IndexerCoordinatorNode extends BaseServerNode lifecycle.addManagedInstance(segmentAnnouncer); taskToolboxFactory = new TaskToolboxFactory( taskConfig, - new RemoteTaskActionClientFactory(httpClient, coordinatorServiceProvider, getJsonMapper()), + new RemoteTaskActionClientFactory( + httpClient, + coordinatorServiceProvider, + new RetryPolicyFactory( + configFactory.buildWithReplacements( + RetryPolicyConfig.class, + ImmutableMap.of("base_path", "druid.worker.taskActionClient") + ) + ), + getJsonMapper() + ), emitter, s3Service, segmentPusher, diff --git a/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java b/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java index 9ccc3845555..a3980820268 100644 --- a/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java +++ b/merger/src/test/java/com/metamx/druid/merger/coordinator/RemoteTaskRunnerTest.java @@ -9,13 +9,14 @@ import com.metamx.druid.aggregation.AggregatorFactory; import com.metamx.druid.client.DataSegment; import com.metamx.druid.jackson.DefaultObjectMapper; import com.metamx.druid.merger.TestTask; +import com.metamx.druid.merger.common.RetryPolicyFactory; import com.metamx.druid.merger.common.TaskCallback; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.TaskToolboxFactory; import com.metamx.druid.merger.common.config.IndexerZkConfig; import com.metamx.druid.merger.common.config.TaskConfig; import com.metamx.druid.merger.coordinator.config.RemoteTaskRunnerConfig; -import com.metamx.druid.merger.coordinator.config.RetryPolicyConfig; +import com.metamx.druid.merger.common.config.RetryPolicyConfig; import com.metamx.druid.merger.coordinator.setup.WorkerSetupData; import com.metamx.druid.merger.worker.Worker; import com.metamx.druid.merger.worker.WorkerCuratorCoordinator; diff --git a/merger/src/test/java/com/metamx/druid/merger/coordinator/RetryPolicyTest.java b/merger/src/test/java/com/metamx/druid/merger/coordinator/RetryPolicyTest.java index 5445c05e7dd..41b356d5f8d 100644 --- a/merger/src/test/java/com/metamx/druid/merger/coordinator/RetryPolicyTest.java +++ b/merger/src/test/java/com/metamx/druid/merger/coordinator/RetryPolicyTest.java @@ -1,6 +1,7 @@ package com.metamx.druid.merger.coordinator; -import com.metamx.druid.merger.coordinator.config.RetryPolicyConfig; +import com.metamx.druid.merger.common.RetryPolicy; +import com.metamx.druid.merger.common.config.RetryPolicyConfig; import junit.framework.Assert; import org.joda.time.Duration; import org.junit.Test; From 765e70bc8eb2a44ce5347784e15a75c98687c7e2 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Mon, 11 Mar 2013 15:23:45 -0700 Subject: [PATCH 07/18] RemoteTaskActionClient: Better logging --- .../druid/merger/common/actions/RemoteTaskActionClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java index 8059c4d4ea3..ca9fe9c5c73 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java @@ -82,6 +82,8 @@ public class RemoteTaskActionClient implements TaskActionClient return jsonMapper.convertValue(responseDict.get("result"), taskAction.getReturnTypeReference()); } catch(IOException e) { + log.warn(e, "Exception submitting action for task: %s", task.getId()); + if (retryPolicy.hasExceededRetryThreshold()) { throw e; } else { From 1e0f2c2d9214b5539d4089ce7fce644c5ed7165a Mon Sep 17 00:00:00 2001 From: Eric Tschetter Date: Wed, 13 Mar 2013 19:26:22 -0500 Subject: [PATCH 08/18] 1) Make log a bit more descriptive --- .../src/main/java/com/metamx/druid/utils/CompressionUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/com/metamx/druid/utils/CompressionUtils.java b/common/src/main/java/com/metamx/druid/utils/CompressionUtils.java index c34b8e7e960..bb44f05e495 100644 --- a/common/src/main/java/com/metamx/druid/utils/CompressionUtils.java +++ b/common/src/main/java/com/metamx/druid/utils/CompressionUtils.java @@ -62,7 +62,7 @@ public class CompressionUtils zipOut = new ZipOutputStream(new FileOutputStream(outputZipFile)); File[] files = directory.listFiles(); for (File file : files) { - log.info("Adding file[%s] with size[%,d]. Total size[%,d]", file, file.length(), totalSize); + log.info("Adding file[%s] with size[%,d]. Total size so far[%,d]", file, file.length(), totalSize); if (file.length() >= Integer.MAX_VALUE) { zipOut.close(); outputZipFile.delete(); From e5d5050c3f93d66ee550044ad1f2b8363d5cf341 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Wed, 13 Mar 2013 23:06:55 -0700 Subject: [PATCH 09/18] RemoteTaskActionClient: Log retry timer on errors --- .../druid/merger/common/actions/RemoteTaskActionClient.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java index 644d15c09f3..d2c761f2770 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/actions/RemoteTaskActionClient.java @@ -13,6 +13,7 @@ import com.metamx.http.client.response.ToStringResponseHandler; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.curator.x.discovery.ServiceInstance; import com.netflix.curator.x.discovery.ServiceProvider; +import org.joda.time.Duration; import java.io.IOException; import java.net.URI; @@ -89,7 +90,9 @@ public class RemoteTaskActionClient implements TaskActionClient throw e; } else { try { - Thread.sleep(retryPolicy.getAndIncrementRetryDelay().getMillis()); + final long sleepTime = retryPolicy.getAndIncrementRetryDelay().getMillis(); + log.info("Will try again in %s.", new Duration(sleepTime).toString()); + Thread.sleep(sleepTime); } catch (InterruptedException e2) { throw Throwables.propagate(e2); From 9fe6a37f86b0c5704180d70a52af5bf5eb07fc19 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Thu, 14 Mar 2013 12:35:38 -0700 Subject: [PATCH 10/18] Realtime: Remove MetadataUpdater --- .../examples/RealtimeStandaloneMain.java | 23 +++++--- .../examples/RealtimeStandaloneMain.java | 41 +++++++------ .../merger/common/task/RealtimeIndexTask.java | 4 +- .../http/IndexerCoordinatorNode.java | 8 +-- .../druid/merger/worker/http/WorkerNode.java | 4 +- .../druid/realtime/DbSegmentPublisher.java | 4 +- .../realtime/DbSegmentPublisherConfig.java | 9 +++ .../druid/realtime/MetadataUpdater.java | 35 ----------- .../druid/realtime/MetadataUpdaterConfig.java | 47 --------------- .../metamx/druid/realtime/RealtimeNode.java | 58 +++++++++++++------ .../druid/realtime/RealtimePlumberSchool.java | 28 +++++---- .../druid/realtime/ZkSegmentAnnouncer.java | 6 +- .../realtime/ZkSegmentAnnouncerConfig.java | 23 ++++++++ 13 files changed, 139 insertions(+), 151 deletions(-) create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisherConfig.java delete mode 100644 realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java delete mode 100644 realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdaterConfig.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncerConfig.java diff --git a/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java b/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java index 1a7e57ffbba..daba1b62310 100644 --- a/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java +++ b/examples/rand/src/main/java/druid/examples/RealtimeStandaloneMain.java @@ -8,8 +8,9 @@ import com.metamx.druid.client.ZKPhoneBook; import com.metamx.druid.jackson.DefaultObjectMapper; import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.log.LogLevelAdjuster; -import com.metamx.druid.realtime.MetadataUpdater; import com.metamx.druid.realtime.RealtimeNode; +import com.metamx.druid.realtime.SegmentAnnouncer; +import com.metamx.druid.realtime.SegmentPublisher; import com.metamx.phonebook.PhoneBook; import java.io.File; @@ -41,10 +42,11 @@ public class RealtimeStandaloneMain }; rn.setPhoneBook(dummyPhoneBook); - MetadataUpdater dummyMetadataUpdater = - new MetadataUpdater(null, null) { + SegmentAnnouncer dummySegmentAnnouncer = + new SegmentAnnouncer() + { @Override - public void publishSegment(DataSegment segment) throws IOException + public void announceSegment(DataSegment segment) throws IOException { // do nothing } @@ -54,17 +56,20 @@ public class RealtimeStandaloneMain { // do nothing } - + }; + SegmentPublisher dummySegmentPublisher = + new SegmentPublisher() + { @Override - public void announceSegment(DataSegment segment) throws IOException + public void publishSegment(DataSegment segment) throws IOException { // do nothing } }; - // dummyMetadataUpdater will not send updates to db because standalone demo has no db - rn.setMetadataUpdater(dummyMetadataUpdater); - + // dummySegmentPublisher will not send updates to db because standalone demo has no db + rn.setSegmentAnnouncer(dummySegmentAnnouncer); + rn.setSegmentPublisher(dummySegmentPublisher); rn.setDataSegmentPusher( new DataSegmentPusher() { diff --git a/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java b/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java index 53b41416366..c632b8d022d 100644 --- a/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java +++ b/examples/twitter/src/main/java/druid/examples/RealtimeStandaloneMain.java @@ -8,8 +8,9 @@ import com.metamx.druid.client.ZKPhoneBook; import com.metamx.druid.jackson.DefaultObjectMapper; import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.log.LogLevelAdjuster; -import com.metamx.druid.realtime.MetadataUpdater; import com.metamx.druid.realtime.RealtimeNode; +import com.metamx.druid.realtime.SegmentAnnouncer; +import com.metamx.druid.realtime.SegmentPublisher; import com.metamx.phonebook.PhoneBook; import druid.examples.twitter.TwitterSpritzerFirehoseFactory; @@ -43,30 +44,34 @@ public class RealtimeStandaloneMain }; rn.setPhoneBook(dummyPhoneBook); - final MetadataUpdater dummyMetadataUpdater = - new MetadataUpdater(null, null) { + final SegmentAnnouncer dummySegmentAnnouncer = + new SegmentAnnouncer() + { + @Override + public void announceSegment(DataSegment segment) throws IOException + { + // do nothing + } + + @Override + public void unannounceSegment(DataSegment segment) throws IOException + { + // do nothing + } + }; + SegmentPublisher dummySegmentPublisher = + new SegmentPublisher() + { @Override public void publishSegment(DataSegment segment) throws IOException { // do nothing } - - @Override - public void unannounceSegment(DataSegment segment) throws IOException - { - // do nothing - } - - @Override - public void announceSegment(DataSegment segment) throws IOException - { - // do nothing - } }; - // dummyMetadataUpdater will not send updates to db because standalone demo has no db - rn.setMetadataUpdater(dummyMetadataUpdater); - + // dummySegmentPublisher will not send updates to db because standalone demo has no db + rn.setSegmentAnnouncer(dummySegmentAnnouncer); + rn.setSegmentPublisher(dummySegmentPublisher); rn.setDataSegmentPusher( new DataSegmentPusher() { diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java index 22bc8de52aa..6744f2280f0 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java @@ -22,7 +22,6 @@ import com.metamx.druid.realtime.FireDepartmentConfig; import com.metamx.druid.realtime.FireDepartmentMetrics; import com.metamx.druid.realtime.Firehose; import com.metamx.druid.realtime.FirehoseFactory; -import com.metamx.druid.realtime.MetadataUpdater; import com.metamx.druid.realtime.Plumber; import com.metamx.druid.realtime.RealtimePlumberSchool; import com.metamx.druid.realtime.Schema; @@ -186,7 +185,8 @@ public class RealtimeIndexTask extends AbstractTask realtimePlumberSchool.setDataSegmentPusher(toolbox.getSegmentPusher()); realtimePlumberSchool.setConglomerate(toolbox.getQueryRunnerFactoryConglomerate()); realtimePlumberSchool.setVersioningPolicy(versioningPolicy); - realtimePlumberSchool.setMetadataUpdater(new MetadataUpdater(lockingSegmentAnnouncer, segmentPublisher)); + realtimePlumberSchool.setSegmentAnnouncer(lockingSegmentAnnouncer); + realtimePlumberSchool.setSegmentPublisher(segmentPublisher); realtimePlumberSchool.setServerView(toolbox.getNewSegmentServerView()); realtimePlumberSchool.setServiceEmitter(toolbox.getEmitter()); diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java index 783555d3e37..b862644c8c1 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/http/IndexerCoordinatorNode.java @@ -62,10 +62,12 @@ import com.metamx.druid.jackson.DefaultObjectMapper; import com.metamx.druid.loading.DataSegmentKiller; import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.loading.S3DataSegmentKiller; +import com.metamx.druid.merger.common.RetryPolicyFactory; import com.metamx.druid.merger.common.TaskToolboxFactory; import com.metamx.druid.merger.common.actions.LocalTaskActionClientFactory; import com.metamx.druid.merger.common.actions.TaskActionToolbox; import com.metamx.druid.merger.common.config.IndexerZkConfig; +import com.metamx.druid.merger.common.config.RetryPolicyConfig; import com.metamx.druid.merger.common.config.TaskConfig; import com.metamx.druid.merger.common.index.StaticS3FirehoseFactory; import com.metamx.druid.merger.coordinator.DbTaskStorage; @@ -73,7 +75,6 @@ import com.metamx.druid.merger.coordinator.HeapMemoryTaskStorage; import com.metamx.druid.merger.coordinator.LocalTaskRunner; import com.metamx.druid.merger.coordinator.MergerDBCoordinator; import com.metamx.druid.merger.coordinator.RemoteTaskRunner; -import com.metamx.druid.merger.common.RetryPolicyFactory; import com.metamx.druid.merger.coordinator.TaskLockbox; import com.metamx.druid.merger.coordinator.TaskMasterLifecycle; import com.metamx.druid.merger.coordinator.TaskQueue; @@ -85,7 +86,6 @@ import com.metamx.druid.merger.coordinator.config.EC2AutoScalingStrategyConfig; import com.metamx.druid.merger.coordinator.config.IndexerCoordinatorConfig; import com.metamx.druid.merger.coordinator.config.IndexerDbConnectorConfig; import com.metamx.druid.merger.coordinator.config.RemoteTaskRunnerConfig; -import com.metamx.druid.merger.common.config.RetryPolicyConfig; import com.metamx.druid.merger.coordinator.scaling.AutoScalingStrategy; import com.metamx.druid.merger.coordinator.scaling.EC2AutoScalingStrategy; import com.metamx.druid.merger.coordinator.scaling.NoopAutoScalingStrategy; @@ -95,9 +95,9 @@ import com.metamx.druid.merger.coordinator.scaling.ResourceManagementSchedulerFa import com.metamx.druid.merger.coordinator.scaling.SimpleResourceManagementStrategy; import com.metamx.druid.merger.coordinator.scaling.SimpleResourceManagmentConfig; import com.metamx.druid.merger.coordinator.setup.WorkerSetupData; -import com.metamx.druid.realtime.MetadataUpdaterConfig; import com.metamx.druid.realtime.SegmentAnnouncer; import com.metamx.druid.realtime.ZkSegmentAnnouncer; +import com.metamx.druid.realtime.ZkSegmentAnnouncerConfig; import com.metamx.druid.utils.PropUtils; import com.metamx.emitter.EmittingLogger; import com.metamx.emitter.core.Emitters; @@ -529,7 +529,7 @@ public class IndexerCoordinatorNode extends BaseServerNode if (taskToolboxFactory == null) { final DataSegmentKiller dataSegmentKiller = new S3DataSegmentKiller(s3Service); final SegmentAnnouncer segmentAnnouncer = new ZkSegmentAnnouncer( - configFactory.build(MetadataUpdaterConfig.class), + configFactory.build(ZkSegmentAnnouncerConfig.class), getPhoneBook() ); lifecycle.addManagedInstance(segmentAnnouncer); diff --git a/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java b/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java index cec5172bdbd..7a7e0e8ed7f 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisher.java @@ -17,12 +17,12 @@ public class DbSegmentPublisher implements SegmentPublisher private static final Logger log = new Logger(DbSegmentPublisher.class); private final ObjectMapper jsonMapper; - private final MetadataUpdaterConfig config; + private final DbSegmentPublisherConfig config; private final DBI dbi; public DbSegmentPublisher( ObjectMapper jsonMapper, - MetadataUpdaterConfig config, + DbSegmentPublisherConfig config, DBI dbi ) { diff --git a/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisherConfig.java b/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisherConfig.java new file mode 100644 index 00000000000..5dcaccac49b --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/DbSegmentPublisherConfig.java @@ -0,0 +1,9 @@ +package com.metamx.druid.realtime; + +import org.skife.config.Config; + +public abstract class DbSegmentPublisherConfig +{ + @Config("druid.database.segmentTable") + public abstract String getSegmentTable(); +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java b/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java deleted file mode 100644 index b03fa70dc75..00000000000 --- a/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdater.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.metamx.druid.realtime; - -import com.metamx.druid.client.DataSegment; - -import java.io.IOException; - -public class MetadataUpdater implements SegmentAnnouncer, SegmentPublisher -{ - private final SegmentAnnouncer segmentAnnouncer; - private final SegmentPublisher segmentPublisher; - - public MetadataUpdater(SegmentAnnouncer segmentAnnouncer, SegmentPublisher segmentPublisher) - { - this.segmentAnnouncer = segmentAnnouncer; - this.segmentPublisher = segmentPublisher; - } - - @Override - public void announceSegment(DataSegment segment) throws IOException - { - segmentAnnouncer.announceSegment(segment); - } - - @Override - public void unannounceSegment(DataSegment segment) throws IOException - { - segmentAnnouncer.unannounceSegment(segment); - } - - @Override - public void publishSegment(DataSegment segment) throws IOException - { - segmentPublisher.publishSegment(segment); - } -} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdaterConfig.java b/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdaterConfig.java deleted file mode 100644 index ca9ae5684ad..00000000000 --- a/realtime/src/main/java/com/metamx/druid/realtime/MetadataUpdaterConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Druid - a distributed column store. - * Copyright (C) 2012 Metamarkets Group Inc. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -package com.metamx.druid.realtime; - -import org.skife.config.Config; -import org.skife.config.Default; - -/** - */ -public abstract class MetadataUpdaterConfig -{ - @Config("druid.host") - public abstract String getServerName(); - - @Config("druid.host") - public abstract String getHost(); - - @Config("druid.server.maxSize") - @Default("0") - public abstract long getMaxSize(); - - @Config("druid.database.segmentTable") - public abstract String getSegmentTable(); - - @Config("druid.zk.paths.announcementsPath") - public abstract String getAnnounceLocation(); - - @Config("druid.zk.paths.servedSegmentsPath") - public abstract String getServedSegmentsLocation(); -} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java index b523bb94a57..96052ae3d29 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeNode.java @@ -77,7 +77,8 @@ public class RealtimeNode extends BaseServerNode private final Map injectablesMap = Maps.newLinkedHashMap(); - private MetadataUpdater metadataUpdater = null; + private SegmentAnnouncer segmentAnnouncer = null; + private SegmentPublisher segmentPublisher = null; private DataSegmentPusher dataSegmentPusher = null; private List fireDepartments = null; private ServerView view = null; @@ -102,10 +103,17 @@ public class RealtimeNode extends BaseServerNode return this; } - public RealtimeNode setMetadataUpdater(MetadataUpdater metadataUpdater) + public RealtimeNode setSegmentAnnouncer(SegmentAnnouncer segmentAnnouncer) { - Preconditions.checkState(this.metadataUpdater == null, "Cannot set metadataUpdater once it has already been set."); - this.metadataUpdater = metadataUpdater; + Preconditions.checkState(this.segmentAnnouncer == null, "Cannot set segmentAnnouncer once it has already been set."); + this.segmentAnnouncer = segmentAnnouncer; + return this; + } + + public RealtimeNode setSegmentPublisher(SegmentPublisher segmentPublisher) + { + Preconditions.checkState(this.segmentPublisher == null, "Cannot set segmentPublisher once it has already been set."); + this.segmentPublisher = segmentPublisher; return this; } @@ -130,10 +138,16 @@ public class RealtimeNode extends BaseServerNode return this; } - public MetadataUpdater getMetadataUpdater() + public SegmentAnnouncer getSegmentAnnouncer() { - initializeMetadataUpdater(); - return metadataUpdater; + initializeSegmentAnnouncer(); + return segmentAnnouncer; + } + + public SegmentPublisher getSegmentPublisher() + { + initializeSegmentPublisher(); + return segmentPublisher; } public DataSegmentPusher getDataSegmentPusher() @@ -157,7 +171,8 @@ public class RealtimeNode extends BaseServerNode protected void doInit() throws Exception { initializeView(); - initializeMetadataUpdater(); + initializeSegmentAnnouncer(); + initializeSegmentPublisher(); initializeSegmentPusher(); initializeJacksonInjectables(); @@ -213,7 +228,8 @@ public class RealtimeNode extends BaseServerNode injectables.put("queryRunnerFactoryConglomerate", getConglomerate()); injectables.put("segmentPusher", dataSegmentPusher); - injectables.put("metadataUpdater", metadataUpdater); + injectables.put("segmentAnnouncer", segmentAnnouncer); + injectables.put("segmentPublisher", segmentPublisher); injectables.put("serverView", view); injectables.put("serviceEmitter", getEmitter()); @@ -253,21 +269,25 @@ public class RealtimeNode extends BaseServerNode } } - protected void initializeMetadataUpdater() + protected void initializeSegmentAnnouncer() { - if (metadataUpdater == null) { - final MetadataUpdaterConfig metadataUpdaterConfig = getConfigFactory().build(MetadataUpdaterConfig.class); - final SegmentAnnouncer segmentAnnouncer = new ZkSegmentAnnouncer(metadataUpdaterConfig, getPhoneBook()); - final SegmentPublisher segmentPublisher = new DbSegmentPublisher( + if (segmentAnnouncer == null) { + final ZkSegmentAnnouncerConfig zkSegmentAnnouncerConfig = getConfigFactory().build(ZkSegmentAnnouncerConfig.class); + segmentAnnouncer = new ZkSegmentAnnouncer(zkSegmentAnnouncerConfig, getPhoneBook()); + getLifecycle().addManagedInstance(segmentAnnouncer); + } + } + + protected void initializeSegmentPublisher() + { + if (segmentPublisher == null) { + final DbSegmentPublisherConfig dbSegmentPublisherConfig = getConfigFactory().build(DbSegmentPublisherConfig.class); + segmentPublisher = new DbSegmentPublisher( getJsonMapper(), - metadataUpdaterConfig, + dbSegmentPublisherConfig, new DbConnector(getConfigFactory().build(DbConnectorConfig.class)).getDBI() ); - - getLifecycle().addManagedInstance(segmentAnnouncer); getLifecycle().addManagedInstance(segmentPublisher); - - metadataUpdater = new MetadataUpdater(segmentAnnouncer, segmentPublisher); } } diff --git a/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java b/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java index 2adee9623e4..84d5058a3ac 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java @@ -90,7 +90,8 @@ public class RealtimePlumberSchool implements PlumberSchool private volatile RejectionPolicyFactory rejectionPolicyFactory = null; private volatile QueryRunnerFactoryConglomerate conglomerate = null; private volatile DataSegmentPusher dataSegmentPusher = null; - private volatile MetadataUpdater metadataUpdater = null; + private volatile SegmentAnnouncer segmentAnnouncer = null; + private volatile SegmentPublisher segmentPublisher = null; private volatile ServerView serverView = null; private ServiceEmitter emitter; @@ -136,10 +137,16 @@ public class RealtimePlumberSchool implements PlumberSchool this.dataSegmentPusher = dataSegmentPusher; } - @JacksonInject("metadataUpdater") - public void setMetadataUpdater(MetadataUpdater metadataUpdater) + @JacksonInject("segmentAnnouncer") + public void setSegmentAnnouncer(SegmentAnnouncer segmentAnnouncer) { - this.metadataUpdater = metadataUpdater; + this.segmentAnnouncer = segmentAnnouncer; + } + + @JacksonInject("segmentPublisher") + public void setSegmentPublisher(SegmentPublisher segmentPublisher) + { + this.segmentPublisher = segmentPublisher; } @JacksonInject("serverView") @@ -200,7 +207,7 @@ public class RealtimePlumberSchool implements PlumberSchool retVal = new Sink(sinkInterval, schema, versioningPolicy.getVersion(sinkInterval)); try { - metadataUpdater.announceSegment(retVal.getSegment()); + segmentAnnouncer.announceSegment(retVal.getSegment()); sinks.put(truncatedTime, retVal); } catch (IOException e) { @@ -297,7 +304,7 @@ public class RealtimePlumberSchool implements PlumberSchool for (final Sink sink : sinks.values()) { try { - metadataUpdater.unannounceSegment(sink.getSegment()); + segmentAnnouncer.unannounceSegment(sink.getSegment()); } catch (Exception e) { log.makeAlert("Failed to unannounce segment on shutdown") @@ -375,7 +382,7 @@ public class RealtimePlumberSchool implements PlumberSchool Sink currSink = new Sink(sinkInterval, schema, versioningPolicy.getVersion(sinkInterval), hydrants); sinks.put(sinkInterval.getStartMillis(), currSink); - metadataUpdater.announceSegment(currSink.getSegment()); + segmentAnnouncer.announceSegment(currSink.getSegment()); } catch (IOException e) { log.makeAlert(e, "Problem loading sink[%s] from disk.", schema.getDataSource()) @@ -415,7 +422,7 @@ public class RealtimePlumberSchool implements PlumberSchool if (segment.getVersion().compareTo(sink.getSegment().getVersion()) >= 0) { try { - metadataUpdater.unannounceSegment(sink.getSegment()); + segmentAnnouncer.unannounceSegment(sink.getSegment()); FileUtils.deleteDirectory(computePersistDir(schema, sink.getInterval())); sinks.remove(sinkKey); } @@ -527,7 +534,7 @@ public class RealtimePlumberSchool implements PlumberSchool sink.getSegment().withDimensions(Lists.newArrayList(index.getAvailableDimensions())) ); - metadataUpdater.publishSegment(segment); + segmentPublisher.publishSegment(segment); } catch (IOException e) { log.makeAlert(e, "Failed to persist merged index[%s]", schema.getDataSource()) @@ -711,7 +718,8 @@ public class RealtimePlumberSchool implements PlumberSchool { Preconditions.checkNotNull(conglomerate, "must specify a queryRunnerFactoryConglomerate to do this action."); Preconditions.checkNotNull(dataSegmentPusher, "must specify a segmentPusher to do this action."); - Preconditions.checkNotNull(metadataUpdater, "must specify a metadataUpdater to do this action."); + Preconditions.checkNotNull(segmentAnnouncer, "must specify a segmentAnnouncer to do this action."); + Preconditions.checkNotNull(segmentPublisher, "must specify a segmentPublisher to do this action."); Preconditions.checkNotNull(serverView, "must specify a serverView to do this action."); Preconditions.checkNotNull(emitter, "must specify a serviceEmitter to do this action."); } diff --git a/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java b/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java index 624a7855363..2be03b558d2 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncer.java @@ -18,14 +18,14 @@ public class ZkSegmentAnnouncer implements SegmentAnnouncer private final Object lock = new Object(); - private final MetadataUpdaterConfig config; + private final ZkSegmentAnnouncerConfig config; private final PhoneBook yp; private final String servedSegmentsLocation; private volatile boolean started = false; public ZkSegmentAnnouncer( - MetadataUpdaterConfig config, + ZkSegmentAnnouncerConfig config, PhoneBook yp ) { @@ -83,7 +83,7 @@ public class ZkSegmentAnnouncer implements SegmentAnnouncer return; } - log.info("Stopping MetadataUpdater with config[%s]", config); + log.info("Stopping ZkSegmentAnnouncer with config[%s]", config); yp.unannounce(config.getAnnounceLocation(), config.getServerName()); started = false; diff --git a/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncerConfig.java b/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncerConfig.java new file mode 100644 index 00000000000..131d8acd47a --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/ZkSegmentAnnouncerConfig.java @@ -0,0 +1,23 @@ +package com.metamx.druid.realtime; + +import org.skife.config.Config; +import org.skife.config.Default; + +public abstract class ZkSegmentAnnouncerConfig +{ + @Config("druid.host") + public abstract String getServerName(); + + @Config("druid.host") + public abstract String getHost(); + + @Config("druid.server.maxSize") + @Default("0") + public abstract long getMaxSize(); + + @Config("druid.zk.paths.announcementsPath") + public abstract String getAnnounceLocation(); + + @Config("druid.zk.paths.servedSegmentsPath") + public abstract String getServedSegmentsLocation(); +} From c34108418a5343cf9c3c6c3be10f18c04d9a2f4c Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Thu, 14 Mar 2013 12:49:17 -0700 Subject: [PATCH 11/18] RealtimeIndexTask: Reword comments --- .../merger/common/task/RealtimeIndexTask.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java index 6744f2280f0..999aeec6c6b 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java @@ -158,11 +158,12 @@ public class RealtimeIndexTask extends AbstractTask } }; - // TODO -- This can block if there is lock contention, which will block plumber.getSink (and thus the firehose) - // TODO -- Shouldn't usually happen, since we don't expect people to submit tasks that intersect with the - // TODO -- realtime window, but if they do it can be problematic. - // TODO -- If we decide to care, we can use more threads in the plumber such that waiting for the coordinator - // TODO -- doesn't block data processing. + // NOTE: getVersion will block if there is lock contention, which will block plumber.getSink + // NOTE: (and thus the firehose) + + // Shouldn't usually happen, since we don't expect people to submit tasks that intersect with the + // realtime window, but if they do it can be problematic. If we decide to care, we can use more threads in + // the plumber such that waiting for the coordinator doesn't block data processing. final RealtimePlumberSchool.VersioningPolicy versioningPolicy = new RealtimePlumberSchool.VersioningPolicy() { @Override @@ -180,8 +181,10 @@ public class RealtimeIndexTask extends AbstractTask } }; - // TODO - Might need to have task id in segmentPusher path or some other way of making redundant realtime - // TODO - workers not step on each other's toes when pushing segments to S3 + // NOTE: This pusher selects path based purely on global configuration and the DataSegment, which means + // NOTE: that redundant realtime tasks will upload to the same location. This can cause index.zip and + // NOTE: descriptor.json to mismatch, or it can cause compute nodes to load different instances of the + // NOTE: "same" segment. realtimePlumberSchool.setDataSegmentPusher(toolbox.getSegmentPusher()); realtimePlumberSchool.setConglomerate(toolbox.getQueryRunnerFactoryConglomerate()); realtimePlumberSchool.setVersioningPolicy(versioningPolicy); From b8c08f235a6553a37e0779b19d8e54214c4d864d Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Thu, 14 Mar 2013 12:56:25 -0700 Subject: [PATCH 12/18] Realtime: - Move VersioningPolicy, RetryPolicy outside of RealtimePlumberSchool - Move plumber stuff into its own package, since there's a lot of it --- .../common/index/YeOldePlumberSchool.java | 6 +- .../common/task/IndexGeneratorTask.java | 4 +- .../merger/common/task/RealtimeIndexTask.java | 9 +- .../metamx/druid/realtime/FireDepartment.java | 2 + .../druid/realtime/RealtimeManager.java | 2 + .../IntervalStartVersioningPolicy.java | 12 ++ .../MessageTimeRejectionPolicyFactory.java | 39 +++++++ .../druid/realtime/{ => plumber}/Plumber.java | 2 +- .../realtime/{ => plumber}/PlumberSchool.java | 4 +- .../{ => plumber}/RealtimePlumberSchool.java | 107 +----------------- .../realtime/plumber/RejectionPolicy.java | 9 ++ .../plumber/RejectionPolicyFactory.java | 15 +++ .../ServerTimeRejectionPolicyFactory.java | 34 ++++++ .../druid/realtime/{ => plumber}/Sink.java | 4 +- .../realtime/plumber/VersioningPolicy.java | 14 +++ 15 files changed, 150 insertions(+), 113 deletions(-) create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/plumber/IntervalStartVersioningPolicy.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/plumber/MessageTimeRejectionPolicyFactory.java rename realtime/src/main/java/com/metamx/druid/realtime/{ => plumber}/Plumber.java (97%) rename realtime/src/main/java/com/metamx/druid/realtime/{ => plumber}/PlumberSchool.java (90%) rename realtime/src/main/java/com/metamx/druid/realtime/{ => plumber}/RealtimePlumberSchool.java (89%) create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicy.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicyFactory.java create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/plumber/ServerTimeRejectionPolicyFactory.java rename realtime/src/main/java/com/metamx/druid/realtime/{ => plumber}/Sink.java (97%) create mode 100644 realtime/src/main/java/com/metamx/druid/realtime/plumber/VersioningPolicy.java diff --git a/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java b/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java index 777fc3dd378..c89122f49e3 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/index/YeOldePlumberSchool.java @@ -39,10 +39,10 @@ import com.metamx.druid.loading.DataSegmentPusher; import com.metamx.druid.query.QueryRunner; import com.metamx.druid.realtime.FireDepartmentMetrics; import com.metamx.druid.realtime.FireHydrant; -import com.metamx.druid.realtime.Plumber; -import com.metamx.druid.realtime.PlumberSchool; +import com.metamx.druid.realtime.plumber.Plumber; +import com.metamx.druid.realtime.plumber.PlumberSchool; import com.metamx.druid.realtime.Schema; -import com.metamx.druid.realtime.Sink; +import com.metamx.druid.realtime.plumber.Sink; import org.apache.commons.io.FileUtils; diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/IndexGeneratorTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/IndexGeneratorTask.java index 6eb58ea91c6..8922a0473bf 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/IndexGeneratorTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/IndexGeneratorTask.java @@ -39,9 +39,9 @@ import com.metamx.druid.merger.common.index.YeOldePlumberSchool; import com.metamx.druid.realtime.FireDepartmentMetrics; import com.metamx.druid.realtime.Firehose; import com.metamx.druid.realtime.FirehoseFactory; -import com.metamx.druid.realtime.Plumber; +import com.metamx.druid.realtime.plumber.Plumber; import com.metamx.druid.realtime.Schema; -import com.metamx.druid.realtime.Sink; +import com.metamx.druid.realtime.plumber.Sink; import org.joda.time.DateTime; import org.joda.time.Interval; diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java index 999aeec6c6b..ac0c3a6b77b 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java @@ -22,12 +22,13 @@ import com.metamx.druid.realtime.FireDepartmentConfig; import com.metamx.druid.realtime.FireDepartmentMetrics; import com.metamx.druid.realtime.Firehose; import com.metamx.druid.realtime.FirehoseFactory; -import com.metamx.druid.realtime.Plumber; -import com.metamx.druid.realtime.RealtimePlumberSchool; +import com.metamx.druid.realtime.plumber.Plumber; +import com.metamx.druid.realtime.plumber.RealtimePlumberSchool; import com.metamx.druid.realtime.Schema; import com.metamx.druid.realtime.SegmentAnnouncer; import com.metamx.druid.realtime.SegmentPublisher; -import com.metamx.druid.realtime.Sink; +import com.metamx.druid.realtime.plumber.Sink; +import com.metamx.druid.realtime.plumber.VersioningPolicy; import com.metamx.emitter.EmittingLogger; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -164,7 +165,7 @@ public class RealtimeIndexTask extends AbstractTask // Shouldn't usually happen, since we don't expect people to submit tasks that intersect with the // realtime window, but if they do it can be problematic. If we decide to care, we can use more threads in // the plumber such that waiting for the coordinator doesn't block data processing. - final RealtimePlumberSchool.VersioningPolicy versioningPolicy = new RealtimePlumberSchool.VersioningPolicy() + final VersioningPolicy versioningPolicy = new VersioningPolicy() { @Override public String getVersion(final Interval interval) diff --git a/realtime/src/main/java/com/metamx/druid/realtime/FireDepartment.java b/realtime/src/main/java/com/metamx/druid/realtime/FireDepartment.java index aab4509bbe5..b895cb21040 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/FireDepartment.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/FireDepartment.java @@ -24,6 +24,8 @@ package com.metamx.druid.realtime; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.metamx.druid.realtime.plumber.Plumber; +import com.metamx.druid.realtime.plumber.PlumberSchool; import java.io.IOException; diff --git a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java index 17dba60a847..26cb785fbc0 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/RealtimeManager.java @@ -36,6 +36,8 @@ import com.metamx.druid.query.QueryRunnerFactoryConglomerate; import com.metamx.druid.query.QueryToolChest; import com.metamx.druid.query.segment.QuerySegmentWalker; import com.metamx.druid.query.segment.SegmentDescriptor; +import com.metamx.druid.realtime.plumber.Plumber; +import com.metamx.druid.realtime.plumber.Sink; import com.metamx.emitter.EmittingLogger; import org.joda.time.DateTime; import org.joda.time.Interval; diff --git a/realtime/src/main/java/com/metamx/druid/realtime/plumber/IntervalStartVersioningPolicy.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/IntervalStartVersioningPolicy.java new file mode 100644 index 00000000000..4ad3f123299 --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/IntervalStartVersioningPolicy.java @@ -0,0 +1,12 @@ +package com.metamx.druid.realtime.plumber; + +import org.joda.time.Interval; + +public class IntervalStartVersioningPolicy implements VersioningPolicy +{ + @Override + public String getVersion(Interval interval) + { + return interval.getStart().toString(); + } +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/plumber/MessageTimeRejectionPolicyFactory.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/MessageTimeRejectionPolicyFactory.java new file mode 100644 index 00000000000..117fa6a40eb --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/MessageTimeRejectionPolicyFactory.java @@ -0,0 +1,39 @@ +package com.metamx.druid.realtime.plumber; + +import org.joda.time.DateTime; +import org.joda.time.Period; + +public class MessageTimeRejectionPolicyFactory implements RejectionPolicyFactory +{ + @Override + public RejectionPolicy create(final Period windowPeriod) + { + final long windowMillis = windowPeriod.toStandardDuration().getMillis(); + + return new RejectionPolicy() + { + private volatile long maxTimestamp = Long.MIN_VALUE; + + @Override + public DateTime getCurrMaxTime() + { + return new DateTime(maxTimestamp); + } + + @Override + public boolean accept(long timestamp) + { + maxTimestamp = Math.max(maxTimestamp, timestamp); + + return timestamp >= (maxTimestamp - windowMillis); + } + + @Override + public String toString() + { + return String.format("messageTime-%s", windowPeriod); + } + }; + } +} + diff --git a/realtime/src/main/java/com/metamx/druid/realtime/Plumber.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/Plumber.java similarity index 97% rename from realtime/src/main/java/com/metamx/druid/realtime/Plumber.java rename to realtime/src/main/java/com/metamx/druid/realtime/plumber/Plumber.java index 57366b5ee5b..3487c655efb 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/Plumber.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/Plumber.java @@ -17,7 +17,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package com.metamx.druid.realtime; +package com.metamx.druid.realtime.plumber; import com.metamx.druid.Query; import com.metamx.druid.query.QueryRunner; diff --git a/realtime/src/main/java/com/metamx/druid/realtime/PlumberSchool.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/PlumberSchool.java similarity index 90% rename from realtime/src/main/java/com/metamx/druid/realtime/PlumberSchool.java rename to realtime/src/main/java/com/metamx/druid/realtime/plumber/PlumberSchool.java index 5fcc1f29f7d..7963c58a0d8 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/PlumberSchool.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/PlumberSchool.java @@ -17,11 +17,13 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package com.metamx.druid.realtime; +package com.metamx.druid.realtime.plumber; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.metamx.druid.realtime.FireDepartmentMetrics; +import com.metamx.druid.realtime.Schema; /** */ diff --git a/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/RealtimePlumberSchool.java similarity index 89% rename from realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java rename to realtime/src/main/java/com/metamx/druid/realtime/plumber/RealtimePlumberSchool.java index 84d5058a3ac..d30ef7d7156 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/RealtimePlumberSchool.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/RealtimePlumberSchool.java @@ -17,13 +17,11 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package com.metamx.druid.realtime; +package com.metamx.druid.realtime.plumber; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; @@ -55,6 +53,11 @@ import com.metamx.druid.query.QueryRunner; import com.metamx.druid.query.QueryRunnerFactory; import com.metamx.druid.query.QueryRunnerFactoryConglomerate; import com.metamx.druid.query.QueryToolChest; +import com.metamx.druid.realtime.FireDepartmentMetrics; +import com.metamx.druid.realtime.FireHydrant; +import com.metamx.druid.realtime.Schema; +import com.metamx.druid.realtime.SegmentAnnouncer; +import com.metamx.druid.realtime.SegmentPublisher; import com.metamx.emitter.EmittingLogger; import com.metamx.emitter.service.ServiceEmitter; import com.metamx.emitter.service.ServiceMetricEvent; @@ -572,104 +575,6 @@ public class RealtimePlumberSchool implements PlumberSchool }; } - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes(value = { - @JsonSubTypes.Type(name = "intervalStart", value = IntervalStartVersioningPolicy.class) - }) - public static interface VersioningPolicy - { - public String getVersion(Interval interval); - } - - public static class IntervalStartVersioningPolicy implements VersioningPolicy - { - @Override - public String getVersion(Interval interval) - { - return interval.getStart().toString(); - } - } - - public interface RejectionPolicy - { - public DateTime getCurrMaxTime(); - public boolean accept(long timestamp); - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes(value = { - @JsonSubTypes.Type(name = "serverTime", value = ServerTimeRejectionPolicyFactory.class), - @JsonSubTypes.Type(name = "messageTime", value = MessageTimeRejectionPolicyFactory.class) - }) - public static interface RejectionPolicyFactory - { - public RejectionPolicy create(Period windowPeriod); - } - - public static class ServerTimeRejectionPolicyFactory implements RejectionPolicyFactory - { - @Override - public RejectionPolicy create(final Period windowPeriod) - { - final long windowMillis = windowPeriod.toStandardDuration().getMillis(); - - return new RejectionPolicy() - { - @Override - public DateTime getCurrMaxTime() - { - return new DateTime(); - } - - @Override - public boolean accept(long timestamp) - { - return timestamp >= (System.currentTimeMillis() - windowMillis); - } - - @Override - public String toString() - { - return String.format("serverTime-%s", windowPeriod); - } - }; - } - } - - public static class MessageTimeRejectionPolicyFactory implements RejectionPolicyFactory - { - @Override - public RejectionPolicy create(final Period windowPeriod) - { - final long windowMillis = windowPeriod.toStandardDuration().getMillis(); - - return new RejectionPolicy() - { - private volatile long maxTimestamp = Long.MIN_VALUE; - - @Override - public DateTime getCurrMaxTime() - { - return new DateTime(maxTimestamp); - } - - @Override - public boolean accept(long timestamp) - { - maxTimestamp = Math.max(maxTimestamp, timestamp); - - return timestamp >= (maxTimestamp - windowMillis); - } - - @Override - public String toString() - { - return String.format("messageTime-%s", windowPeriod); - } - }; - } - } - private File computeBaseDir(Schema schema) { return new File(basePersistDirectory, schema.getDataSource()); diff --git a/realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicy.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicy.java new file mode 100644 index 00000000000..847c917dc35 --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicy.java @@ -0,0 +1,9 @@ +package com.metamx.druid.realtime.plumber; + +import org.joda.time.DateTime; + +public interface RejectionPolicy +{ + public DateTime getCurrMaxTime(); + public boolean accept(long timestamp); +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicyFactory.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicyFactory.java new file mode 100644 index 00000000000..40e8e496bf6 --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/RejectionPolicyFactory.java @@ -0,0 +1,15 @@ +package com.metamx.druid.realtime.plumber; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.joda.time.Period; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes(value = { + @JsonSubTypes.Type(name = "serverTime", value = ServerTimeRejectionPolicyFactory.class), + @JsonSubTypes.Type(name = "messageTime", value = MessageTimeRejectionPolicyFactory.class) +}) +public interface RejectionPolicyFactory +{ + public RejectionPolicy create(Period windowPeriod); +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/plumber/ServerTimeRejectionPolicyFactory.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/ServerTimeRejectionPolicyFactory.java new file mode 100644 index 00000000000..3557a8ba3bc --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/ServerTimeRejectionPolicyFactory.java @@ -0,0 +1,34 @@ +package com.metamx.druid.realtime.plumber; + +import org.joda.time.DateTime; +import org.joda.time.Period; + +public class ServerTimeRejectionPolicyFactory implements RejectionPolicyFactory +{ + @Override + public RejectionPolicy create(final Period windowPeriod) + { + final long windowMillis = windowPeriod.toStandardDuration().getMillis(); + + return new RejectionPolicy() + { + @Override + public DateTime getCurrMaxTime() + { + return new DateTime(); + } + + @Override + public boolean accept(long timestamp) + { + return timestamp >= (System.currentTimeMillis() - windowMillis); + } + + @Override + public String toString() + { + return String.format("serverTime-%s", windowPeriod); + } + }; + } +} diff --git a/realtime/src/main/java/com/metamx/druid/realtime/Sink.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/Sink.java similarity index 97% rename from realtime/src/main/java/com/metamx/druid/realtime/Sink.java rename to realtime/src/main/java/com/metamx/druid/realtime/plumber/Sink.java index 70e305b67e4..d1985082622 100644 --- a/realtime/src/main/java/com/metamx/druid/realtime/Sink.java +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/Sink.java @@ -17,7 +17,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package com.metamx.druid.realtime; +package com.metamx.druid.realtime.plumber; import com.google.common.base.Function; import com.google.common.base.Predicate; @@ -32,6 +32,8 @@ import com.metamx.druid.client.DataSegment; import com.metamx.druid.index.v1.IncrementalIndex; import com.metamx.druid.index.v1.IndexIO; import com.metamx.druid.input.InputRow; +import com.metamx.druid.realtime.FireHydrant; +import com.metamx.druid.realtime.Schema; import org.joda.time.Interval; import javax.annotation.Nullable; diff --git a/realtime/src/main/java/com/metamx/druid/realtime/plumber/VersioningPolicy.java b/realtime/src/main/java/com/metamx/druid/realtime/plumber/VersioningPolicy.java new file mode 100644 index 00000000000..5fe790dd284 --- /dev/null +++ b/realtime/src/main/java/com/metamx/druid/realtime/plumber/VersioningPolicy.java @@ -0,0 +1,14 @@ +package com.metamx.druid.realtime.plumber; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.joda.time.Interval; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes(value = { + @JsonSubTypes.Type(name = "intervalStart", value = IntervalStartVersioningPolicy.class) +}) +public interface VersioningPolicy +{ + public String getVersion(Interval interval); +} From e45a51714f0db97cf48e51fbfb884dc5425842e0 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Thu, 14 Mar 2013 13:43:40 -0700 Subject: [PATCH 13/18] RealtimeIndexTask: Fix serde --- .../merger/common/task/RealtimeIndexTask.java | 45 ++++++++++++++++--- .../merger/common/task/TaskSerdeTest.java | 37 +++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java index ac0c3a6b77b..27278537cca 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/RealtimeIndexTask.java @@ -1,6 +1,7 @@ package com.metamx.druid.merger.common.task; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; @@ -39,27 +40,29 @@ import java.io.IOException; public class RealtimeIndexTask extends AbstractTask { - @JsonProperty + @JsonIgnore final Schema schema; - @JsonProperty + @JsonIgnore final FirehoseFactory firehoseFactory; - @JsonProperty + @JsonIgnore final FireDepartmentConfig fireDepartmentConfig; - @JsonProperty + @JsonIgnore final Period windowPeriod; - @JsonProperty + @JsonIgnore final IndexGranularity segmentGranularity; + @JsonIgnore private volatile Plumber plumber = null; private static final EmittingLogger log = new EmittingLogger(RealtimeIndexTask.class); @JsonCreator public RealtimeIndexTask( + @JsonProperty("id") String id, @JsonProperty("schema") Schema schema, @JsonProperty("firehose") FirehoseFactory firehoseFactory, @JsonProperty("fireDepartmentConfig") FireDepartmentConfig fireDepartmentConfig, // TODO rename? @@ -68,7 +71,7 @@ public class RealtimeIndexTask extends AbstractTask ) { super( - String.format( + id != null ? id : String.format( "index_realtime_%s_%d_%s", schema.getDataSource(), schema.getShardSpec().getPartitionNum(), new DateTime() ), @@ -257,6 +260,36 @@ public class RealtimeIndexTask extends AbstractTask return TaskStatus.success(getId()); } + @JsonProperty + public Schema getSchema() + { + return schema; + } + + @JsonProperty("firehose") + public FirehoseFactory getFirehoseFactory() + { + return firehoseFactory; + } + + @JsonProperty + public FireDepartmentConfig getFireDepartmentConfig() + { + return fireDepartmentConfig; + } + + @JsonProperty + public Period getWindowPeriod() + { + return windowPeriod; + } + + @JsonProperty + public IndexGranularity getSegmentGranularity() + { + return segmentGranularity; + } + public static class TaskActionSegmentPublisher implements SegmentPublisher { final Task task; diff --git a/merger/src/test/java/com/metamx/druid/merger/common/task/TaskSerdeTest.java b/merger/src/test/java/com/metamx/druid/merger/common/task/TaskSerdeTest.java index 1f1a8c41038..1bb89b5f899 100644 --- a/merger/src/test/java/com/metamx/druid/merger/common/task/TaskSerdeTest.java +++ b/merger/src/test/java/com/metamx/druid/merger/common/task/TaskSerdeTest.java @@ -8,17 +8,20 @@ import com.metamx.druid.aggregation.AggregatorFactory; import com.metamx.druid.aggregation.CountAggregatorFactory; import com.metamx.druid.aggregation.DoubleSumAggregatorFactory; import com.metamx.druid.client.DataSegment; +import com.metamx.druid.index.v1.IndexGranularity; import com.metamx.druid.indexer.HadoopDruidIndexerConfig; import com.metamx.druid.indexer.data.JSONDataSpec; import com.metamx.druid.indexer.granularity.UniformGranularitySpec; import com.metamx.druid.indexer.path.StaticPathSpec; import com.metamx.druid.indexer.rollup.DataRollupSpec; import com.metamx.druid.jackson.DefaultObjectMapper; +import com.metamx.druid.merger.common.index.StaticS3FirehoseFactory; import com.metamx.druid.realtime.Schema; import com.metamx.druid.shard.NoneShardSpec; import junit.framework.Assert; import com.fasterxml.jackson.databind.ObjectMapper; import org.joda.time.Interval; +import org.joda.time.Period; import org.junit.Test; public class TaskSerdeTest @@ -193,6 +196,40 @@ public class TaskSerdeTest ); } + @Test + public void testRealtimeIndexTaskSerde() throws Exception + { + final Task task = new RealtimeIndexTask( + null, + new Schema("foo", new AggregatorFactory[0], QueryGranularity.NONE, new NoneShardSpec()), + null, + null, + new Period("PT10M"), + IndexGranularity.HOUR + ); + + final ObjectMapper jsonMapper = new DefaultObjectMapper(); + final String json = jsonMapper.writeValueAsString(task); + + Thread.sleep(100); // Just want to run the clock a bit to make sure the task id doesn't change + final Task task2 = jsonMapper.readValue(json, Task.class); + + Assert.assertEquals("foo", task.getDataSource()); + Assert.assertEquals(Optional.absent(), task.getImplicitLockInterval()); + Assert.assertEquals(new Period("PT10M"), ((RealtimeIndexTask) task).getWindowPeriod()); + Assert.assertEquals(IndexGranularity.HOUR, ((RealtimeIndexTask) task).getSegmentGranularity()); + + Assert.assertEquals(task.getId(), task2.getId()); + Assert.assertEquals(task.getGroupId(), task2.getGroupId()); + Assert.assertEquals(task.getDataSource(), task2.getDataSource()); + Assert.assertEquals(task.getImplicitLockInterval(), task2.getImplicitLockInterval()); + Assert.assertEquals(((RealtimeIndexTask) task).getWindowPeriod(), ((RealtimeIndexTask) task).getWindowPeriod()); + Assert.assertEquals( + ((RealtimeIndexTask) task).getSegmentGranularity(), + ((RealtimeIndexTask) task).getSegmentGranularity() + ); + } + @Test public void testDeleteTaskSerde() throws Exception { From a933438e4e266eb1aebf308ccc860188060d8dd6 Mon Sep 17 00:00:00 2001 From: Eric Tschetter Date: Fri, 15 Mar 2013 13:48:55 -0500 Subject: [PATCH 14/18] 1) Fix bugs with VersionConverterTask 2) Fix bugs with NPEs on indexing --- .../indexing/IndexingServiceClient.java | 5 + index-common/pom.xml | 4 + .../v1/ComplexMetricColumnSerializer.java | 0 .../index/v1/FloatMetricColumnSerializer.java | 0 .../index/v1/IncrementalIndexAdapter.java | 0 .../com/metamx/druid/index/v1/IndexIO.java | 21 +++ .../metamx/druid/index/v1/IndexMerger.java | 2 + .../druid/index/v1/IndexableAdapter.java | 0 .../druid/index/v1/MMappedIndexAdapter.java | 0 .../index/v1/MetricColumnSerializer.java | 0 .../v1/QueryableIndexIndexableAdapter.java | 29 +++- .../com/metamx/druid/index/v1/Rowboat.java | 0 merger/pom.xml | 21 --- .../common/task/VersionConverterTask.java | 48 +++++- .../merger/coordinator/LocalTaskRunner.java | 159 ++++++++++++------ pom.xml | 5 + realtime/pom.xml | 37 ---- server/pom.xml | 2 - 18 files changed, 212 insertions(+), 121 deletions(-) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/ComplexMetricColumnSerializer.java (100%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/FloatMetricColumnSerializer.java (100%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/IncrementalIndexAdapter.java (100%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/IndexMerger.java (99%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/IndexableAdapter.java (100%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/MMappedIndexAdapter.java (100%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/MetricColumnSerializer.java (100%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/QueryableIndexIndexableAdapter.java (88%) rename {server => index-common}/src/main/java/com/metamx/druid/index/v1/Rowboat.java (100%) diff --git a/client/src/main/java/com/metamx/druid/client/indexing/IndexingServiceClient.java b/client/src/main/java/com/metamx/druid/client/indexing/IndexingServiceClient.java index b659148d338..34277797cba 100644 --- a/client/src/main/java/com/metamx/druid/client/indexing/IndexingServiceClient.java +++ b/client/src/main/java/com/metamx/druid/client/indexing/IndexingServiceClient.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.metamx.common.IAE; +import com.metamx.common.ISE; import com.metamx.druid.client.DataSegment; import com.metamx.http.client.HttpClient; import com.metamx.http.client.response.InputStreamResponseHandler; @@ -106,6 +107,10 @@ public class IndexingServiceClient { try { final ServiceInstance instance = serviceProvider.getInstance(); + if (instance == null) { + throw new ISE("Cannot find instance of indexingService"); + } + return String.format("http://%s:%s/mmx/merger/v1", instance.getAddress(), instance.getPort()); } catch (Exception e) { diff --git a/index-common/pom.xml b/index-common/pom.xml index d36ea5de375..125abb066c7 100644 --- a/index-common/pom.xml +++ b/index-common/pom.xml @@ -71,6 +71,10 @@ com.google.guava guava + + commons-io + commons-io + diff --git a/server/src/main/java/com/metamx/druid/index/v1/ComplexMetricColumnSerializer.java b/index-common/src/main/java/com/metamx/druid/index/v1/ComplexMetricColumnSerializer.java similarity index 100% rename from server/src/main/java/com/metamx/druid/index/v1/ComplexMetricColumnSerializer.java rename to index-common/src/main/java/com/metamx/druid/index/v1/ComplexMetricColumnSerializer.java diff --git a/server/src/main/java/com/metamx/druid/index/v1/FloatMetricColumnSerializer.java b/index-common/src/main/java/com/metamx/druid/index/v1/FloatMetricColumnSerializer.java similarity index 100% rename from server/src/main/java/com/metamx/druid/index/v1/FloatMetricColumnSerializer.java rename to index-common/src/main/java/com/metamx/druid/index/v1/FloatMetricColumnSerializer.java diff --git a/server/src/main/java/com/metamx/druid/index/v1/IncrementalIndexAdapter.java b/index-common/src/main/java/com/metamx/druid/index/v1/IncrementalIndexAdapter.java similarity index 100% rename from server/src/main/java/com/metamx/druid/index/v1/IncrementalIndexAdapter.java rename to index-common/src/main/java/com/metamx/druid/index/v1/IncrementalIndexAdapter.java diff --git a/index-common/src/main/java/com/metamx/druid/index/v1/IndexIO.java b/index-common/src/main/java/com/metamx/druid/index/v1/IndexIO.java index 621989b0d08..aa10fa8b2ec 100644 --- a/index-common/src/main/java/com/metamx/druid/index/v1/IndexIO.java +++ b/index-common/src/main/java/com/metamx/druid/index/v1/IndexIO.java @@ -204,10 +204,31 @@ public class IndexIO final int version = getVersionFromDir(toConvert); switch (version) { + case 1: + case 2: + case 3: + final String mappableDirName = "mappable"; + if (toConvert.getName().equals(mappableDirName)) { + throw new ISE("Infinite recursion at play! OMFG quit it, please, it hurts!"); + } + + File mappable = new File(toConvert, mappableDirName); + final Index index = readIndex(toConvert); + storeLatest(index, mappable); + + return convertSegment(mappable, converted); + case 4: + case 5: + case 6: + case 7: + log.info("Old version, re-persisting."); + IndexMerger.append(Arrays.asList(new QueryableIndexIndexableAdapter(loadIndex(toConvert))), converted); + return true; case 8: DefaultIndexIOHandler.convertV8toV9(toConvert, converted); return true; default: + log.info("Version[%s], skipping.", version); return false; } } diff --git a/server/src/main/java/com/metamx/druid/index/v1/IndexMerger.java b/index-common/src/main/java/com/metamx/druid/index/v1/IndexMerger.java similarity index 99% rename from server/src/main/java/com/metamx/druid/index/v1/IndexMerger.java rename to index-common/src/main/java/com/metamx/druid/index/v1/IndexMerger.java index 3cd9170f9e2..dc03b8866dd 100644 --- a/server/src/main/java/com/metamx/druid/index/v1/IndexMerger.java +++ b/index-common/src/main/java/com/metamx/druid/index/v1/IndexMerger.java @@ -310,9 +310,11 @@ public class IndexMerger throw new ISE("Couldn't make outdir[%s].", outDir); } +/* if (indexes.size() < 2) { throw new ISE("Too few indexes provided for append [%d].", indexes.size()); } +*/ final List mergedDimensions = mergeIndexed( Lists.transform( diff --git a/server/src/main/java/com/metamx/druid/index/v1/IndexableAdapter.java b/index-common/src/main/java/com/metamx/druid/index/v1/IndexableAdapter.java similarity index 100% rename from server/src/main/java/com/metamx/druid/index/v1/IndexableAdapter.java rename to index-common/src/main/java/com/metamx/druid/index/v1/IndexableAdapter.java diff --git a/server/src/main/java/com/metamx/druid/index/v1/MMappedIndexAdapter.java b/index-common/src/main/java/com/metamx/druid/index/v1/MMappedIndexAdapter.java similarity index 100% rename from server/src/main/java/com/metamx/druid/index/v1/MMappedIndexAdapter.java rename to index-common/src/main/java/com/metamx/druid/index/v1/MMappedIndexAdapter.java diff --git a/server/src/main/java/com/metamx/druid/index/v1/MetricColumnSerializer.java b/index-common/src/main/java/com/metamx/druid/index/v1/MetricColumnSerializer.java similarity index 100% rename from server/src/main/java/com/metamx/druid/index/v1/MetricColumnSerializer.java rename to index-common/src/main/java/com/metamx/druid/index/v1/MetricColumnSerializer.java diff --git a/server/src/main/java/com/metamx/druid/index/v1/QueryableIndexIndexableAdapter.java b/index-common/src/main/java/com/metamx/druid/index/v1/QueryableIndexIndexableAdapter.java similarity index 88% rename from server/src/main/java/com/metamx/druid/index/v1/QueryableIndexIndexableAdapter.java rename to index-common/src/main/java/com/metamx/druid/index/v1/QueryableIndexIndexableAdapter.java index d05864716af..26a9b63add7 100644 --- a/server/src/main/java/com/metamx/druid/index/v1/QueryableIndexIndexableAdapter.java +++ b/index-common/src/main/java/com/metamx/druid/index/v1/QueryableIndexIndexableAdapter.java @@ -19,12 +19,12 @@ package com.metamx.druid.index.v1; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.Closeables; import com.metamx.common.ISE; +import com.metamx.common.logger.Logger; import com.metamx.druid.index.QueryableIndex; import com.metamx.druid.index.column.BitmapIndex; import com.metamx.druid.index.column.Column; @@ -44,6 +44,7 @@ import org.joda.time.Interval; import java.io.Closeable; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; @@ -52,13 +53,35 @@ import java.util.Set; */ public class QueryableIndexIndexableAdapter implements IndexableAdapter { + private static final Logger log = new Logger(QueryableIndexIndexableAdapter.class); + private final int numRows; private final QueryableIndex input; + private final List availableDimensions; + public QueryableIndexIndexableAdapter(QueryableIndex input) { this.input = input; numRows = input.getNumRows(); + + // It appears possible that the dimensions have some columns listed which do not have a DictionaryEncodedColumn + // This breaks current logic, but should be fine going forward. This is a work-around to make things work + // in the current state. This code shouldn't be needed once github tracker issue #55 is finished. + this.availableDimensions = Lists.newArrayList(); + for (String dim : input.getAvailableDimensions()) { + final Column col = input.getColumn(dim); + + if (col == null) { + log.warn("Wtf!? column[%s] didn't exist!?!?!?", dim); + } + else if (col.getDictionaryEncoding() != null) { + availableDimensions.add(dim); + } + else { + log.info("No dictionary on dimension[%s]", dim); + } + } } @Override @@ -76,7 +99,7 @@ public class QueryableIndexIndexableAdapter implements IndexableAdapter @Override public Indexed getAvailableDimensions() { - return input.getAvailableDimensions(); + return new ListIndexed(availableDimensions, String.class); } @Override @@ -161,7 +184,7 @@ public class QueryableIndexIndexableAdapter implements IndexableAdapter { dimensions = Maps.newLinkedHashMap(); - for (String dim : input.getAvailableDimensions()) { + for (String dim : getAvailableDimensions()) { dimensions.put(dim, input.getColumn(dim).getDictionaryEncoding()); } diff --git a/server/src/main/java/com/metamx/druid/index/v1/Rowboat.java b/index-common/src/main/java/com/metamx/druid/index/v1/Rowboat.java similarity index 100% rename from server/src/main/java/com/metamx/druid/index/v1/Rowboat.java rename to index-common/src/main/java/com/metamx/druid/index/v1/Rowboat.java diff --git a/merger/pom.xml b/merger/pom.xml index a9fe0a84f31..cc5826a2f74 100644 --- a/merger/pom.xml +++ b/merger/pom.xml @@ -182,25 +182,4 @@ curator-test - - - - - maven-shade-plugin - - - package - - shade - - - - ${project.build.directory}/${project.artifactId}-${project.version}-selfcontained.jar - - - - - - - diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java index 4f4bd02b734..853207f0cf0 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java @@ -34,6 +34,7 @@ import com.metamx.druid.loading.SegmentLoadingException; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.TaskToolbox; import com.metamx.druid.merger.common.actions.SegmentInsertAction; +import com.metamx.druid.merger.common.actions.SegmentListUsedAction; import com.metamx.druid.merger.common.actions.SpawnTasksAction; import com.metamx.druid.merger.common.actions.TaskActionClient; import org.joda.time.DateTime; @@ -77,7 +78,7 @@ public class VersionConverterTask extends AbstractTask } @JsonCreator - private VersionConverterTask( + private static VersionConverterTask createFromJson( @JsonProperty("id") String id, @JsonProperty("groupId") String groupId, @JsonProperty("dataSource") String dataSource, @@ -85,12 +86,26 @@ public class VersionConverterTask extends AbstractTask @JsonProperty("segment") DataSegment segment ) { - super( - id, - groupId, - dataSource, - interval - ); + if (id == null) { + if (segment == null) { + return create(dataSource, interval); + } + else { + return create(segment); + } + } + return new VersionConverterTask(id, groupId, dataSource, interval, segment); + } + + private VersionConverterTask( + String id, + String groupId, + String dataSource, + Interval interval, + DataSegment segment + ) + { + super(id, groupId, dataSource, interval); this.segment = segment; } @@ -122,6 +137,7 @@ public class VersionConverterTask extends AbstractTask @Override public TaskStatus preflight(TaskToolbox toolbox) throws Exception { + log.info("HLKFJDSLKFJDSKLJFLKDSF -- Preflight for segment[%s]", segment); if (segment != null) { return super.preflight(toolbox); } @@ -224,6 +240,21 @@ public class VersionConverterTask extends AbstractTask throws SegmentLoadingException, IOException { log.info("Converting segment[%s]", segment); + final TaskActionClient actionClient = toolbox.getTaskActionClient(); + final List currentSegments = actionClient.submit( + new SegmentListUsedAction(segment.getDataSource(), segment.getInterval()) + ); + + for (DataSegment currentSegment : currentSegments) { + final String version = currentSegment.getVersion(); + final Integer binaryVersion = currentSegment.getBinaryVersion(); + + if (version.startsWith(segment.getVersion()) && CURR_VERSION_INTEGER.equals(binaryVersion)) { + log.info("Skipping already updated segment[%s].", segment); + return; + } + } + final Map localSegments = toolbox.getSegments(Arrays.asList(segment)); final File location = localSegments.get(segment); @@ -236,8 +267,7 @@ public class VersionConverterTask extends AbstractTask DataSegment updatedSegment = segment.withVersion(String.format("%s_v%s", segment.getVersion(), outVersion)); updatedSegment = toolbox.getSegmentPusher().push(outLocation, updatedSegment); - toolbox.getTaskActionClient() - .submit(new SegmentInsertAction(Sets.newHashSet(updatedSegment)).withAllowOlderVersions(true)); + actionClient.submit(new SegmentInsertAction(Sets.newHashSet(updatedSegment)).withAllowOlderVersions(true)); } else { log.info("Conversion failed."); } diff --git a/merger/src/main/java/com/metamx/druid/merger/coordinator/LocalTaskRunner.java b/merger/src/main/java/com/metamx/druid/merger/coordinator/LocalTaskRunner.java index 5dbe8273d6c..b8a40cdb0b4 100644 --- a/merger/src/main/java/com/metamx/druid/merger/coordinator/LocalTaskRunner.java +++ b/merger/src/main/java/com/metamx/druid/merger/coordinator/LocalTaskRunner.java @@ -19,20 +19,33 @@ package com.metamx.druid.merger.coordinator; +import com.google.common.base.Function; import com.google.common.base.Throwables; +import com.google.common.collect.Collections2; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.metamx.common.guava.FunctionalIterable; import com.metamx.common.lifecycle.LifecycleStop; import com.metamx.common.logger.Logger; +import com.metamx.druid.merger.common.RetryPolicy; import com.metamx.druid.merger.common.TaskCallback; import com.metamx.druid.merger.common.TaskStatus; import com.metamx.druid.merger.common.TaskToolbox; import com.metamx.druid.merger.common.TaskToolboxFactory; import com.metamx.druid.merger.common.task.Task; import org.apache.commons.io.FileUtils; +import org.joda.time.DateTime; +import org.mortbay.thread.ThreadPool; +import javax.annotation.Nullable; import java.io.File; import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; /** * Runs tasks in a JVM thread using an ExecutorService. @@ -42,6 +55,8 @@ public class LocalTaskRunner implements TaskRunner private final TaskToolboxFactory toolboxFactory; private final ExecutorService exec; + private final Set runningItems = new ConcurrentSkipListSet(); + private static final Logger log = new Logger(LocalTaskRunner.class); public LocalTaskRunner( @@ -64,65 +79,39 @@ public class LocalTaskRunner implements TaskRunner { final TaskToolbox toolbox = toolboxFactory.build(task); - exec.submit( - new Runnable() - { - @Override - public void run() - { - final long startTime = System.currentTimeMillis(); - - TaskStatus status; - - try { - log.info("Running task: %s", task.getId()); - status = task.run(toolbox); - } - catch (InterruptedException e) { - log.error(e, "Interrupted while running task[%s]", task); - throw Throwables.propagate(e); - } - catch (Exception e) { - log.error(e, "Exception while running task[%s]", task); - status = TaskStatus.failure(task.getId()); - } - catch (Throwable t) { - log.error(t, "Uncaught Throwable while running task[%s]", task); - throw Throwables.propagate(t); - } - - try { - final File taskDir = toolbox.getTaskDir(); - - if (taskDir.exists()) { - log.info("Removing task directory: %s", taskDir); - FileUtils.deleteDirectory(taskDir); - } - } - catch (Exception e) { - log.error(e, "Failed to delete task directory: %s", task.getId()); - } - - try { - callback.notify(status.withDuration(System.currentTimeMillis() - startTime)); - } catch(Exception e) { - log.error(e, "Uncaught Exception during callback for task[%s]", task); - throw Throwables.propagate(e); - } - } - } - ); + exec.submit(new LocalTaskRunnerRunnable(task, toolbox, callback)); } @Override public Collection getRunningTasks() { - return Lists.newArrayList(); + return runningItems; } @Override public Collection getPendingTasks() { + if (exec instanceof ThreadPoolExecutor) { + ThreadPoolExecutor tpe = (ThreadPoolExecutor) exec; + + return Lists.newArrayList( + FunctionalIterable.create(tpe.getQueue()) + .keep( + new Function() + { + @Override + public TaskRunnerWorkItem apply(Runnable input) + { + if (input instanceof LocalTaskRunnerRunnable) { + return ((LocalTaskRunnerRunnable) input).getTaskRunnerWorkItem(); + } + return null; + } + } + ) + ); + } + return Lists.newArrayList(); } @@ -131,4 +120,76 @@ public class LocalTaskRunner implements TaskRunner { return Lists.newArrayList(); } + + private static class LocalTaskRunnerRunnable implements Runnable + { + private final Task task; + private final TaskToolbox toolbox; + private final TaskCallback callback; + + private final DateTime createdTime; + + public LocalTaskRunnerRunnable(Task task, TaskToolbox toolbox, TaskCallback callback) + { + this.task = task; + this.toolbox = toolbox; + this.callback = callback; + + this.createdTime = new DateTime(); + } + + @Override + public void run() + { + final long startTime = System.currentTimeMillis(); + + TaskStatus status; + + try { + log.info("Running task: %s", task.getId()); + status = task.run(toolbox); + } + catch (InterruptedException e) { + log.error(e, "Interrupted while running task[%s]", task); + throw Throwables.propagate(e); + } + catch (Exception e) { + log.error(e, "Exception while running task[%s]", task); + status = TaskStatus.failure(task.getId()); + } + catch (Throwable t) { + log.error(t, "Uncaught Throwable while running task[%s]", task); + throw Throwables.propagate(t); + } + + try { + final File taskDir = toolbox.getTaskDir(); + + if (taskDir.exists()) { + log.info("Removing task directory: %s", taskDir); + FileUtils.deleteDirectory(taskDir); + } + } + catch (Exception e) { + log.error(e, "Failed to delete task directory: %s", task.getId()); + } + + try { + callback.notify(status.withDuration(System.currentTimeMillis() - startTime)); + } catch(Exception e) { + log.error(e, "Uncaught Exception during callback for task[%s]", task); + throw Throwables.propagate(e); + } + } + + public TaskRunnerWorkItem getTaskRunnerWorkItem() + { + return new TaskRunnerWorkItem( + task, + callback, + null, + createdTime + ); + } + } } diff --git a/pom.xml b/pom.xml index c31f8d8d485..1f66f1e4c39 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,11 @@ commons-logging 1.1.1 + + commons-lang + commons-lang + 2.6 + com.ning compress-lzf diff --git a/realtime/pom.xml b/realtime/pom.xml index 1d61bc68a8a..4d2b0845059 100644 --- a/realtime/pom.xml +++ b/realtime/pom.xml @@ -182,41 +182,4 @@ - - - - - org.scala-tools - maven-scala-plugin - - - -unchecked - -deprecation - - - - - compile - - compile - - compile - - - test-compile - - testCompile - - test-compile - - - - compile - - process-resources - - - - - diff --git a/server/pom.xml b/server/pom.xml index 3e986e60054..3fdc65458b0 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -63,12 +63,10 @@ commons-cli commons-cli - 1.2 commons-lang commons-lang - 2.6 commons-io From ff017fe72a98e37282266833eff5ae5d692de571 Mon Sep 17 00:00:00 2001 From: Eric Tschetter Date: Fri, 15 Mar 2013 13:53:33 -0500 Subject: [PATCH 15/18] [maven-release-plugin] prepare release druid-0.3.22 --- client/pom.xml | 2 +- common/pom.xml | 2 +- druid-services/pom.xml | 4 ++-- examples/pom.xml | 2 +- examples/rand/pom.xml | 2 +- examples/twitter/pom.xml | 2 +- index-common/pom.xml | 2 +- indexer/pom.xml | 2 +- merger/pom.xml | 2 +- pom.xml | 2 +- realtime/pom.xml | 2 +- server/pom.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index b232092d027..a48a3ab8a9a 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/common/pom.xml b/common/pom.xml index 3111998cb7a..ccc8e58cb60 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/druid-services/pom.xml b/druid-services/pom.xml index 58487a127a4..1e7078bcff0 100644 --- a/druid-services/pom.xml +++ b/druid-services/pom.xml @@ -24,11 +24,11 @@ druid-services druid-services druid-services - 0.3.22-SNAPSHOT + 0.3.22 com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/examples/pom.xml b/examples/pom.xml index e3a508b7b1e..4160b2e0801 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/examples/rand/pom.xml b/examples/rand/pom.xml index 63439d5f1c9..d58cef79629 100644 --- a/examples/rand/pom.xml +++ b/examples/rand/pom.xml @@ -9,7 +9,7 @@ com.metamx druid-examples - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/examples/twitter/pom.xml b/examples/twitter/pom.xml index 6436a52a21f..8dd4ad17a25 100644 --- a/examples/twitter/pom.xml +++ b/examples/twitter/pom.xml @@ -9,7 +9,7 @@ com.metamx druid-examples - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/index-common/pom.xml b/index-common/pom.xml index 125abb066c7..21c757b6030 100644 --- a/index-common/pom.xml +++ b/index-common/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/indexer/pom.xml b/indexer/pom.xml index 17573c6b6f4..3fb3955abc0 100644 --- a/indexer/pom.xml +++ b/indexer/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/merger/pom.xml b/merger/pom.xml index cc5826a2f74..3a777f6fedc 100644 --- a/merger/pom.xml +++ b/merger/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/pom.xml b/pom.xml index 1f66f1e4c39..2ca1def454d 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ com.metamx druid pom - 0.3.22-SNAPSHOT + 0.3.22 druid druid diff --git a/realtime/pom.xml b/realtime/pom.xml index 4d2b0845059..8d06e6d25c9 100644 --- a/realtime/pom.xml +++ b/realtime/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 diff --git a/server/pom.xml b/server/pom.xml index 3fdc65458b0..1700119458a 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22-SNAPSHOT + 0.3.22 From 7dacf952d6bb7ffcd34ac60cb63fe89099529e23 Mon Sep 17 00:00:00 2001 From: Eric Tschetter Date: Fri, 15 Mar 2013 13:53:39 -0500 Subject: [PATCH 16/18] [maven-release-plugin] prepare for next development iteration --- client/pom.xml | 2 +- common/pom.xml | 2 +- druid-services/pom.xml | 4 ++-- examples/pom.xml | 2 +- examples/rand/pom.xml | 2 +- examples/twitter/pom.xml | 2 +- index-common/pom.xml | 2 +- indexer/pom.xml | 2 +- merger/pom.xml | 2 +- pom.xml | 2 +- realtime/pom.xml | 2 +- server/pom.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index a48a3ab8a9a..d47b9d70b48 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/common/pom.xml b/common/pom.xml index ccc8e58cb60..14d465c555a 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/druid-services/pom.xml b/druid-services/pom.xml index 1e7078bcff0..256d5db5ce0 100644 --- a/druid-services/pom.xml +++ b/druid-services/pom.xml @@ -24,11 +24,11 @@ druid-services druid-services druid-services - 0.3.22 + 0.3.23-SNAPSHOT com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/examples/pom.xml b/examples/pom.xml index 4160b2e0801..2e70a7edd94 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/examples/rand/pom.xml b/examples/rand/pom.xml index d58cef79629..f058291e86a 100644 --- a/examples/rand/pom.xml +++ b/examples/rand/pom.xml @@ -9,7 +9,7 @@ com.metamx druid-examples - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/examples/twitter/pom.xml b/examples/twitter/pom.xml index 8dd4ad17a25..b4432b4873c 100644 --- a/examples/twitter/pom.xml +++ b/examples/twitter/pom.xml @@ -9,7 +9,7 @@ com.metamx druid-examples - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/index-common/pom.xml b/index-common/pom.xml index 21c757b6030..5b0bad72e7f 100644 --- a/index-common/pom.xml +++ b/index-common/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/indexer/pom.xml b/indexer/pom.xml index 3fb3955abc0..a2dcbf974c5 100644 --- a/indexer/pom.xml +++ b/indexer/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/merger/pom.xml b/merger/pom.xml index 3a777f6fedc..ca4d5bdcc94 100644 --- a/merger/pom.xml +++ b/merger/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/pom.xml b/pom.xml index 2ca1def454d..ac9dabaa23c 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ com.metamx druid pom - 0.3.22 + 0.3.23-SNAPSHOT druid druid diff --git a/realtime/pom.xml b/realtime/pom.xml index 8d06e6d25c9..02d61e07eeb 100644 --- a/realtime/pom.xml +++ b/realtime/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT diff --git a/server/pom.xml b/server/pom.xml index 1700119458a..864acfccdd6 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -28,7 +28,7 @@ com.metamx druid - 0.3.22 + 0.3.23-SNAPSHOT From 72b82b1b1dd95ef83ffe977e224f496dea3ec747 Mon Sep 17 00:00:00 2001 From: Eric Tschetter Date: Fri, 15 Mar 2013 15:50:37 -0500 Subject: [PATCH 17/18] 1) Remove logline that really shouldn't be there. --- .../metamx/druid/merger/common/task/VersionConverterTask.java | 1 - 1 file changed, 1 deletion(-) diff --git a/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java b/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java index 853207f0cf0..457608ef1e6 100644 --- a/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java +++ b/merger/src/main/java/com/metamx/druid/merger/common/task/VersionConverterTask.java @@ -137,7 +137,6 @@ public class VersionConverterTask extends AbstractTask @Override public TaskStatus preflight(TaskToolbox toolbox) throws Exception { - log.info("HLKFJDSLKFJDSKLJFLKDSF -- Preflight for segment[%s]", segment); if (segment != null) { return super.preflight(toolbox); } From 92efba5f305890c9b48d4e94c12036e0985fb5ed Mon Sep 17 00:00:00 2001 From: Eric Tschetter Date: Fri, 15 Mar 2013 16:07:54 -0500 Subject: [PATCH 18/18] Add CLA documents --- DruidCorporateCLA.pdf | Bin 0 -> 71022 bytes DruidIndividualCLA.pdf | Bin 0 -> 67704 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 DruidCorporateCLA.pdf create mode 100644 DruidIndividualCLA.pdf diff --git a/DruidCorporateCLA.pdf b/DruidCorporateCLA.pdf new file mode 100644 index 0000000000000000000000000000000000000000..be61238eb13b58e87a717eba65dadf9ff0dcdbe9 GIT binary patch literal 71022 zcmaI7Wmp_b*Djn8+}#NS1Pks=Ai&`6?yds_mp};a?(S|u6P#cf+}#q~-Sr#xyZ3(H z=UnGIKYCTwswMZT?wRTCnwwftRGf*8nG>1%&+h&tGBhZ}8z#?Mg zYy`HqKxR=ivM_T3utDdQkp%>h&FoBH?0Ei9`xmnpV@ESP=fAC4l+B#%T^voI?$EGe z?#>b_&PL8=F9Qiy0PjB$S;Xz3E>NX7D**VfQqj@gM8(V*pu++M3Sdz+b9V-?NZCTO z2>;!R{M}0Fz2IVzGc&a^61H~-=&(YAaI>)kc!2DB$Sfkz=uk}15?=KGyN16Me-|xo z1$H)b1h9yMp$J9IOzcge)ySIJSvXq)*tpo)xuH1#PR@>IMz+ZA8HwKeW#%nW%g>Lv zmoT>r*C+u(**eeiyfFxfQX(P<6COSxK}FjSMU{n0w73U)6~u!{wpuE+cBdLKg3t5= zK6kPmPro{zPko*`6|`mV{GV^KeIBk2o=F5BF3RS%S^Oe9PPRK92dnp4o=>ZN9ws{i z44&_NTtcuGs|l^U37=)Y_|V+`d=k7JCih`}vrM%y*GK&Gp-2Asc_tjY@U6-|`E8}( z<72eXoz4y3eszb*SOb;H7y2LrpXY1ACnv*eh#VrzL zbjh%6B~MR~H9r98wU%=Ealk-9^0}T2`|LG6yOOQF_FlFE#EPm`ZmBM}>=+1{!7w~rFv(CsRVqHQC zm5u^aHnzBNJ}0n;xY5ZD0d~kyNZv6}N17D8-h41n5@@>=lHG)6%x#BE&1A|wiYu*r z%p&`AiwY3kMqV)>0g>hWTfb;rB+d&5r>4eNhitQ(YSOF#UKW$9El1mUzsQC!=b8uw^#eH-M>DmOsi3Y8*3CfO7(#2WP%w#n&v zfRs@qU)EXZ$yN9cI9~dodZIEai#x-pGrL1jjVv<#ZJ0wL;ABz&avPgG_+`YTv2fsN z`w?YD0a3D_goV_&YJk8#VF9USirMCE1fF$uUa49$!&$G5zxPN0g&+k&pMqL_J}+N< zKeTyl@jL;&>Auuc;EtiW`+fBqSZvkxNzIP(Pk&MzT^7lPZgk0hHtu)(p<5%{-$e^V zi;a{FY4Ob3bM%`K&QW;R1&LC4b^%JSlqE|hA5Xi3J{~N^+))S(F=#3YVw!R?QT$!j zDpgp9?0|2|oZ!CdF~{!t^0)_aa793bYgCb;)(s=?n}H3<+HVpHv8tyaUO8eqo!o7L z9zv@wt$c8OR#=6XkDy0zN@zgc0_>t`v~Av}nG(bFW4dQAVyiqO_-}GoQSZZO@Mz~~ zKI<}1oT!{ffXCaI*6BYZyxp9n9V7*hi=aF7U5jq>sGG)o@?bVKF#Tyf9dJD}ZDOD- zyG_@J=w89;CPp6`

pMj6 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%0DKpW7S4L!1WQ38{&cUUNZ<#n_ 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 X%m~lGJI5KrVGpOS%F}X> z0sFy>L>9?)Zpc%;H~G+6T*)|fR`_`Cv-#R{S&0*~V#kc`ufZ12VHy39@}Imt5TA&v zXSKWbbIn|eB{NW*l+2&)CljE_#|rm*CV^Tf)rADVBnvbRRuS)Nr#y5IYen^@X$&$e z3aK$=^G_Ls?N*iqad7$^$nL=5)>!7NI+`zvCRUf54_RF__TOgcU-!+{1~N~YN3lIP zF*CBZ3a{%BR?*H4CEpFT+`>PYlrO^aSmR%CnJuq=VlTfhNaJX4joCYy2I z8OWbyH|fJQkzanN_p}OY+XLawxm6yrSgZFv9)Nc4H41yPd}=wpo6S#>1$QS4fQOE5 z5#5#~@XkkAHk)!SgZ0z-gTa#FDVhYf^RHYl)O4}FWVtxC|?64L$JRny;3D9h!kE} zJwqOL?|OQ9A#+LO!mi8V8C#EwB(fsK6cTs{CFRasL=d8Xe=*%m)ovzHm%^ggS)<yP;E;)W5`g8L)!{Glpms-pZ~v!U7IHDRmAq}_b^ryiqQlwCnbaB{w;ydwzQues>c zsi|hK3UA%(XGy-=Ei}`4#F+Ce1-5tEtwIk!$^MGim#AiL7N>{__$}>~HijQ3!WxgX zcs~vnJ6((*M{KW7`YPAR<%*CwxamlVnv=#E);%13fb;X`W=>_d`{a3dmk43*X4RE(!HjYIB6JC&eYr3HNFz4DB z#!ShLX45tt=%9PW9;&N?(PC>{$HV;K=zf>?3|DcXNQ`X|fiT&I5gZPx`^A^QuplxiGJK3Dg7 zx;H|ZaOTK<3UucAA_%*tbL+I{mx!6p*8fzA;4*y7&U?2}dU_3BR6c+GQ6o(P4Gfv% z(i0#~awSA4+Rc=u;otLPA0amF1}K0?74OHofNTzWTbQg;1zQjK<`}-scJ5)p5-08Z z4=)HZ?}3mo1kII~_&dgqNkhTD#13{M4_b9j%h|V(+vYQk1PR@@%*ffZ6YsJlX^DC3 zk`8egvP^vMJTdyb3H@Sx%rF<)#*}ugzeyql5o^k;Ly2_7daJ;3!&1C3rDdL-4n$s# z9b|Z;Ag&`unQD!#nM?%xJKVhOg(sTJ@pR3oP+m_9EG3#PN#RRw%<>pmY&;UQ{{+_U zc&Oz5VAOu)Dm?0;J6P9=Nb+JqvH7&TtRS{tf;dx&dY-xEE`a!ZA>w9g3M^q>s>&Ef zO9LU@5QKlj^sRGx0v|Tge2^{p_2=Y$6)1iVl2eD0O5$<-5{=Mm(2e@W=Vjz3FA$!$-q|mliYFtK<$UB zj|pUDY`-Ke#)UBd&+=K2Bw%v|=Vj5{ynI8L+Emo~%P|tcR8HkFbW<>e%VpW9K~9%n z0;Fq6at~M%@TP<-%&|vOK*xz zbCDv8F#P3QLn@#&uZ7kMRR-Y7j~{xUe?`wvp}OCVs)lWpf2u-slg}=cE@Q;@gcgIE z1NtjtD-SQlhkD*ppCl^Z)SKw5y}V`N%mwGo5QA%@#|E1S!JRF=h%>Z~3o~A}M$FvP zobTW9#b?s6I0-uGUdpQ|-8tf3tzm1`+jUL~{ z(tk2`kJdgJZR<6Iu#0o<5%RuqQo$L%A3RBL#ORq$<7D+pyzN!BJ}(G(vwFbJ5wVZAcra);MWN0Yf7K^x8aGX)EIKFEnA(8-dc~{KN?HmV-^qASjQEi8 zev75osc*iHXgeCSqvG1D^hnVShn{M_gGqP3AO8#Hy_pg|gqPUO6pDA>BqGiVbMq#_ z3S-yxqd?6ICW0F8HCd}}SY6YM5z@`li&r}pdbvnDe3-ux(cneK`Qy*rc);Njm<$#^jOT5-kj*`P9+A*hxHSNS7D@FR0RgC zvU!kLLEA605G6+NksgPHyUH70vy{-v)y)K9y}Gh$*(m2)H(ZPOZWkav!!Ahi);Fgj%0#&a^R5UA)ai2&_?-&s0)QZqY<|o;UlG+A7`k3E|--{QD2)d@{ z_i?PMC0sMkIY)7c!N0|moeDb~vfb;NYY#J0GcA{Q;qhj=A(lVwsy_mT3+JQ9AK`n| zG$YpBSglK5deXzij!1*porw)FA{HKvB>p1rTjZaC*{m@Q{Z12XUXvK})OtzI*2s8* z_t6}Gb8G6UbT~JEOV(Rrzu!ttS-U>Ie-sbNh+|{Qyw{0h$mhRs>frArJ_P}9DwedYI2U21OLt%GKOm$-l_1)9)OF0_t^#;NQ_k0|Bn z?ybP<*o+6r_ej6#q8!7{S_H6%7uIJ&5jy7<9*2b@^}4e>ZW#yhxY!yy>F4;rgO!;e zaRo5$F-OW!gEY*)fw~5l;cNZuCQc0;t$ng(n_iRcCS+xby{0>_N5m6wq4nxlUwMN6 z`msGg5zIQ1h)5D&a9~|-Yq<~`+@OtzbSLIcUNBbmb{AIvd5h0nVfi6sF~r{`;cJQ% zFU(QyAM>O=7{g{k=jD}fOP3fOrJTkmqsfDsKhfE1ZI;Y_j_#4;!lf{PtFA6eQZ_Sh zg6(I8x6Nc_cKiY?b5@F@iS#2;HV;)hV1RSpN$nG8ti=xiEYBBfN!B9oh-iemwispC z9%vuLM#_3ii~pU|Zs@RCI{frxa1(sqlC31un<01z?2y!a*sW!4-P9pRmjdH#o1;>z zg%(3v-wLv5GGX-ok0Xn9n;lrgiy#E?EtT*IEgBiXL@tf@KE%|4IxmCb+C1}E?($=G4@qqZGORv_bPK znvu>2;B=vX3zz!Um^TxC#Vg^GEf7q|-Ur}nBi%pdLtxxg;l`A_1Zv@9x%2K73?$e;jAFW+mJy0I^J)6U z3>mSL{mq)&kq9RS>~PJ+0XG4r#V+t**GE>V>_1{|)<4)OW83){rfw8;7=awG7yzc7 zVSEH)x>N?+8d?~ve@*aT4Gtmy-xw{jaRRb9JTQUy^`$JBLc0a|H z546Y%i0oJ7j-0nQqj;U`g@A89a+*%yh-upV*dzaVTnR4D=cYr3drEZ^&)gn4OZpVDM^^#um>dC1K--;uM-l zOffQH6AbzC*Ig{apUf$0iubd)g$UGks(*;fjmovM?fKT5MMsP@`t#~+2f1mo?UX7sXEQ9hC|XiU{9?+k;T zJ9?fnD#S#7ZKj0RkvbsclW~x>jDZbe`j3pxrH~BS$GR8QQO_6@pxt_{R-&f z_Y~}ap!V*Vz73`6wL|2+*xd9+-|21SBC2?icQ6#CLz;OHbKN8j(La00201mQ+{Xm* zHNFHjTc_?tX=RJN_OG(G`YX#H5PKZ$@jnqI##bX?28y&V;UDf>Q9dOng{P~d>tXx= z`+2zKFEVb@u)}DQ+XX*lmXF5ubkQcPK}XsLAT;(;O>RuWUxu7d>4eulU2|8tUMIX^ z7GJT`3RKmp6881Ug*1O+k^^7-Nf$7{l>bVPPUdweN1(+*p6Z3f_;-!1LJ#zEV0T1RN zX&qB~pAx4&+eHa$Zld)f$*%s7VwLKV?83Jfb{1CBLILDts(9W9)MTnE zN{2|Vxs8_r+o1JsHP1n>ZE@%Z`9kD}=HUZ#{#m7|Oj;l`Is)fJ{M$!>X*kO^io~N5 z3V;i5LxwNg{T@*pL9gY&+X#W6#J7uxhf*qpe90N!b2Ht`{kCESf4Exy=*XN594 z?Y^h&--W*zIxD1T8%(diaWEBwu&+TNfx*vcG75c7`T3NuLcb_d7P$K!o?Z?qa^C()u73C?#=G!U)qaH zw4RA6)BZHBgSpI9oY{PkI7Jfi7nOvOO=dIrE~;HJYAY^K&Gy(ihoONvEp#$Nf{7V) zOy)tMv4l9DPQ@LzISW3?R6ILkXHJhnG0V5lGX+Fx2v#NVf*crR+LoNt!t1K|)-tPfv6e88iplopcil2s$v4G|4})J|W1tivfN3k!Al$s=qZpZZ zvg~490Fk>nKNfZqg%0iIdtXnn$LP!%3fkt-uqC}CKHoiUKXaQO^52{0EuIURTyDdJ zpUN&AKgE=t*{Tz?W~#7(GD)?FkCJb(>exeLu}YLJ0Y}x)h(1=dbEU9se*xR1LtA$< zQ0Ue4#X4CJcAD331VjiAdY<7pUbJjq1JB7uhT3%)5A2%iQ%z>GBS4utOI&*CK3!~wPVDb2rrU=34+_i%` z`LS!-(w$jti-lm(9B%p2#R2n<{KWJnyOjx`Cmrpy-FA~+z!rw3L}u`s{ok4aOz3SX!D zdML$GU%%&$q!AF#mE)!v;+-G#9|z#VpjkoKy8X%f5ke>foZWCl+rF!k`22)4PyM`h8GAWOb#^!Drfmo;Yi2tsp(af8@I0VRQ~sE(O$>zx8>W33Z{MBJ zdPd8qHh^vVJUU^bq6E>U?*t7?g}e0{__i|WHbVM7H23_mdN#fys(!cJB2w&x1NaLE zD0EoF?)0q-dX zuwKGDX@dcAnYUx6Y}GHbm&?XkA&TnokZ_&llbURI@8~{ZHt7PCWAy8nT=rJ#0IvZT z#RGf3aY4J8+8@n7>hx@tq?H3HyZiwMGY?n81t#^xxcjHvEjTp0n91eU@!V8_2SO)v z+&5)!WY;|a7oUgGs2*$69;Al~oC1;gC@4v2@HX!jrSxW%2j{o`-h{`Os~B@@l)0!4 z{r<&dL~ma)9mh2u19>|ux-WO`AxEJL9JB!-F;-9OFz{ppKPDZpr&tWQYfqd-26`xY zUp#kyEC=@!ckP4OEl|2uL}QqLtrs5Yiyy-&ZuyPmW`ljEzoUb;F@2@g1)u(Uou`;n zC+n+iS4<>``Cs`QWvFX{-+VU4b)nv4R-G`o@S%<{Dx&)MqF2qs6FKJ{MaN0~V9COqdgnBjKPvK}2+3cdO9`WHVrap=s~6H+K262sM< zhpypRC|*%-yzihFUQv#g7+p6~Y|{;R@pVn1fgm^-^uHC+-0E zYrumbGNLJf(52#ME~JYwhdkDON-*~}y**b$ORe@&PfKm((3p+AodN^1tEljSxP9lUX@+8u5 z{J_)ft-APz8nlm@+Oz%r56%vBjeA?kq=da=2KhMhAkO0Zs-Pcxaj4U>Z0a>n!?snW znFP}F$HbDtVgQUQde}Q(!$$C9aEtM{zEZ@>}3<&V@HFOMk{Xz+0)^KtZdESPk~>6Iv7O04|1KZuqfWDSn7f! zXf$ixD~@Rx;5y9w8rOfk|9ZC(2@TgLKDgQl2FM>AWwD~)7LpU37u6W+esPCQl{?ay z9n~6{&kX9~tNG6PRd8v!DykZlfgs7CKY*Ie2(w{MlA?xsW>rvq zk(1CFjSWv1JuK3Q;2KT&9tIWvBIel~;ZM(->WbC=nL(-6_Pfa*s^6DdyN7;v1w4#W zvmeK9XLZruI4)MG6~PG+!Cy+`GTQl7P$$$ER$?T4x|Xiz;2c|SW_A2Q9`w2BRVQev zoQN0%g)b+TWJxXpa;s6^{of#0DOBTG>9r3(MR$6`N==y^w^jRkY{o{Z$-ZySrs5G^Nk%r)2DV zMD>xBP_g4I(n2dH6Cz4CQ0>dgv^L-T2K}h=wN*$VQyNavolSV@TQvc!;=Vr7)_wP> z{Z)(S2hm2^r-w{A|NbZIx*zb~%$qqag}247e}}EeyhjjJ93Bsz$PWdl-!dFpa zGc@9=cl%&LFrQ6p2|ciily5q%esepnoxb2c8#i-ydoPW?Ao;BF_nZ31IPZ8kAieKt zvBCvqxL5Xu^K5awZmArb5GeGj#M5la0x6n3cahdp@RjM!=9TO`t0&*pgk%}0Tw#8R*Q*S z+90I7j(8r!qIUMLdbJsyK9PV*8(Jo(JBg8|Wd}|X-gn*(unzF;*MGS#zMKpgMj27S zj0oS$!{5WE!i`SCRkfi%gR9$|XJ2=kBc6-A{~Apec{hvM#n~qtOW%}od&7mg#n}cy zVDIF!#S$h#byO>JBo>ipR_`o!Xv!lKl}FKE`o7T$&%q?K0{eg4opo6=@}e`xIg=|! z^a1eTB$cA~=M>Nr_U#giV9{Ty#ON=J?!|I34$In1kDPBQjHiM68O^`nkbAZQS?}uK z4&`Pf#}B|eB#X^{)QZwID%8`HU3@4qSZOHtqqR0)!dq{%2y^$PRA{i%=1=nn$x)^3 zIil;SR(-r%QXBBlm$^wma5ng+XT*H*#Zf<1y)oZ8i$cG)vv3HMUw-@2Cp9%Q{ItKK z*0uNjpzrZ3JgrQ*y}UW9M^T25`4_C;3@-FnWR*o}c`SqFD040=vm$-Zg@uKK#6vov zrb_+p0WTPtyJz>Lo6qS$yp&}9FfDXE^41Tk*!6h;?zgtIhr=%C#X8w$Rx5UP{J06Y zEw+D9KZDJ4cdL3OTvHD1P^FZ>T$d(v!d~qx*s)N4#L_V_pDk8uT z(a@S^;bYSLKF^LX>IE_g0(O(KDDGQ>!PvTkSxC&>8jUU}iF|X{-*=rM9 zt1MEqOGV58b9W?>`oB|{L9zNi{DBjtu@_(Q?msMdVUS()1N35QBF@2C7*Y#Yyw4@6 z5__{Dp6Y1cX~RR~l6}T@tKr~+^5r&Cw{g*|?q?dw!LZ>Aw_#A$mXsRsqcfSF*Y&0O zaM2CB(!!i9+9v&&Qho`j($W>ODdN<@iH01+{{EpG5NcDil z=FUa0q|is=kCvato*1z*ji4cabzjLDp z%gh0RRQeY!V3{CUZPQj3Guc&MHD!dteN&)1ExEG01kVBe{`|nBb1ot{K8pJDV338H z5hQZPkl4phbR42%aFKE6O;2{^rFGFy6B}nllVJSNZ;yX)RS2KbTeUco->usJapD*s zItk40!lNlz%rxaLfsZQoskJ&lPl2m$KRJ%D6xsxS_R>4KuWpw=nKI~r22-G8@4mX2 zi+S$InrO4%gjS44%Wt%%I}A|OB_KuaF%t6CDS^B&ZcS-fdk zq1+;ziz~(~j~oxG`ra zyU^QKkfNA0vCm5I@Upe(edd@Ij`ymlfEJP2$=GC>8Dl(7Y@s3Cc?caC;~JX0qI5IkNug zzL=YqT5xP9IrIGa(&#~v&7r8sM$E|_~(l;+o7d8)2l=b(dP;#=s3EkSIRYV{h29 zJjnUF9=45(4ih6-!gpC4AoLX^Cn;pB)vhD3;5jC%;YQE5fz!!;3RflC;Rbf4j7;w*zs$!R)uo;SR%0{73O7R-<|i-*oHLnC|~rO=sR zQKOt~LzH(9c#=a;yfwE>G4xXEJHH z9=U~NsnGIU!>tCycFiYx+3cD7>+G;1-TlV$^7BLm=_vkOv4x2YkO59?UJh~D>a{Q~ zZd6z&-r3%Y?$>TULDKY8FNm2R(|eM!xwZ?w@z z$c6Z0>Xt51Y;fsF)o9LvaTnk$Wvx5!m+0QEo1K?vs$;{`(UftJD=zq68eemv*eri1r3WvC~qmMp& z2(M>zlk*$G0smRKLCoOZx*wbLY7Q~M|08fn0c@)XeGP<>|K5#PTBqhhoahk~gFz{| z6yS(CJsn>Lg}1>S5T(yvh!FsYVZuf*NUuOo2t%MJ9MUW9NM^(Mhy#SR|GgvphnN_y z2*R+sFG5537Gc2bzqXA3+6FQZrOI^sNoWLXG)4K}n%we6w}4#tD^WX?6%&9dmQBoj z?Fi`SOar*64$g|!_mE&+9a6`R&5=xd{))uogW`GizrBJQxsL@#eF4ZsbK3b?=qN_! z4ucJSY~hE899(~<75AJhqnDNtvK!Hq8uAJciaN2o(5Fo@*Z!e6*Gc21%z@P3^dIW6 z;?!rDQa=vvmjz|XO^?@;-dffI8Rjt_rr-71S?1HmRjKp2r&XF;BcDC8#;gzESPc2j zi2K%A3#9&Kf@w*+j>4woLUTX$=&{5abeU!G?u<|I#cup9R0<-2*JCaEM(@o18hsYI zxR5o5+R|hCE#3y+c>u>A+d2{jE@q+1tpe*S=s#(^-?vDZP{X2vJ(u@#ZqSmZ5k6Ua zTr1;k4g#I`|M)KGBy=a)McMZ;|5~!1O6{QTcwjiEoWlRY@=`Dy1cohO%w)9e`fym3 z46cK8!2QyhbgmO?2{%KUs+LcvbMzR65&e&fc%^<6CeqE>DEcqRgD`@y^`35GNgZ&JL)5L;?Kv7rvskEK)RZtKvb0r|*Z&7oozqA!h<4lx<+yPvWbv*np zO#3N~KAjF0MXrr5L(BKKiY*u709z`&aY>x$Vzf`TnpfZ=2XY;0A>NoX`rh@jhEb|o zYs#p(l+E505;+t5Y5(^L4*S{1^f6YqerSc`)NM0)Y>`D2m9Gy*UDnpX`L1j>sJdzU zL&uPuQ%Qt?GU$)Y@MFVi!4u|Lt&$zfYp>4}lnSphfCSE(1ftcqT)9MR7ZuMadcL~$dcF8o;|4KZG|6Ax7ZL*zHy!O0Rr zBoRH=nQIO3pLveXS6b5(IC=JTuWdYsb(!aDX8B)lzE+%8!K1t=dY^q{jCURa;a$mn zzYdB_?MnQMda_noYuaLlrlE06hD6PlPVM-;38voDoX|^h)sF*NMpu1uy=S{t?1R}w z|1lVO%hX(E9fCnOexY2A^i+%+Y%z$I zXNF~~kMbLQX^IqMo1G2LF8iybl8b=hqkT7xygM7_xEx@(@lOCf#1>!5!PcA$s3VPA zMZogQEnD0TKyV9XE=HL4_x((fCO(tHqOem!(;F=GwCK1mlYa6$zed8~sWRiaydlGk zA0KzJ(QF!ut*%esY-V^CsX)cKk69>qQMP!j$nfFPwmiIiOxKvySGxbA>CD5uK(?0~ z166lLaCrnNSp)6ppIL{a{Nctpf@@8Fh$NdYDFIT|yZVK^lF>nwD8Ia_*fgdRnm{z& zz61Qym^dl+_Q(hZO{Q((Dj--efqD>WQ=1{1y&^NUFp!*gQ0Vh0i}@A;R@+$CI8OSM zZjY?$hNIZvU(kH=N|1EovgDK=dmKIVv2r&-cLp3Dpa5`a*Zm>>yRqMz%h@L;#A2cx z5=0|UqgqA8V-@{`*sc0e*Hg-doQ4XqCEL1p3P2=w%>5{+s)38Qb8!u&#rNNQrJ1SW zo*c#{ggLhfM^nl$Cp)c9US{oaBd1Q@f=<=q9eSSdGMJvOsu5~aNwa0TU7kWf+WAj! z&9#X~v)AMGw~rG$GGO5_ zOXK4PgaC#Jqk8wvMip(iRqv)y{v^?_^iFndI#w*VDu*f_z@k)!{KPWg!;c*l33SZa zxB>o5xfPZHn6UOq%nOde%X8!p#p+%=PxIF?A zAnpn~>{GiVOJ@UnC6`IC`0SOwCN|Atw#!|8vAb>j$U4E%;}fHHW5U?S(EfuTBb$f= z^`2_P2342{ z<*1+q9MYGLYF>2;4H-4y0o6#|qf5b+=Z?L!w-YVtvYqrM#h$I@M~}6Bf?cGM5gCfF z$sk7%IEo*=^!=k9XrKMpn@Nj~Bisj%X79AT7>QE&CSc?fEQ-^w z;w-S;7}w|QW?V1nA(NJSa3UG=$sN5fCL-y1A@+>&JTT^GS&2Q(Bzf?`l(?vIv}o{% zi7IFhKUq>sV<6I}rN;om_~c%TZdwePq~&J)25mSGw04!TFfXY~QWgve`NdRmoavbZ z7^$}2J@tLH^B93_Pob5pEV|i|Hd)pvZi?Gq*4SxJuks4NZx(v4(237vqB4rjLc~+B z$Sd~dUqeiNC1pQR1L6m^hs85@nV;3+NAt%aXk(mf^0tyUBwF^7p;+kh%PS!Qh)U=fB)SgpQCK5NeAp}A z^i#F{Ev1qMliYiRTmINn4vIXoDnuSE@cSN(j2~}flY+Q2nBM2T@7v&u#2I1B7BOht z5&m7AR`s#G(sz-O8}%kef;ssfm)GhixcIh8Zjas4VBAPvIW;OURC(#)6 z?SaHG{N5s>E4$%&s!ZHB;d!)TYUjOT_GtT<`%=;3&0WX`cT;i=r#IhoSH~K_j7A}G zx#9wdXA@rA@Z9%Y)%-S1K&O zQnsy|OQnaNmntqtw}K($gP+NF2OXH)MPUOIOWHd9t6wx6ijIB_w`fK`kTAe+AI`@C z-3oZs*8N_%cs+j*7wrMj-5zq6de`SZa+fgwlD1qG^NODy%ec^=zoY|l6hQ3qy;LwL zjfWn3@FOw;51sGJ^y&yYvC~+;C^RojZ1>qf@YqKRzFREb*JAJDG$1`m)4_CM z5#ThWuV3?+>~U+YNQXgo&`MT0UromJQghSaOK^na?`8E2AHR5~^{LfFUWcO1-q0Hu zcmGeWB1q-ckB1fn9epuaxS$iLd^o_BD4@ImA!x<39xiOFs5PBGpI$lErgkOF__qV| zQk$mWu02=FFd)nj>W59$^}@|Vp^GH9_&w}LS{H|=ov)LICx5m^+-qH+`ZY48%?%f> z&RqfzrhD+SIKnroN2f>G&S8y#(O_T*XSE|RHVc{%7glK4t*sEY_fIEsVGdrn>o;)< z5{3$}9mO||R?#mD-3|l(w4OnSeLaiaZ@UGiC$7cJW4`K4QQm2?0PFyCNqbnAy2amG z?x)0??()I6-dScB@|*T<2XQ5{s}zu5I-i2fj$G99!bCzppZ=U)XzA*LBXTP4$V;5n zova*HiE(|yg|&qq+aOz~eWBb}Z^W`%ufO$E3;6A*kmYB+PTKXHbLO62R4GAZ0_vDu6aZ)c;F+hBo74Fy8fp8knkE@U@Zqs!+z!4dEBO zZ-RSWKMxf_llVM2Wyo=m{cNI#`rlh#)Q_eRfQ?1A0gvM1SXIW?xLEYHtkb2#z-phn zeQ%AAz^0Q`({S^s@ZE=|EfGuRMv|4N8J*AvS$R!!rtcP6w0R7Af(H4L-1cd>9{2%O zA~OF}D_Mj6o6wVOd6>+mj+1&o)Z`Kd+4m!H>DjHaW%P=h8i0snV)QDfGmvnhMNJ!qYdH53D zgkqx7=9o-XgaD>vIcu~c=Z9%rYsx!-vF&|j{;F?AcF4^cnn8HcqpZ=E)BK0L2$Oxi zw%oO55*Ic4Q%BPd%XJHj}d-#_1OwuOl?Emt;u~Q_4t99rget!eC~Z8v4Enp3VS%wBy;(^-V|xW zHS0u~p9w%xYfh_(G)dh3cydp}Z)cM57z3}4%Ft9zd=2g+^`o*gp?Yvt2+6!AVo(E< zlw5Sc;@#2{9i@Tr-#^ObS*cX=eo{*wJ_7zCs(d)(|-VXQek9t0A{r46d z^qcJ=<{F%^)rY2%;rmqCDVACUA@QJ<#5YPMOB#i~zxPRN^zS@o2o*5qp4h)Q&Xg@u zKVxBx`V^LFN%#^C4=D6)q`3oPF8H%}#DnHe%B5s#@?Q!2VgRHeuVj2Z*493kP(a#- z9CPlPQ4M3V%|)hY)2srFP3OK(>ZH)Uu(N8v0X*UBaW_0`WRV19GQgrDw~Y3aXvm*v z0$Gffqm*rGrbzC zsUspdWltA=vBn4bus&4YV(buCueNsplK4*zR%QxV3cbi~N>rM)-te9t&xe9%mM!6X zavKcdE4k%$ctZP_W{>InI;AXHdNY+g+SvYM39p|~CwrIkU~OMw5i8nvCub-J>##dxs>KIq5qdlIFu?AF}Y}D<#p0-jgO?+CGNK-ESPObFlyE z%l1O_xhRW9NKegu<$#pg_I-5|p2jtet#R@kX+o$9eOtb|5Q&iXO+jc{{g0->BM%7} z$YmM}ci|%m8{;>f%;-)colD)*B$;~vm~LQqOzHqVj_Ug(_OOh9uFEoCCm8L)pw}$d zl(P6^8yMEt(N(!{V!NdBXep{t%3;wVG%Qhe!h2a+xo~>E1OQjdDezsQFl`h0p%(I0 zX$@A2(#JcF6#5_ z?B0#Cp`FS5I^P(r0}gF*XWJ4jl*t0_6UP1TCmDtC%E6*OVzDR4&HV6a9s9r{f}ss3?QMF|Jh2qhHCxuHXd_6M1bc zbH*g87O`IsY1Wf$UvrR`De~wAx#}5rESwqICEA%c_J*z6J0kfijI?AF0tPz5NQ=VMdQrU8T-Y4nvy+EgcG51}k!a#EMGm7Laa)a|fp+-QGL4s`WU>cT z0c~c!@&Hk7OSmWd`nLCKvABiwTab2|S*iu8&%)C+?*Lh&J3GJk8di_V-6wx4(#Df9 zztAD&jvkeAsjB6x5|P0w<73NcJtTX`Fw8|+vFb13#XW|i*A+JqEn6E%QjCPRqfCpb z{d&t4KTr!q!;w9fwprY4YhQs@vNgnT@b7b3s0dmbiB8Tcs3&E2WdVY82FI^!lp zo5N9i9<{0;$3k+Q&{1XRU(2heI;_>6_vaPzi*1B0e-@>qdbvi7Nru) zzi9r z3Gr49fT;pgUPA2HD#Zm4qj=Rb;2kcXO*;sVEYoPX_Kd2g`$nOsCoGEn8P=W_WMDLN zoJ(2)R;4og=Pm3I^Q}*t^%uwP-rR7|YJwC*$K^7~v7tn3?7M5ts=(Tng!i2?6RR=z zEwr5>py3qk^Wdn-z0=sfbHSElJ~FJ_oNXx&Mu|U<(??NL~}kO)T7== zJ+rf8f(w4l1jk`~^HBz`nns!blj!5-*d&?v2FByotDRo%0skE|W-3GJN;VQjecj(m zWu#i14R{ep315GU7|iGlr7_@pD?lX1P(dIMlHeor1iu~f`%HyFEkI+Ji-tE@5Lxnk zq}W$c{<__jJayVu0*@6*s;*C$!`zpZ>2$fRTOgdNok`7k8SI*i79%bEY)#{%(?#iZekp*c8}|1+`9eG+H)IlgV#hglTdQ)6{R#-F@8Zyw!B-Sf z?BTJi+Su%-1pDmpVOEv<-^UUW(V_WDtMDK1sv&H5t}@E}eplLK>Lqbl*1z+~!&B6% zim;1;G1?nYLXEtPyVS?i#M`QN^G^&HpL6<)I|U3w#6PKlwQY+FsN+WRjktE85k(7H z-0>R(Soz|>c9pL$w`>Io+Np(qKsv|J2zu>b;aI|k7h)ZZ?LG2si<9@6W^ssu^E2B* zu34MHMRD=c1V0_tPon`l|=y0xw1BGVq^&*nR(sn~!Z?}ejab!AW-+gs? z%M9n9-;1K#g^q2!deA z!qM>QAuZJ)373?~6!As69D?7ZcnO4974J0hzHmsHfHMf+p9gYe%0ndh$lqx0bl`YO zm?Ws}&jVs=5TD)BYiqU#`X?!6#!2dT#3sS1hZ$VrI*3%JN_0JW%vRkU*pp^d{K7}2 z)+E;vk4WvHggqghFeczHd)nuzDEBaD!9`4@sGZ8gAGetmUQVm`z-zA-Z-*dM!}9Ek z=!kQ<<-Os8t{v^mnHMFDD##ykDQYRA6HIG+?Z&U9Pbj0uA#`nh2oW98XTkpy%CV=s za)rAui4dO28Gl%`!ygyKw+pq zXI#o}B=Yjs=(-@hCcrNhAp9Wb6y*JgiY#_8kAZx9sC+_`psQ^)s8 z%VSZ8fqZUMY6nu5I56|9X(>7y#hM%-eZ%K3$}j0=#$qyQNvWKi6+Jl3{doQ@_5LTT zx`Be_-gCa({`Evt^{*aY`C8NCC$mM~$-ob(t`Q=(379RnOmXAOm)dujz3{5PG%uX%AUSkZ!gD!kVK8(3 zPh9>Xf4Xsv$dNix&WjefvNx9dsxYh4HHx=a5#uDsZ~92kuXh?(%XsrGS}QGqIqvW5 zP@H^S^Hzsct7;V##Xr-Q#eg$DuuwKzk`xuSU8kor_J+Nir?#RyK7#ii8KbwiXN-i3 zs)@RrN|6-#csc4V{cUc5f1erN-~vS&?Vu{b_4gA#QF&~5#i}09;PvaZ!Q!;?+!uoH z@DoMk0xKH;z|_iwG$HbbcD#gS2>_2vn;tUi?U zp>v5g1&v*-XWA;F4jss5FbAu37Ik}V9JeR|C8*J{rNRLgek_wHJjY%URw^xsoSG!? zq;P&gGyCh8BvPs_$UfR1{|3TqPyT^%VrWzA&mdUAWn?`iz{E9)J zJ9n4fyXb!{EY68*(F~^C1KCYBSb=YcP4p)-4S*qMItXe%&%q$c;hJhcy3|b{a}jgz zNXw?sDCc=XfuOY`<7uVwNhQ|2Ivu48qMhTr=&H(Y<&~ zb-bO{=f&aq&ex64?fw6YcljUk;{OLq;`>kU|HD@Nzo?{Uj7@p+uv_vJSF>8#NFONQ?)SK_;nXxi&TyLK<3=d*`$aN7&O>+xQc zBaT7GN?r6Ti?!kt<}f^ncI2g7)aSwJuQ`!TFjcKENY|&z@$3ZLYWfY$Y^I<4vAcY= z&FjVKHgtP4we;a%Y8fs1i?`Rq@oa+Kg4c2jt@dkBVN_UTIyjASRt0LOZ z1?X`zCQMZ776Twu*7+u5W}moE5q;f00+oc+`F%Ot$(sjs$G9;E>_uBls+~L^RFi0h z@~J8nQh7uLl}2P=uR5pc;@V^@mBqs?HKv2k56!+Pq*$ViM~jus7YJi5n9_kt+~nkO zH0X2=rmuT_%Ck?06ZC^)00%5S?akn0}k_I)3wPNCA# zIFVcPL(We7nyEHX{=?0&Z}LF6a(R$kR%_ENXu*n|lx5j!nAYX5!~6QNuIPiRC{e?O z_pyLU7kfE}j&}>y{m$K=+BYoaU%ittag+}_f(Y_q%iCE#t?@c;2|QpGXbhU`y%Rjd zuRPw5y~ARqgE1_BQ}4FS&IX+kQfe2?w3Ti0Vkgls#+4wrn*GQ`d$rU*aW<4QTAIxo zIts9<@Ch18`wi;Ni*xnz*evec+M|hjoAQFdd+oPLn0azTgnj)yJ=+8x&896HF@#k5 zzpCfbg!?RCiyu&HdhyhZMzE^u4+iLF%;KxE`&-njoLh;_j$7#!uUQhSD&k=Y_vwjo z6kxW|iPTnJW&w-3Pzwty4yS^v3^^yTL2cjy{mj&P{Bf1QIw3(H)CbB$?-s0zg_;Pht@sY4Dk& zPEy|WNmeAE7vK%ynZ zs$hD+rXHoic2xxU$mE(maz{7D@ayk;`z)TPKO|HE1>34t;%mi|c{0Xax$Gu&6&rPp z@s4u#4$Is*Uu5IM5eamjxg6a<3F4G+A}uh%E|o-azZiVVGEsjG-$R2!6=Q@MuCz(; zctP)ku`bWrh0cHzc}P(iTs`%atVX69@ws|g-R!*vh6VF*mQ%`U9P#MYo-KK~nskqR zsk(uV*?X92%hrIUP-*bCM)arZmc7^Ms4KfhhPzj4OL~)~G}g^eG__-!`uWEn>9a+m zajIuo%ERk0HxH|S@XIrlT&~tyyX-(p~UgYr6?Q}1WVVPy@pZBsFev@X3 z@?A+kouC$eQ$Kd4;GWVjsfGOpXM@r8Z!vw+>Ji5~t@m_K4oLuFX_Lhg4QQ;x%q64U zf;B_*j4Iz!_n*vg2<1|SkcMbvJc*a{jMB1Zh@e{Zc~$qzM#1#LMX)1H5jgxPVh1Bl zVN40kxLtno_u)YrZrMakts!aGC|$uz*hn;ES(*vTEiyALBK%_E2>PeUCA) z+CshpMiX=#t?QFS01Kw`;pYt7pE+?Pq@vy?H)b4<38vP9E^J3XHiq@hFj^dbk zVo*Fsy&kCu=$E1#=g7&KMYK4OVj180hIRlarVizx@2~UT6v2{(nTzZ}mYTjbIXiP+ zVg-qHk#tsB`aVvV$5*bILDEkoy5!h-C{faRJnh+M>^{l0_Lj2YCu*NID~mIwvp$cq zU@GMOCNmd54W1gpYi)iHBTwq$(bq{4&>ypVfqcg1JRk6(9(USzO-P@o*R~YS{ZukM z2V`M0JygokzZ$3j4ygp8tMmB?NAAsyP0csMr0q1ObZiX%k*N)nW6D<5r0vK(uNV3n znM;2ng<;z}^Th8(rN3K$_9tiL)j@Wfx6y`^eZTlCjNT z>>-7+kA19ulY}z1jGZx|sE|=nMk4*D-*bM?IZyrW zKlk%_pZDkcz4!ig&%NiK*C{}@@OkT1^tEhwRDHLfoluKIqcxykh3X1&HGORZDn{wr z@#}wL8Jrc24HheFdHDx*C~h0YjcdN&{IO!kQJsB#jn`C9==U%EfHKoxeC=*73R*UzVa@ zV!u$Ouc{~l(9`P3AbvNYJ(unh#mS4QQRfchiwqJIY+*`ajP)r1OiD=Q%}7Sd=cIwVC!o}K1X6C+&Xaf$6gB5L{*Vj`&~9%+7_kpcD3`piZieAi&L2}I&9 z+6Hu}^&cTlE9@jQe6k10%B*(zd<^$2_M&Y}yjS)Dm}#}i+dQvcY~x5;cK}{SXxYrg z!~=B9$3=S#i>lZ+EoPbk#}Xer96jhmTkP!-T8D`1MBY=!4W$8dGxpPfgbbrab34&; zl=0Cnw-*u}`iC_a&4Wb|m(QI9NY8?fJC0hskWi&_R4#v15`J{k8Z54DKB(E{Hj~yJ z*w09@2A@Pns{AodA*7GE3Yn$1Ap}3*g5_B;Q43 z+c*9tL<&9#Z?MYSY&~`4Qo&9wQ{%Cx^0J|-FRiMFmisvw-kngj%;{qVx;ObdQzGIw zfguSm#yU6!F7DIeC304eaQp$@1qx*Mj}lM@hTv~9&UL~Vu1IK|<6rueFgjDbn6zQp zzr44b3}16EO55|3)&r7{(8GsE7yPbm@4tdr2{;1W>u=pBsq0y{Ugg+?C>uYL+FbA8*5J5gG{|+3%-b9uAU5vbz!6@@UVQ5^&fj8C%)5>x$4R~24 zWY)p;=y~5$8LcXq373ZSF~jn{ofK-;;mYFBhr1E5&sTcpO2;7sdb6JCo?DN)PPR9Z z=)|S9RE_x@Y1Kw`oa1n_&S$R=(&fPwGLC;fcTMR%?~02Oa1sgP{|>eDMz0>Fl&r@V zQdF50o$=QWOqySzW-_e|$avTAve);cx7$fKFn+?HxV~cVBMh7+!$ryC!FS`MBhgvy zg&!S>Fk}19`sed(kuB=%I>x7_(}}7XCQ%wRalpO8vU5YqNS%GR#IAmgx4E$)v;c#> z+JmlN10p@Ve10SzoIoVXdvYt8;W`a?TNt{*-+5<;{3Can2KX_mU9m)FR2r=s|Fvb< zp|bkZctAU*BSvcvF4{i2slp_7c%W)!-t0Y_t|mD z1%D~c0nQGFyf0-kAKHBNq*BWXbdh6R{GHUW+jP56tmvIPuz8OZC^@qaede)4D|e#1 z=GN@^U`)@KO-OMisB0#N)Z5=Z zwK+RranZT~=A9{yG+L@X&g1y3ra>AhZ9-01xxjA(*cG>JV1+TNGi4@KeBtyUcFM+~ z!%(#~l}W4t(>5K1zMu!KtXo0VTC$r;ZMZ4eybr>J%6BEQyLqn>O>ye^GxF~O{+ugH zkq4>?bhBe~rV(;qyT9ZF{s`jKsjbdP8$4pz^??U^dlwVxIp>ywD0L5qSNrs<(9V~O%%Z19Bwge45>-ox zy0}&!AKgm@5xrMKQySH!*6m)N&dfECt(0#U5@Zn03 zA>9_cb3ruGRpuMZ4_7zYoPLKc5uMj2;Xd`iE}+%cuJ%OCnX2(^oEye{p15>7DJXQa zbcAy&*|MiZTrKA8NK5kYScBM*wl;hp?L<0O!ueH=}j!#(+j4+4ckIry2MG;Qe5J}_A0|M+xtwSTJB(gN%L zGGSZ)CVH2i_FU!{OmL2FCgSSUz=!(9lRZ&iDUfzeGg(hDZK) Qadp6)3G*l@nAw^C2U89JZ2$lO literal 0 HcmV?d00001 diff --git a/DruidIndividualCLA.pdf b/DruidIndividualCLA.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2c5a4a94b7f266b42980f74804a0f705d54ac1f7 GIT binary patch literal 67704 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 literal 0 HcmV?d00001