diff --git a/.gitignore b/.gitignore index 02816a120e8..636570f7e76 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,7 @@ /classes /lib deploy/lib -deploy/logs .emacs-project -*.jar bin/jzmq .DS_Store deploy/classes @@ -21,17 +19,41 @@ _release *.zip *.tar.gz .lein-deps-sum -*.iml target /.project/ /.lein-plugins/ -*.ipr -*.iws -.idea -.* +#.* !/.travis.yml !/.gitignore _site dependency-reduced-pom.xml -derby.log metastore_db +build +/docs/javadocs +*.class + +# logs +logs +*.log + +# Eclipse +.settings/ +.project +.classpath + +# Intellij +*.iml +*.ipr +*.iws +.idea + +# Package Files +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# ignore vagrant files +/integration-test/config/.vagrant/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 27484eb4eae..f9149f4469a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +addons: + hosts: + - node1 + +env: + - MODULES=storm-core + - MODULES='!storm-core' + - MODULES='INTEGRATION-TEST' + language: java jdk: - oraclejdk7 @@ -17,4 +26,12 @@ before_install: - rvm use 2.1.5 --install - nvm install 0.12.2 - nvm use 0.12.2 -script: /bin/bash ./dev-tools/travis/travis-build.sh `pwd` +install: /bin/bash ./dev-tools/travis/travis-install.sh `pwd` +script: + - /bin/bash ./dev-tools/travis/travis-script.sh `pwd` $MODULES +sudo: true +cache: + directories: + - "$HOME/.m2/repository" + - "$HOME/.rvm" + - "$NVM_DIR" diff --git a/CHANGELOG.md b/CHANGELOG.md index 77977e02488..420a66f0ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,707 @@ -## 0.11.0 +## 1.1.1 + * STORM-2315: New kafka spout can't commit offset when ack is disabled + * STORM-2467: Use explicit charset when decoding from array backed buffer + * STORM-1114: Race condition in trident zookeeper zk-node create/delete + * STORM-2429: Properly validate supervisor.scheduler.meta + * STORM-2194: Stop ignoring socket timeout error from executor + * STORM-2451: windows storm.cmd does not set log4j2 config file correctly by default + +## 1.1.0 + * STORM-2432: Storm-Kafka-Client Trident Spout Seeks Incorrect Offset With UNCOMMITTED_LATEST Strategy + * STORM-2425: Storm Hive Bolt not closing open transactions + * STORM-2409: Storm-Kafka-Client KafkaSpout Support for Failed and NullTuples + * STORM-2423: Join Bolt should use explicit instead of default window anchoring for emitted tuples + * STORM-2416: Improve Release Packaging to Reduce File Size + * STORM-2414: Skip checking meta's ACL when subject has write privileges for any blobs + * STORM-2038: Disable symlinks with a config option + * STORM-2240: STORM PMML Bolt - Add Support to Load Models from Blob Store + * STORM-2412: Nimbus isLeader check while waiting for max replication + * STORM-2408: build failed if storm.kafka.client.version = 0.10.2.0 + * STORM-2403: Fix KafkaBolt test failure: tick tuple should not be acked + * STORM-2361: Kafka spout - after leader change, it stops committing offsets to ZK + * STORM-2353: Replace kafka-unit by kafka_2.11 and kafka-clients to test kafka-clients:0.10.1.1 + * STORM-2387: Handle tick tuples properly for Bolts in external modules + * STORM-2345: Type mismatch in ReadClusterState's ProfileAction processing Map + * STORM-2400: Upgraded Curator to 2.12.0 and made respective API changes + * STORM-2396: setting interrupted status back before throwing a RuntimeException + * STORM-1772: Adding Perf module with topologies for measuring performance + * STORM-2395: storm.cmd supervisor calls the wrong class name + * STORM-2391: Move HdfsSpoutTopology from storm-starter to storm-hdfs-examples + * STORM-2389: Avoid instantiating Event Logger when topology.eventlogger.executors=0 + * STORM-2386: Fail-back Blob deletion also fails in BlobSynchronizer.syncBlobs. + * STORM-2388: JoinBolt breaks compilation against JDK 7 + * STORM-2374: Storm Kafka Client Test Topologies Must be Serializable + * STORM-2372: Pacemaker client doesn't clean up heartbeats properly + * STORM-2326: Upgrade log4j and slf4j + * STORM-2334: Join Bolt implementation + * STORM-1363: TridentKafkaState should handle null values from TridentTupleToKafkaMapper.getMessageFromTuple() + * STORM-2365: Support for specifying output stream in event hubs spout + * STORM-2250: Kafka spout refactoring to increase modularity and testability + * STORM-2340: fix AutoCommitMode issue in KafkaSpout + * STORM-2344: Flux YAML File Viewer for Nimbus UI + * STORM-2350: Storm-HDFS's listFilesByModificationTime is broken + * STORM-2270 Kafka spout should consume from latest when ZK partition commit offset bigger than the latest offset + * STORM-1464: storm-hdfs support for multiple output files and partitioning + * STORM-2320: DRPC client printer class reusable for local and remote DRPC + * STORM-2281: Running Multiple Kafka Spouts (Trident) Throws Illegal State Exception + * STORM-2296: Kafka spout no dup on leader changes + * STORM-2244: Some shaded jars doesn't exclude dependency signature files + * STORM-2014: New Kafka spout duplicates checking if failed messages have reached max retries + * STORM-1443: [Storm SQL] Support customizing parallelism in StormSQL + * STORM-2148: [Storm SQL] Trident mode: back to code generate and compile Trident topology + * STORM-2331: Emitting from JavaScript should work when not anchoring. + * STORM-2225: change spout config to be simpler. + * STORM-2323: Precondition for Leader Nimbus should check all topology blobs and also corresponding dependencies + * STORM-2330: Fix storm sql code generation for UDAF with non standard sql types + * STORM-2298: Don't kill Nimbus when ClusterMetricsConsumer is failed to initialize + * STORM-2301: [storm-cassandra] upgrade cassandra driver to 3.1.2 + * STORM-1446: Compile the Calcite logical plan to Storm Trident logical plan + * STORM-2303: [storm-opentsdb] Fix list invariant issue for JDK 7 + * STORM-2236: storm kafka client should support manual partition management + * STORM-2295: KafkaSpoutStreamsNamedTopics should return output fields with predictable ordering + * STORM-2300: [Flux] support list of references + * STORM-2297: [storm-opentsdb] Support Flux for OpenTSDBBolt + * STORM-2294: Send activate and deactivate command to ShellSpout + * STORM-2280: Upgrade Calcite version to 1.11.0 + * STORM-2278: Allow max number of disruptor queue flusher threads to be configurable + * STORM-2277: Add shaded jar for Druid connector + * STORM-2274: Support named output streams in Hdfs Spout + * STORM-2204: Adding caching capabilities in HBaseLookupBolt + * STORM-2267: Use user's local maven repo. directory to local repo. + * STORM-2254: Provide Socket time out for nimbus thrift client + * STORM-2200: [Storm SQL] Drop Aggregate & Join support on Trident mode + * STORM-2266: Close NimbusClient instances appropriately + * STORM-2203: Add a getAll method to KeyValueState interface + * STORM-1886: Extend KeyValueState iface with delete + * STORM-2022: update Fields test to match new behavior + * STORM-2020: Stop using sun internal classes + * STORM-1228: port fields_test to java + * STORM-2104: New Kafka spout crashes if partitions are reassigned while tuples are in-flight + * STORM-2257: Add built in support for sum function with different types. + * STORM-2082: add sql external module storm-sql-hdfs + * STORM-2256: storm-pmml breaks on java 1.7 + * STORM-2223: PMML Bolt. + * STORM-2222: Repeated NPEs thrown in nimbus if rebalance fails + * STORM-2190: reduce contention between submission and scheduling + * STORM-2239: Handle InterruptException in new Kafka spout + * STORM-2087: Storm-kafka-client: Failed tuples are not always replayed + * STORM-2238: Add Timestamp extractor for windowed bolt + * STORM-2235: Introduce new option: 'add remote repositories' for dependency resolver + * STORM-2215: validate blobs are present before submitting + * STORM-2170: [Storm SQL] Add built-in socket datasource to runtime + * STORM-2226: Fix kafka spout offset lag ui for kerberized kafka + * STORM-2224: Exposed a method to override in computing the field from given tuple in FieldSelector + * STORM-2220: Added config support for each bolt in Cassandra bolts, fixed the bolts to be used also as sinks. + * STORM-2205: Racecondition in getting nimbus summaries while ZK connectionions are reconnected + * STORM-2182: Refactor Storm Kafka Examples Into Own Modules. + * STORM-1694: Kafka Spout Trident Implementation Using New Kafka Consumer API + * STORM-2173: [SQL] Support CSV as input / output format + * STORM-2177: [SQL] Support TSV as input / output format + * STORM-2172: [SQL] Support Avro as input / output format + * STORM-2185: Storm Supervisor doesn't delete directories properly sometimes + * STORM-2103: [SQL] Introduce new sql external module: storm-sql-mongodb + * STORM-2175: fix double close of workers + * STORM-2109: Under supervisor V2 SUPERVISOR_MEMORY_CAPACITY_MB and SUPERVISOR_CPU_CAPACITY must be Doubles + * STORM-2110: in supervisor v2 filter out empty command line args + * STORM-2117: Supervisor V2 with local mode extracts resources directory to topology root directory instead of temporary directory + * STORM-2131: Add blob command to worker-launcher, make stormdist directory not writeable by topo owner + * STORM-2018: Supervisor V2 + * STORM-2139: Let ShellBolts and ShellSpouts run with scripts from blobs + * STORM-2072: Add map, flatMap with different outputs (T->V) in Trident + * STORM-2134: improving the current scheduling strategy for RAS + * STORM-2125: Use Calcite's implementation of Rex Compiler + * STORM-1546: Adding Read and Write Aggregations for Pacemaker to make it HA compatible + * STORM-1444: Support EXPLAIN statement in StormSQL + * STORM-2099: Introduce new sql external module: storm-sql-redis + * STORM-2097: Improve logging in trident core and examples + * STORM-2144: Fix Storm-sql group-by behavior in standalone mode + * STORM-2066: make error message in IsolatedPool.java more descriptive + * STORM-1870: Allow FluxShellBolt/Spout set custom "componentConfig" via yaml + * STORM-2126: fix NPE due to race condition in compute-new-sched-assign… + * STORM-2124: show requested cpu mem for each component + * STORM-2089: Replace Consumer of ISqlTridentDataSource with SqlTridentConsumer + * STORM-2118: A few fixes for storm-sql standalone mode + * STORM-2105: Cluster/Supervisor total and available resources displayed in the UI + * STORM-2078: enable paging in worker datatable + * STORM-1664: Allow Java users to start a local cluster with a Nimbus Thrift server. + * STORM-1872: Release Jedis connection when topology shutdown + * STORM-2100: Fix Trident SQL join tests to not rely on ordering + * STORM-1837: Fix complete-topology and prevent message loss + * STORM-2098: DruidBeamBolt: Pass DruidConfig.Builder as constructor argument + * STORM-2092: optimize TridentKafkaState batch sending + * STORM-1979: Storm Druid Connector implementation. + * STORM-2057: Support JOIN statement in Storm SQL + * STORM-1970: external project examples refator + * STORM-2074: fix storm-kafka-monitor NPE bug + * STORM-1459: Allow not specifying producer properties in read-only Kafka table in StormSQL + * STORM-2052: Kafka Spout New Client API - Log Improvements and Parameter Tuning for Better Performance. + * STORM-2050: [storm-sql] Support User Defined Aggregate Function for Trident mode + * STORM-1434: Support the GROUP BY clause in StormSQL + * STORM-2016: Topology submission improvement: support adding local jars and maven artifacts on submission + * STORM-1994: Add table with per-topology & worker resource usage and components in (new) supervisor and topology pages + * STORM-2042: Nimbus client connections not closed properly causing connection leaks + * STORM-1766: A better algorithm server rack selection for RAS + * STORM-1913: Additions and Improvements for Trident RAS API + * STORM-2037: debug operation should be whitelisted in SimpleAclAuthorizer. + * STORM-2023: Add calcite-core to dependency of storm-sql-runtime + * STORM-2036: Fix minor bug in RAS Tests + * STORM-1979: Storm Druid Connector implementation. + * STORM-1839: Storm spout implementation for Amazon Kinesis Streams. + * STORM-1876: Option to build storm-kafka and storm-kafka-client with different kafka client version + * STORM-2000: Package storm-opentsdb as part of external dir in installation + * STORM-1989: X-Frame-Options support for Storm UI + * STORM-1962: support python 3 and 2 in multilang + * STORM-1964: Unexpected behavior when using count window together with timestamp extraction + * STORM-1890: ensure we refetch static resources after package build + * STORM-1988: Kafka Offset not showing due to bad classpath. + * STORM-1966: Expand metric having Map type as value into multiple metrics based on entries + * STORM-1737: storm-kafka-client has compilation errors with Apache Kafka 0.10 + * STORM-1968: Storm logviewer does not work for nimbus.log in secure cluster + * STORM-1910: One topology cannot use hdfs spout to read from two locations + * STORM-1960: Add CORS support to STORM UI Rest api + * STORM-1959: Add missing license header to KafkaPartitionOffsetLag + * STORM-1950: Change response json of "Topology Lag" REST API to keyed by spoutId, topic, partition. + * STORM-1833: Simple equi-join in storm-sql standalone mode + * STORM-1866: Update Resource Aware Scheduler Documentation + * STORM-1930: Kafka New Client API - Support for Topic Wildcards + * STORM-1924: Adding conf options for Persistent Word Count Topology + * STORM-1956: Disabling Backpressure by default + * STORM-1934: Fix race condition between sync-supervisor and sync-processes + * STORM-1919: Introduce FilterBolt on storm-redis + * STORM-1945: Fix NPE bugs on topology spout lag for storm-kafka-monitor + * STORM-1888: add description for shell command + * STORM-1902: add a simple & flexible FileNameFormat for storm-hdfs + * STORM-1914: Storm Kafka Field Topic Selector + * STORM-1907: PartitionedTridentSpoutExecutor has incompatible types that cause ClassCastException + * STORM-1925: Remove Nimbus thrift call from Nimbus itself + * STORM-1909: Update HDFS spout documentation + * STORM-1136: Command line module to return kafka spout offsets lag and display in storm ui + * STORM-1911: IClusterMetricsConsumer should use seconds to timestamp unit + * STORM-1893: Support OpenTSDB for storing timeseries data. + * STORM-1723: Introduce ClusterMetricsConsumer + * STORM-1700: Introduce 'whitelist' / 'blacklist' option to MetricsConsumer + * STORM-1698: Asynchronous MetricsConsumerBolt + * STORM-1705: Cap number of retries for a failed message + * STORM-1884: Prioritize pendingPrepare over pendingCommit + * STORM-1575: fix TwitterSampleSpout NPE on close + * STORM-1874: Update logger private permissions + * STORM-1865: update command line client document + * STORM-1771: HiveState should flushAndClose before closing old or idle Hive connections + * STORM-1882: Expose TextFileReader public + * STORM-1873: Implement alternative behaviour for late tuples + * STORM-1719: Introduce REST API: Topology metric stats for stream + * STORM-1887: Fixed help message for set_log_level command + * STORM-1878: Flux can now handle IStatefulBolts + * STORM-1864: StormSubmitter should throw respective exceptions and log respective errors forregistered submitter hook invocation + * STORM-1868: Modify TridentKafkaWordCount to run in distributed mode + * STORM-1859: Ack late tuples in windowed mode + * STORM-1851: Fix default nimbus impersonation authorizer config + * STORM-1848: Make KafkaMessageId and Partition serializable to support + * STORM-1862: Flux ShellSpout and ShellBolt can't emit to named streams + * Storm-1728: TransactionalTridentKafkaSpout error + * STORM-1850: State Checkpointing Documentation update + * STORM-1674: Idle KafkaSpout consumes more bandwidth than needed + * STORM-1842: Forward references in storm.thrift cause tooling issues + * STORM-1730: LocalCluster#shutdown() does not terminate all storm threads/thread pools. + * STORM-1709: Added group by support in storm sql standalone mode + * STORM-1720: Support GEO in storm-redis + +## 1.0.4 + * STORM-2450: Write resources into correct local director + * STORM-2038: No symlinks for local cluster + +## 1.0.3 + * STORM-2197: NimbusClient connectins leak due to leakage in ThriftClient + * STORM-2321: Handle blobstore zk key deletion in KeySequenceNumber + * STORM-2324: Fix deployment failure if resources directory is missing in topology jar + * STORM-2335: Fix broken Topology visualization with empty ':transferred' in executor stats + * STORM-2336: Close Localizer and AsyncLocalizer when supervisor is shutting down + * STORM-2338: Subprocess exception handling is broken in storm.py on Windows environment + * STORM-2337: Broken documentation generation for storm-metrics-profiling-internal-actions.md and windows-users-guide.md + * STORM-2325: Logviewer doesn't consider 'storm.local.hostname' + * STORM-1742: More accurate 'complete latency' + * STORM-2176: Workers do not shutdown cleanly and worker hooks don't run when a topology is killed + * STORM-2293: hostname should only refer node's 'storm.local.hostname' + * STORM-2246: Logviewer download link has urlencoding on part of the URL + * STORM-1906: Window count/length of zero should be disallowed + * STORM-1841: Address a few minor issues in windowing and doc + * STORM-2268: Fix integration test for Travis CI build + * STORM-2283: Fix DefaultStateHandler kryo multithreading issues + * STORM-2264: OpaqueTridentKafkaSpout failing after STORM-2216 + * STORM-2276: Remove twitter4j usages due to license issue (JSON.org is catalog X) + * STORM-2095: remove any remaining files when deleting blobstore directory + * STORM-2251: Integration test refers specific version of Storm which should be project version + * STORM-2234: heartBeatExecutorService in shellSpout don't work well with deactivate + * STORM-2216: Favor JSONValue.parseWithException + * STORM-2208: HDFS State Throws FileNotFoundException in Azure Data Lake Store file system (adl://) + * STORM-2213: ShellSpout has race condition when ShellSpout is being inactive longer than heartbeat timeout + * STORM-2210: remove array shuffle from ShuffleGrouping + * STORM-2198: perform RotationAction when stopping HdfsBolt + * STORM-2196: A typo in RAS_Node::consumeCPU + * STORM-2189: RAS_Node::freeCPU outputs incorrect info + * STORM-2184: Don't wakeup KafkaConsumer on shutdown + * STORM-2018: Supervisor V2 + * STORM-2145: Leave leader nimbus's hostname to log when trying to connect leader nimbus + * STORM-2127: Storm-eventhubs should use latest amqp and eventhubs-client versions + * STORM-2040: Fix bug on assert-can-serialize + * STORM-2017: ShellBolt stops reporting task ids + * STORM-2119: bug in log message printing to stdout + * STORM-2120: Emit to _spoutConfig.outputStreamId + * STORM-2101: fixes npe in compute-executors in nimbus + * STORM-2090: Add integration test for storm windowing + * STORM-2003: Make sure config contains TOPIC before get it + * STORM-1567: in defaults.yaml 'topology.disable.loadaware' should be 'topology.disable.loadaware.messaging' + * STORM-1987: Fix TridentKafkaWordCount arg handling in distributed mode. + * STORM-1969: Modify HiveTopology to show usage of non-partition table. + * STORM-1849: HDFSFileTopology should use the 3rd argument as topologyName + * STORM-2086: use DefaultTopicSelector instead of creating a new one + * STORM-2079: Unneccessary readStormConfig operation + * STORM-2081: create external directory for storm-sql various data sources and move storm-sql-kafka to it + * STORM-2054: DependencyResolver should be aware of relative path and absolute path + * STORM-1344: Remove sql command from storm-jdbc build + * STORM-2070: Fix sigar native binary download link + * STORM-2056: Bugs in logviewer + * STORM-1646: Fix ExponentialBackoffMsgRetryManager test + * STORM-2039: Backpressure refactoring in worker and executor + * STORM-2064: Add storm name and function, access result and function to log-thrift-access + * STORM-2063: Add thread name in worker logs + * STORM-2042: Nimbus client connections not closed properly causing connection leaks + * STORM-2032: removes warning in case more than one metrics tuple is received + * STORM-1594: org.apache.storm.tuple.Fields can throw NPE if given invalid field in selector + * STORM-1995: downloadChunk in nimbus.clj should close the input stream + +## 1.0.2 + * STORM-1976: Remove cleanup-corrupt-topologies! + * STORM-1977: Restore logic: give up leadership when elected as leader but doesn't have one or more topology codes on local + * STORM-1939: Frequent InterruptedException raised by ShellBoltMessageQueue.poll + * STORM-1928: ShellSpout should check heartbeat while ShellSpout is waiting for subprocess to sync + * STORM-1922: Supervisor summary default order by host + * STORM-1895: blobstore replication-factor argument + * STORM-118: Docs: typo in transactional-commit-flow.png + * STORM-1633: Document blobstore to command-line-client.md + * STORM-1899: Release HBase connection when topology shutdown + * STORM-1844: Some tests are flaky due to low timeout + * STORM-1946: initialize lastHeartbeatTimestamp before starting heartbeat task + * STORM-1941: Nimbus discovery can fail when zookeeper reconnect happens + * STORM-1937: Fix WindowTridentProcessor cause NullPointerException + * STORM-1924: Add a config file parameter to HDFS test topologies + * STORM-1861: Storm submit command returns exit code of 0 even when it fails. + * STORM-1755: Revert the kafka client version to 0.8.x in storm-kafka + * STORM-1745: Add partition to log output in PartitionManager + * STORM-1735: Nimbus should log that replication succeeded when min replicas was reached exactly + * STORM-1835: add lock info in thread dump + * STORM-1749: Fix storm-starter github links + * STORM-1764: Pacemaker is throwing some stack traces + * STORM-1761: Storm-Solr Example Throws ArrayIndexOutOfBoundsException in Remote Cluster Mode + * STORM-1756: Explicitly null KafkaServer reference in KafkaTestBroker to prevent out of memory on large test classes. + * STORM-1750: Ensure worker dies when report-error-and-die is called. + * STORM-1715: using Jedis Protocol.DEFAULT_HOST to replace DEFAULT_HOST + * STORM-1713: Replace NotImplementedException with UnsupportedOperationException + * STORM-1678: abstract batch processing to common api `BatchHelper` + * STORM-1773: Utils.javaDeserialize() doesn't work with primitive types + * STORM-1661: Introduce a config to turn off blobstore acl validation in insecure mode + +## 1.0.1 + * STORM-1853: Replace ClassLoaderObjectInputStream with ObjectInputStream + * STORM-1741: remove unconditional setting of JAVA_HOME from storm-env.sh + * STORM-1739: update the minor JAVA version dependency in 0.10.0 and above + * STORM-1727: document 1.0 package renaming and how to use the migration tool + * STORM-1733: Flush stdout and stderr before calling "os.execvp" to prevent log loss. + * STORM-1729: Get rid of reflections while recording stats + * STORM-1731: Avoid looking up debug / backpressure enable flags within critical path + * STORM-1535: Make sure hdfs key tab login happens only once for multiple bolts/executors. + * STORM-1725: Kafka Spout New Consumer API - KafkaSpoutRetryExponential Backoff method should use HashMap instead of TreeMap not to throw Exception + * STORM-1544: Document Debug/Sampling of Topologies + * STORM-1679: add storm Scheduler documents + * STORM-1704: When logviewer_search.html opens daemon file, next search always show no result + * STORM-1714: StatefulBolts ends up as normal bolts while using TopologyBuilder.setBolt without parallelism + * STORM-1683: only check non-system streams by default + * STORM-1680: Provide configuration to set min fetch size in KafkaSpout + * STORM-1649: Optimize Kryo instaces creation in HBaseWindowsStore + * STORM-1696: status not sync if zk fails in backpressure + * STORM-1693: Move stats cleanup to executor shutdown + * STORM-1585: Add DDL support for UDFs in storm-sql + * STORM-1681: Bug in scheduling cyclic topologies when scheduling with RAS + * STORM-1706: Add RELEASE and storm-env.sh to storm-diet assembly + * STORM-1613: Upgraded HBase version to 1.1.0 + * STORM-1687: divide by zero in stats + +## 1.0.0 + * STORM-1670: LocalState#get(String) can throw FileNotFoundException which may result supervisor.clj#sync-processes stop assigning new workers/assignments + * STORM-1677: Test resource files are excluded from source distribution, which makes logviewer-test failing + * STORM-676: Storm Trident support for sliding/tumbling windows + * STORM-1671: Enable logviewer to delete a dir without yaml + * STORM-822: Kafka Spout New Consumer API + * STORM-1673: log4j2/worker.xml refers old package of LoggerMetricsConsumer + * STORM-1632 Disable event logging by default + * STORM-1667: Log the IO exception when deleting worker pid dir + * STORM-1669: Fix SolrUpdateBolt flush bug + * STORM-1668: Flux silently fails while setting a non-existent property. + * STORM-1573: Add batch support for MongoInsertBolt + * STORM-1660: remove flux gitignore file and move rules to top level gitignore + * STORM-1622: Rename classes with older third party shaded packages + * STORM-1537: Upgrade to kryo 3 + * STORM-1556: nimbus.clj/wait-for-desired-code-replication wrong reset for current-replication-count-jar in local mode + * STORM-1636: Supervisor shutdown with worker id pass in being nil + * STORM-1641: make subtree node creation consistent + * STORM-1604: Delayed transition should handle NotALeaderException + * STORM-1602: Blobstore UTs are failed on Windows + * STORM-1629: Files/move doesn't work properly with non-empty directory in Windows + * STORM-1616: Add RAS API for Trident + * STORM-1483: add storm-mongodb connector + * STORM-1614: backpressure init and cleanup changes + * STORM-1549: Add support for resetting tuple timeout from bolts via the OutputCollector + * STORM-971: Metric for messages lost due to kafka retention + * STORM-1608: Fix stateful topology acking behavior + * STORM-1609: Netty Client is not best effort delivery on failed Connection + * STORM-1620: Update curator to fix CURATOR-209 + * STORM-1469: Decommission SimpleTransportPlugin and configuration + * STORM-1469: Adding Plain Sasl Transport Plugin + * STORM-1588: Do not add event logger details if number of event loggers is zero + * STORM-1606: print the information of testcase which is on failure + * STORM-1436: Set Travis Heap size to fit in memory limits in travis builds. + * STORM-1529: Change default worker temp directory location for workers + * STORM-1543: DRPCSpout should always try to reconnect disconnected DRPCInvocationsClient + * STORM-1561: Supervisor should relaunch worker if assignments have changed + * STORM-1601: Check if /backpressure/storm-id node exists before requesting children + * STORM-1574: Better handle backpressure exception etc. + * STORM-1587: Avoid NPE while prining Metrics + * STORM-1570: Storm SQL support for nested fields and array + * STORM-1576: fix ConcurrentModificationException in addCheckpointInputs + * STORM-1521: When using Kerberos login from keytab with multiple bolts/executors ticket is not renewed + * STORM-1488: UI Topology Page component last error timestamp is from 1970 + * STORM-1542: Remove profile action retry in case of non-zero exit code + * STORM-1540: Fix Debug/Sampling for Trident + * STORM-1569: Allowing users to specify the nimbus thrift server queue size. + * STORM-1552: Fix topology event sampling log dir + * STORM-1511: min/max operations support on a trident stream + * STORM-1522: REST API throws invalid worker log links + * STORM-1532: Fix readCommandLineOpts to parse JSON correctly + * STORM-1541: Change scope of 'hadoop-minicluster' to test + * STORM-1539: Improve Storm ACK-ing performance + * STORM-1519: Storm syslog logging not confirming to RFC5426 3.1 + * STORM-1533: IntegerValidator for metric consumer parallelism hint + * STORM-1534: Pick correct version of jackson-annotations jar + * STORM-1476: Filter -c options from args and add them as part of storm.options + * STORM-1520: Nimbus Clojure/Zookeeper issue ("stateChanged" method not found) + * STORM-1531: Junit and mockito dependencies need to have correct scope defined in storm-elasticsearch pom.xml + * STORM-1526: Improve Storm core performance + * STORM-1524: Add Pluggable daemon metrics Reporters + * STORM-1517: Add peek api in trident stream + * STORM-1455: kafka spout should not reset to the beginning of partition when offsetoutofrange exception occurs + * STORM-1518: Backport of STORM-1504 + * STORM-1505: Add map, flatMap and filter functions in trident stream + * STORM-1510: Fix broken nimbus log link + * STORM-1503: Worker should not crash on failure to send heartbeats to Pacemaker/ZK + * STORM-1176: Checkpoint window evaluated/expired state + * STORM-1494: Add link to supervisor log in Storm UI + * STORM-1496: Nimbus periodically throws blobstore-related exception + * STORM-1484: ignore subproject .classpath & .project file + * STORM-1478: make bolts getComponentConfiguration method cleaner/simpler + * STORM-1499: fix wrong package name for storm trident + * STORM-1463: added file scehma to log4j config files for windows env + * STORM-1485: DRPC Connectivity Issues + * STORM-1486: Fix storm-kafa documentation + * STORM-1214: add javadoc for Trident Streams and Operations + * STORM-1450: Fix minor bugs and refactor code in ResourceAwareScheduler + * STORM-1452: Fixes profiling/debugging out of the box + * STORM-1406: Add MQTT Support + * STORM-1473: enable log search for daemon logs + * STORM-1472: Fix the errorTime bug and show the time to be readable + * STORM-1466: Move the org.apache.thrift7 namespace to something correct/sensible + * STORM-1470: Applies shading to hadoop-auth, cleaner exclusions + * STORM-1467: Switch apache-rat plugin off by default, but enable for Travis-CI + * STORM-1468: move documentation to asf-site branch + * STORM-1199: HDFS Spout Implementation. + * STORM-1453: nimbus.clj/wait-for-desired-code-replication prints wrong log message + * STORM-1419: Solr bolt should handle tick tuples + * STORM-1175: State store for windowing operations + * STORM-1202: Migrate APIs to org.apache.storm, but try to provide some form of backwards compatability + * STORM-468: java.io.NotSerializableException should be explained + * STORM-1348: refactor API to remove Insert/Update builder in Cassandra connector + * STORM-1206: Reduce logviewer memory usage through directory stream + * STORM-1219: Fix HDFS and Hive bolt flush/acking + * STORM-1150: Fix the authorization of Logviewer in method authorized-log-user? + * STORM-1418: improve debug logs for some external modules + * STORM-1415: Some improvements for trident map StateUpdater + * STORM-1414: Some improvements for multilang JsonSerializer + * STORM-1408: clean up the build directory created by tests + * STORM-1425: Tick tuples should be acked like normal tuples + * STORM-1432: Spurious failure in storm-kafka test + * STORM-1449: Fix Kafka spout to maintain backward compatibility + * STORM-1458: Add check to see if nimbus is already running. + * STORM-1462: Upgrade HikariCP to 2.4.3 + * STORM-1457: Avoid collecting pending tuples if topology.debug is off + * STORM-1430: ui worker checkboxes + * STORM-1423: storm UI in a secure env shows error even when credentials are present + * STORM-702: Exhibitor support + * STORM-1160: Add hadoop-auth dependency needed for storm-core + * STORM-1404: Fix Mockito test failures in storm-kafka. + * STORM-1379: Removed Redundant Structure + * STORM-706: Clarify examples README for IntelliJ. + * STORM-1396: Added backward compatibility method for File Download + * STORM-695: storm CLI tool reports zero exit code on error scenario + * STORM-1416: Documentation for state store + * STORM-1426: keep backtype.storm.tuple.AddressedTuple and delete duplicated backtype.storm.messaging.AddressedTuple + * STORM-1417: fixed equals/hashCode contract in CoordType + * STORM-1422: broken example in storm-starter tutorial + * STORM-1429: LocalizerTest fix + * STORM-1401: removes multilang-test + * STORM-1424: Removed unused topology-path variable + * STORM-1427: add TupleUtils/listHashCode method and delete tuple.clj + * STORM-1413: remove unused variables for some tests + * STORM-1412: null check should be done in the first place + * STORM-1210: Set Output Stream id in KafkaSpout + * STORM-1397: Merge conflict from Pacemaker merge + * STORM-1373: Blobstore API sample example usage + * STORM-1409: StormClientErrorHandler is not used + * STORM-1411: Some fixes for storm-windowing + * STORM-1399: Blobstore tests should write data to `target` so it gets removed when running `mvn clean` + * STORM-1398: Add back in TopologyDetails.getTopology + * STORM-898: Add priorities and per user resource guarantees to Resource Aware Scheduler + * STORM-1187: Support windowing based on tuple ts. + * STORM-1400: Netty Context removeClient() called after term() causes NullPointerException. + * STORM-1383: Supervisors should not crash if nimbus is unavailable + * STORM-1381: Client side topology submission hook. + * STORM-1376: Performance slowdown due excessive zk connections and log-debugging + * STORM-1395: Move JUnit dependency to top-level pom + * STORM-1372: Merging design and usage documents for distcache + * STORM-1393: Update the storm.log.dir function, add doc for logs + * STORM-1377: nimbus_auth_test: very short timeouts causing spurious failures + * STORM-1388: Fix url and email links in README file + * STORM-1389: Removed creation of projection tuples as they are already available + * STORM-1179: Create Maven Profiles for Integration Tests. + * STORM-1387: workers-artifacts directory configurable, and default to be under storm.log.dir. + * STORM-1211: Add trident state and query support for cassandra connector + * STORM-1359: Change kryo links from google code to github + * STORM-1385: Divide by zero exception in stats.clj + * STORM-1370: Bug fixes for MultitenantScheduler + * STORM-1374: fix random failure on WindowManagerTest + * STORM-1040: SQL support for Storm. + * STORM-1364: Log storm version on daemon start + * STORM-1375: Blobstore broke Pacemaker + * STORM-876: Blobstore/DistCache Support + * STORM-1361: Apache License missing from two Cassandra files + * STORM-756: Handle taskids response as soon as possible + * STORM-1218: Use markdown for JavaDoc. + * STORM-1075: Storm Cassandra connector. + * STORM-965: excessive logging in storm when non-kerberos client tries to connect + * STORM-1341: Let topology have own heartbeat timeout for multilang subprocess + * STORM-1207: Added flux support for IWindowedBolt + * STORM-1352: Trident should support writing to multiple Kafka clusters. + * STORM-1220: Avoid double copying in the Kafka spout. + * STORM-1340: Use Travis-CI build matrix to improve test execution times + * STORM-1126: Allow a configMethod that takes no arguments (Flux) + * STORM-1203: worker metadata file creation doesn't use storm.log.dir config + * STORM-1349: [Flux] Allow constructorArgs to take Maps as arguments + * STORM-126: Add Lifecycle support API for worker nodes + * STORM-1213: Remove sigar binaries from source tree + * STORM-885: Heartbeat Server (Pacemaker) + * STORM-1221: Create a common interface for all Trident spout. + * STORM-1198: Web UI to show resource usages and Total Resources on all supervisors + * STORM-1167: Add windowing support for storm core. + * STORM-1215: Use Async Loggers to avoid locking and logging overhead + * STORM-1204: Logviewer should graceful report page-not-found instead of 500 for bad topo-id etc + * STORM-831: Add BugTracker and Central Logging URL to UI + * STORM-1208: UI: NPE seen when aggregating bolt streams stats + * STORM-1016: Generate trident bolt ids with sorted group names + * STORM-1190: System Load too high after recent changes + * STORM-1098: Nimbus hook for topology actions. + * STORM-1145: Have IConnection push tuples instead of pull them + * STORM-1191: bump timeout by 50% due to intermittent travis build failures + * STORM-794: Modify Spout async loop to treat activate/deactivate ASAP + * STORM-1196: Upgrade to thrift 0.9.3 + * STORM-1155: Supervisor recurring health checks + * STORM-1189: Maintain wire compatability with 0.10.x versions of storm. + * STORM-1185: replace nimbus.host with nimbus.seeds + * STORM-1164: Code cleanup for typos, warnings and conciseness. + * STORM-902: Simple Log Search. + * STORM-1052: TridentKafkaState uses new Kafka Producer API. + * STORM-1182: Removing and wrapping some exceptions in ConfigValidation to make code cleaner + * STORM-1134. Windows: Fix log4j config. + * STORM-1127: allow for boolean arguments (Flux) + * STORM-1138: Storm-hdfs README should be updated with Avro Bolt information + * STORM-1154: SequenceFileBolt needs unit tests + * STORM-162: Load Aware Shuffle Grouping + * STORM-1158: Storm metrics to profile various storm functions + * STORM-1161: Add License headers and add rat checks to builds + * STORM-1165: normalize the scales of CPU/Mem/Net when choosing the best node for Resource Aware Scheduler + * STORM-1163: use rmr rather than rmpath for remove worker-root + * STORM-1170: Fix the producer alive issue in DisruptorQueueTest + * STORM-1168: removes noisy log message & a TODO + * STORM-1143: Validate topology Configs during topology submission + * STORM-1157: Adding dynamic profiling for worker, restarting worker, jstack, heap dump, and profiling + * STORM-1123: TupleImpl - Unnecessary variable initialization. + * STORM-1153: Use static final instead of just static for class members. + * STORM-817: Kafka Wildcard Topic Support. + * STORM-40: Turn worker garbage collection and heapdump on by default. + * STORM-1152: Change map keySet iteration to entrySet iteration for efficiency. + * STORM-1147: Storm JDBCBolt should add validation to ensure either insertQuery or table name is specified and not both. + * STORM-1151: Batching in DisruptorQueue + * STORM-350: Update disruptor to latest version (3.3.2) + * STORM-697: Support for Emitting Kafka Message Offset and Partition + * STORM-1074: Add Avro HDFS bolt + * STORM-566: Improve documentation including incorrect Kryo ser. framework docs + * STORM-1073: Refactor AbstractHdfsBolt + * STORM-1128: Make metrics fast + * STORM-1122: Fix the format issue in Utils.java + * STORM-1111: Fix Validation for lots of different configs + * STORM-1125: Adding separate ZK client for read in Nimbus ZK State + * STORM-1121: Remove method call to avoid overhead during topology submission time + * STORM-1120: Fix keyword (schema -> scheme) from main-routes + * STORM-1115: Stale leader-lock key effectively bans all nodes from becoming leaders + * STORM-1119: Create access logging for all daemons + * STORM-1117: Adds visualization-init route previously missing + * STORM-1118: Added test to compare latency vs. throughput in storm. + * STORM-1110: Fix Component Page for system components + * STORM-1093: Launching Workers with resources specified in resource-aware schedulers + * STORM-1102: Add a default flush interval for HiveBolt + * STORM-1112: Add executor id to the thread name of the executor thread for debug + * STORM-1079: Batch Puts to HBase + * STORM-1084: Improve Storm config validation process to use java annotations instead of *_SCHEMA format + * STORM-1106: Netty should not limit attempts to reconnect + * STORM-1103: Changes log message to DEBUG from INFO + * STORM-1104: Nimbus HA fails to find newly downloaded code files + * STORM-1087: Avoid issues with transfer-queue backpressure. + * STORM-893: Resource Aware Scheduling (Experimental) + * STORM-1095: Tuple.getSourceGlobalStreamid() has wrong camel-case naming + * STORM-1091: Add unit test for tick tuples to HiveBolt and HdfsBolt + * STORM-1090: Nimbus HA should support `storm.local.hostname` + * STORM-820: Aggregate topo stats on nimbus, not ui + * STORM-412: Allow users to modify logging levels of running topologies + * STORM-1078: Updated RateTracker to be thread safe + * STORM-1082: fix nits for properties in kafka tests + * STORM-993: include uptimeSeconds as JSON integer field + * STORM-1053: Update storm-kafka README for new producer API confs. + * STORM-1058: create CLI kill_workers to kill workers on a supervisor node + * STORM-1063: support relative log4j conf dir for both daemons and workers + * STORM-1059: Upgrade Storm to use Clojure 1.7.0 + * STORM-1069: add check case for external change of 'now' value. + * STORM-969: HDFS Bolt can end up in an unrecoverable state. + * STORM-1068: Configure request.required.acks to be 1 in KafkaUtilsTest for sync + * STORM-1017: If ignoreZkOffsets set true,KafkaSpout will reset zk offset when recover from failure. + * STORM-1054: Excessive logging ShellBasedGroupsMapping if the user doesn't have any groups. + * STORM-954: Toplogy Event Inspector + * STORM-862: Pluggable System Metrics + * STORM-1032: Add generics to component configuration methods + * STORM-886: Automatic Back Pressure + * STORM-1037: do not remove storm-code in supervisor until kill job + * STORM-1007: Add more metrics to DisruptorQueue + * STORM-1011: HBaseBolt default mapper should handle null values + * STORM-1019: Added missing dependency version to use of org.codehaus.mojo:make-maven-plugin + * STORM-1020: Document exceptions in ITuple & Fields + * STORM-1025: Invalid links at https://storm.apache.org/about/multi-language.html + * STORM-1010: Each KafkaBolt could have a specified properties. + * STORM-1008: Isolate the code for metric collection and retrieval from DisruptorQueue + * STORM-991: General cleanup of the generics (storm.trident.spout package) + * STORM-1000: Use static member classes when permitted + * STORM-1003: In cluster.clj replace task-id with component-id in the declaration + * STORM-1013: [storm-elasticsearch] Expose TransportClient configuration Map to EsConfig + * STORM-1012: Shading jackson. + * STORM-974: [storm-elasticsearch] Introduces Tuple -> ES document mapper to get rid of constant field mapping (source, index, type) + * STORM-978: [storm-elasticsearch] Introduces Lookup(or Query)Bolt which emits matched documents from ES + * STORM-851: Storm Solr connector + * STORM-854: [Storm-Kafka] KafkaSpout can set the topic name as the output streamid + * STORM-990: Refactored TimeCacheMap to extend RotatingMap + * STORM-829: Hadoop dependency confusion + * STORM-166: Nimbus HA + * STORM-976: Config storm.logback.conf.dir is specific to previous logging framework + * STORM-995: Fix excessive logging + * STORM-837: HdfsState ignores commits + * STORM-938: storm-hive add a time interval to flush tuples to hive. + * STORM-964: Add config (with small default value) for logwriter to restrict its memory usage + * STORM-980: Re-include storm-kafka tests from Travis CI build + * STORM-960: HiveBolt should ack tuples only after flushing. + * STORM-951: Storm Hive connector leaking connections. + * STORM-806: use storm.zookeeper.connection.timeout in storm-kafka ZkState when newCurator + * STORM-809: topology.message.timeout.secs should not allow for null or <= 0 values + * STORM-847: Add cli to get the last storm error from the topology + * STORM-864: Exclude storm-kafka tests from Travis CI build + * STORM-477: Add warning for invalid JAVA_HOME + * STORM-826: Update KafkaBolt to use the new kafka producer API + * STORM-912: Support SSL on Logviewer + * STORM-934: The current doc for topology ackers is outdated + * STORM-160: Allow ShellBolt to set env vars (particularly PATH) + * STORM-937: Changing noisy log level from info to debug + * STORM-931: Python Scripts to Produce Formatted JIRA and GitHub Join + * STORM-924: Set the file mode of the files included when packaging release packages + * STORM-799: Use IErrorReport interface more broadly + * STORM-926: change pom to use maven-shade-plugin:2.2 + * STORM-942: Add FluxParser method parseInputStream() to eliminate disk usage + * STORM-67: Provide API for spouts to know how many pending messages there are + * STORM-918: Storm CLI could validate arguments/print usage + * STORM-859: Add regression test of STORM-856 + * STORM-913: Use Curator's delete().deletingChildrenIfNeeded() instead of zk/delete-recursive + * STORM-968: Adding support to generate the id based on names in Trident + * STORM-845: Storm ElasticSearch connector + * STORM-988: supervisor.slots.ports should not allow duplicate element values + * STORM-975: Storm-Kafka trident topology example + * STORM-958: Add config for init params of group mapping service + * STORM-949: On the topology summary UI page, last shown error should have the time and date + * STORM-1142: Some config validators for positive ints need to allow 0 + * STORM-901: Worker Artifacts Directory + * STORM-1144: Display requested and assigned cpu/mem resources for schedulers in UI + * STORM-1217: making small fixes in RAS + +## 0.10.3 + * STORM-2158: Fix OutOfMemoryError in Nimbus' SimpleTransportPlugin + +## 0.10.2 + * STORM-1834: Documentation How to Generate Certificates For Local Testing SSL Setup + * STORM-1754: Correct java version in 0.10.x storm-starter + * STORM-1750: Ensure worker dies when report-error-and-die is called. + * STORM-1739: update the minor JAVA version dependency in 0.10.0 and above + * STORM-1733: Flush stdout and stderr before calling "os.execvp" to prevent log loss + +## 0.10.1 + * STORM-584: Fix logging for LoggingMetricsConsumer metrics.log file + * STORM-1596: Do not use single Kerberos TGT instance between multiple threads + * STORM-1481: avoid Math.abs(Integer) get a negative value + * STORM-1121: Deprecate test only configuraton nimbus.reassign + * STORM-1180: FLUX logo wasn't appearing quite right + * STORM-1482: add missing 'break' for RedisStoreBolt ## 0.10.0 + * STORM-1096: Fix some issues with impersonation on the UI + * STORM-581: Add rebalance params to Storm REST API + * STORM-1108: Fix NPE in simulated time + * STORM-1099: Fix worker childopts as arraylist of strings + * STORM-1094: advance kafka offset when deserializer yields no object + * STORM-1066: Specify current directory when supervisor launches a worker + * STORM-1012: Shaded everything that was not already shaded + * STORM-967: Shaded everything that was not already shaded + * STORM-922: Shaded everything that was not already shaded + * STORM-1042: Shaded everything that was not already shaded + * STORM-1026: Adding external classpath elements does not work + * STORM-1055: storm-jdbc README needs fixes and context + * STORM-1044: Setting dop to zero does not raise an error + * STORM-1050: Topologies with same name run on one cluster + * STORM-1005: Supervisor do not get running workers after restart. + * STORM-803: Better CI logs + * STORM-1027: Use overflow buffer for emitting metrics + * STORM-1024: log4j changes leaving ${sys:storm.log.dir} under STORM_HOME dir + * STORM-944: storm-hive pom.xml has a dependency conflict with calcite + * STORM-994: Connection leak between nimbus and supervisors + * STORM-1001: Undefined STORM_EXT_CLASSPATH adds '::' to classpath of workers + * STORM-977: Incorrect signal (-9) when as-user is true + * STORM-843: [storm-redis] Add Javadoc to storm-redis + * STORM-866: Use storm.log.dir instead of storm.home in log4j2 config + * STORM-810: PartitionManager in storm-kafka should commit latest offset before close + * STORM-928: Add sources->streams->fields map to Multi-Lang Handshake + * STORM-945: element is not a policy,and should not be putted in the element. + * STORM-857: create logs metadata dir when running securely + * STORM-793: Made change to logviewer.clj in order to remove the invalid http 500 response + * STORM-139: hashCode does not work for byte[] + * STORM-860: UI: while topology is transitioned to killed, "Activate" button is enabled but not functioning + * STORM-966: ConfigValidation.DoubleValidator doesn't really validate whether the type of the object is a double + * STORM-742: Let ShellBolt treat all messages to update heartbeat + * STORM-992: A bug in the timer.clj might cause unexpected delay to schedule new event + +## 0.10.0-beta1 + * STORM-873: Flux does not handle diamond topologies + +## 0.10.0-beta + * STORM-867: fix bug with mk-ssl-connector * STORM-856: use serialized value of delay secs for topo actions * STORM-852: Replaced Apache Log4j Logger with SLF4J API * STORM-813: Change storm-starter's README so that it explains mvn exec:java cannot run multilang topology @@ -51,7 +752,6 @@ * STORM-735: [storm-redis] Upgrade Jedis to 2.7.0 * STORM-730: remove extra curly brace * STORM-729: Include Executors (Window Hint) if the component is of Bolt type - * STORM-728: Put emitted and transferred stats under correct columns * STORM-727: Storm tests should succeed even if a storm process is running locally. * STORM-724: Document RedisStoreBolt and RedisLookupBolt which is missed. * STORM-723: Remove RedisStateSetUpdater / RedisStateSetCountQuerier which didn't tested and have a bug @@ -162,12 +862,22 @@ * STORM-188: Allow user to specifiy full configuration path when running storm command * STORM-130: Supervisor getting killed due to java.io.FileNotFoundException: File '../stormconf.ser' does not exist. +## 0.9.6 + * STORM-996: netty-unit-tests/test-batch demonstrates out-of-order delivery + * STORM-1056: allow supervisor log filename to be configurable via ENV variable + * STORM-1051: Netty Client.java's flushMessages produces a NullPointerException + * STORM-763: nimbus reassigned worker A to another machine, but other worker's netty client can't connect to the new worker A + * STORM-935: Update Disruptor queue version to 2.10.4 + * STORM-503: Short disruptor queue wait time leads to high CPU usage when idle + * STORM-728: Put emitted and transferred stats under correct columns + * STORM-643: KafkaUtils repeatedly fetches messages whose offset is out of range + * STORM-933: NullPointerException during KafkaSpout deactivation ## 0.9.5 * STORM-790: Log "task is null" instead of let worker died when task is null in transfer-fn * STORM-796: Add support for "error" command in ShellSpout * STORM-745: fix storm.cmd to evaluate 'shift' correctly with 'storm jar' - * STORM-130: Supervisor getting killed due to java.io.FileNotFoundException: File '../stormconf.ser' does not exist. + * STORM-130: Supervisor getting killed due to java.io.FileNotFoundException: File '../stormconf.ser' does not exist. ## 0.9.4 * STORM-559: ZkHosts in README should use 2181 as port. diff --git a/DEVELOPER.md b/DEVELOPER.md index b3c4c9c2889..5fc1c361426 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -118,6 +118,25 @@ GitHub. 3. Storm committers will iterate with you on the design to make sure you are on the right track. 4. Implement your issue, create a pull request (see below), and iterate from there. +### Testing + +Unit tests and Integration tests are an essential part of code contributions. + +To mark a Java test as a Java integration test, add the annotation `@Category(IntegrationTest.class)` to the test class definition as well as to its hierarchy of superclasses. Java integration tests can be in the same package as Java unit tests. + +```java + @Category(IntegrationTest.class) + public class MyIntegrationTest { + ... + } +``` + +To mark a Clojure test as Clojure integration test, the test source must be located in a package with name prefixed by `integration.` + +For example, the test `test/clj/org.apache.storm.drpc_test.clj` is considered a clojure unit test, whereas + `test/clj/integration.org.apache.storm.drpc_test.clj` is considered a clojure integration test. + +Please refer to section Build the code and run the tests for how to run integration tests, and the info on the build phase each test runs. @@ -160,12 +179,9 @@ your fork up to date with the latest changes of the upstream (official) `storm` ### Approve a pull request -_NOTE: The information in this section may need to be formalized via proper project bylaws._ - -Pull requests are approved with two +1s from committers and need to be up for at least 72 hours for all committers to -have a chance to comment. In case it was a committer who sent the pull request than two _different_ committers must +1 -the request. +[BYLAWS](http://storm.apache.org/documentation/BYLAWS.html) describes the condition of approval for code / non-code change. +Please refer Approvals -> Actions section for more details. @@ -195,6 +211,7 @@ To pull in a merge request you should generally follow the command line instruct 3. Merge the pull request into your local test branch. $ git pull + You can use `./dev-tools/storm-merge.py ` to produce the above command most of the time. 4. Assuming that the pull request merges without any conflicts: Update the top-level `CHANGELOG.md`, and add in the JIRA ticket number (example: `STORM-1234`) and ticket @@ -225,19 +242,31 @@ To pull in a merge request you should generally follow the command line instruct # Build the code and run the tests -The following commands must be run from the top-level directory. +## Prerequisites +Firt of all you need to make sure you are using maven 3.2.5 or below. There is a bug in later versions of maven as linked to from https://issues.apache.org/jira/browse/MSHADE-206 that +cause shaded dependencies to not be packaged correctly. Also please be aware that because we are shading dependencies mvn dependency:tree will not always show the dependencies correctly. + +In order to build `storm` you need `python`, `ruby` and `nodejs`. In order to avoid an overful page we don't provide platform/OS specific installation instructions for those here. Please refer to you platform's/OS' documentation for support. + +The `ruby` package manager `rvm` and `nodejs` package manager `nvm` are for convenience and are used in the tests which run on [travis](https://travis-ci.org/apache/storm). They can be installed using `curl -L https://get.rvm.io | bash -s stable --autolibs=enabled && source ~/.profile` (see the [rvm installation instructions](https://github.com/rvm/rvm) for details) and `wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.26.1/install.sh | bash && source ~/.bashrc` (see the [nvm installation instructions](https://github.com/creationix/nvm) for details). + +With `rvm` and `nvm` installed you can run + +```sh +rvm use 2.1.5 --install +nvm install 0.12.2 +nvm use 0.12.2 +``` + +in order to get started as fast as possible. Users can still install a specific version of `ruby` and/or `node` manually. + +## Building - # Build the code and run the tests (requires nodejs, python and ruby installed) - # `mvn clean package` will fail because storm-core requires storm-maven-plugin. - # This plugin should be installed before compiling storm-core. - $ mvn clean install +The following commands must be run from the top-level directory. - # Build the code and run the tests, with specifying default test timeout (in millisecond) - $ export STORM_TEST_TIMEOUT_MS=10000 - $ mvn clean install +`mvn clean install` - # Build the code but skip the tests - $ mvn clean install -DskipTests=true +If you wish to skip the unit tests you can do this by adding `-DskipTests` to the command line. In case you modified `storm.thrift`, you have to regenerate thrift code as java and python code before compiling whole project. @@ -246,28 +275,46 @@ cd storm-core/src sh genthrift.sh ``` -You can also run tests selectively via the Clojure REPL. The following example runs the tests in -[auth_test.clj](storm-core/test/clj/backtype/storm/security/auth/auth_test.clj), which has the namespace -`backtype.storm.security.auth.auth-test`. - -First, start the REPL from within the relevant sub-project (here: `storm-core`): - - $ cd storm-core/ - $ mvn clojure:repl +## Testing -Now we run the tests in `auth_test.clj` in the REPL: +Tests are separated in two groups, Unit tests, and Integration tests. Java unit tests, Clojure unit tests, and Clojure integration tests (for reasons inherent to the clojure-maven-plugin) run in the maven `test` phase. Java integration tests run in the maven `integration-test` or `verify` phases. + +To run Clojure and Java unit tests but no integration tests execute the command + + mvn test + +Integration tests require that you activate the profile `integration-test` and that you specify the `maven-failsafe-plugin` in the module pom file. + +To run all Java and Clojure integration tests but no unit tests execute one of the commands + + mvn -P integration-tests-only verify + mvn -P integration-tests-only integration-test + +To run all unit tests plus Clojure integration tests but no Java integration tests execute the command + + mvn -P all-tests test + +To run all unit tests and all integration tests execute one of the commands + + mvn -P all-tests verify + mvn -P all-tests integration-test + + +You can also run tests selectively via the Clojure REPL. The following example runs the tests in +[auth_test.clj](storm-core/test/clj/org/apache/storm/security/auth/auth_test.clj), which has the namespace +`org.apache.storm.security.auth.auth-test`. -```clojure -;; You can use both absolute as well as relative paths to the .clj file. -(load-file "test/clj/backtype/storm/security/auth/auth_test.clj") -(ns backtype.storm.security.auth.auth-test) -(run-tests) -``` +You can also run tests selectively with `-Dtest=`. This works for both clojure and junit tests. > Tip: IDEs such as IntelliJ IDEA support a built-in Clojure REPL, which you can also use to run tests selectively. > Sometimes you may find that tests pass/fail depending on which REPL you use, which -- although frustrating -- > can be helpful to narrow down errors. +Unfortunately you might experience failures in clojure tests which are wrapped in the `maven-clojure-plugin` and thus doesn't provide too much useful output at first sight - you might end up with a maven test failure with an error message as unhelpful as `Clojure failed.`. In this case it's recommended to look into `target/test-reports` of the failed project to see what actual tests have failed or scroll through the maven output looking for obvious issues like missing binaries. + +By default integration tests are not run in the test phase. To run Java and Clojure integration tests you must enable the profile + + @@ -277,7 +324,7 @@ You can create a _distribution_ (like what you can download from Apache) as foll do not use the Maven release plugin because creating an official release is the task of our release manager. # First, build the code. - $ mvn clean install # you may skip tests with `-DskipTests=true` to save time + $ mvn clean install # you may skip tests with `-DskipTests=true` to save time # Create the binary distribution. $ cd storm-dist/binary && mvn package @@ -309,12 +356,12 @@ You can verify whether the digital signatures match their corresponding files: ## Testing -Tests should never rely on timing in order to pass. In Storm can properly test functionality that depends on time by +Tests should never rely on timing in order to pass. Storm can properly test functionality that depends on time by simulating time, which means we do not have to worry about e.g. random delays failing our tests indeterministically. If you are testing topologies that do not do full tuple acking, then you should be testing using the "tracked -topologies" utilities in `backtype.storm.testing.clj`. For example, -[test-acking](storm-core/test/clj/backtype/storm/integration_test.clj) (around line 213) tests the acking system in +topologies" utilities in `org.apache.storm.testing.clj`. For example, +[test-acking](storm-core/test/clj/org/apache/storm/integration_test.clj) (around line 213) tests the acking system in Storm using tracked topologies. Here, the key is the `tracked-wait` function: it will only return when both that many tuples have been emitted by the spouts _and_ the topology is idle (i.e. no tuples have been emitted nor will be emitted without further input). Note that you should not use tracked topologies for topologies that have tick tuples. @@ -365,6 +412,8 @@ The Storm JIRA is available at: If you do not have a JIRA account yet, then you can create one via the link above (registration is free). + +The storm codebase is available at [Codota](https://www.codota.com/xref/#/github_apache_storm_560da9ada8cb8703008bbfdc/findUsages) where you can semantically search it like in an IDE (e.g. find usages for a method). diff --git a/DISCLAIMER b/DISCLAIMER deleted file mode 100644 index 8638904d9db..00000000000 --- a/DISCLAIMER +++ /dev/null @@ -1,10 +0,0 @@ -Apache Storm is an effort undergoing incubation at the Apache Software -Foundation (ASF), sponsored by the Apache Incubator PMC. - -Incubation is required of all newly accepted projects until a further review -indicates that the infrastructure, communications, and decision making process -have stabilized in a manner consistent with other successful ASF projects. - -While incubation status is not necessarily a reflection of the completeness -or stability of the code, it does indicate that the project has yet to be -fully endorsed by the ASF. diff --git a/LICENSE b/LICENSE index 8755d1b62df..8569bf7e236 100644 --- a/LICENSE +++ b/LICENSE @@ -258,6 +258,18 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------- +For typeahead jquery 0.10.5 (storm-core/src/ui/public/js/typeahead.jquery.min.js) + +Copyright (c) 2013-2014 Twitter, Inc + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Allan Jardine nor SpryMedia UK may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------------------- + Copyright (c) 2008-2010, Allan Jardine All rights reserved. @@ -271,6 +283,106 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR IM ----------------------------------------------------------------------- +For js-yaml.min.js (storm-core/src/ui/public/js/) + +(The MIT License) + +Copyright (C) 2011-2015 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------- + +For dagre.min.js (storm-core/src/ui/public/js/) + +Copyright (c) 2012-2014 Chris Pettitt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------- + +For cytoscape.min.js and cytoscape-dagre.js (storm-core/src/ui/public/js/) + +Copyright (c) 2016 The Cytoscape Consortium + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------------------------------------------------------------------- + +For esprima.js (storm-core/src/ui/public/js/) + +Copyright JS Foundation and other contributors, https://js.foundation/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------------------- + For arbor.js and arbor-graphics.js (storm-core/src/ui/public/js/) Copyright (c) 2011 Samizdat Drafting Co. @@ -542,3 +654,36 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +----------------------------------------------------------------------- + +For statistic image + +storm-core/src/ui/public/images/statistic.png + +Copyright (c) 2015 Github, Inc. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +This product bundles PMML Sample Files, which are available under a +"3-clause BSD" license. For details, see http://dmg.org/documents/dmg-pmml-license-2016.pdf. diff --git a/README.markdown b/README.markdown index 1fd1f01dea7..3d6660384e1 100644 --- a/README.markdown +++ b/README.markdown @@ -1,14 +1,14 @@ Master Branch: [![Travis CI](https://travis-ci.org/apache/storm.svg?branch=master)](https://travis-ci.org/apache/storm) -Storm is a distributed realtime computation system. Similar to how Hadoop provides a set of general primitives for doing batch processing, Storm provides a set of general primitives for doing realtime computation. Storm is simple, can be used with any programming language, [is used by many companies](http://storm-project.net/documentation/Powered-By.html), and is a lot of fun to use! +Storm is a distributed realtime computation system. Similar to how Hadoop provides a set of general primitives for doing batch processing, Storm provides a set of general primitives for doing realtime computation. Storm is simple, can be used with any programming language, [is used by many companies](http://storm.apache.org/documentation/Powered-By.html), and is a lot of fun to use! -The [Rationale page](http://storm-project.net/documentation/Rationale.html) explains what Storm is and why it was built. [This presentation](http://vimeo.com/40972420) is also a good introduction to the project. +The [Rationale page](http://storm.apache.org/documentation/Rationale.html) explains what Storm is and why it was built. [This presentation](http://vimeo.com/40972420) is also a good introduction to the project. -Storm has a website at [storm-project.net](http://storm-project.net). Follow [@stormprocessor](https://twitter.com/stormprocessor) on Twitter for updates on the project. +Storm has a website at [storm.apache.org](http://storm.apache.org). Follow [@stormprocessor](https://twitter.com/stormprocessor) on Twitter for updates on the project. ## Documentation -Documentation and tutorials can be found on the [Storm website](http://storm-project.net/documentation/Home.html). +Documentation and tutorials can be found on the [Storm website](http://storm.apache.org/documentation/Home.html). Developers and contributors should also take a look at our [Developer documentation](DEVELOPER.md). @@ -32,9 +32,9 @@ You can subscribe to this list by sending an email to [dev-subscribe@storm.apach You can also [browse the archives of the storm-dev mailing list](http://mail-archives.apache.org/mod_mbox/storm-dev/). ### Which list should I send/subscribe to? -If you are using a pre-built binary distribution of Storm, then chances are you should send questions, comments, storm-related announcements, etc. to [user@storm.apache.org](user@storm.apache.org). +If you are using a pre-built binary distribution of Storm, then chances are you should send questions, comments, storm-related announcements, etc. to [user@storm.apache.org](mailto:user@storm.apache.org). -If you are building storm from source, developing new features, or otherwise hacking storm source code, then [dev@storm.apache.org](dev@storm.apache.org) is more appropriate. +If you are building storm from source, developing new features, or otherwise hacking storm source code, then [dev@storm.apache.org](mailto:dev@storm.apache.org) is more appropriate. ### What will happen with storm-user@googlegroups.com? All existing messages will remain archived there, and can be accessed/searched [here](https://groups.google.com/forum/#!forum/storm-user). @@ -84,6 +84,16 @@ under the License. * Sean Zhong ([@clockfly] (http://github.com/clockfly)) * Kyle Nusbaum ([@knusbaum](https://github.com/knusbaum)) * Parth Brahmbhatt ([@Parth-Brahmbhatt](https://github.com/Parth-Brahmbhatt)) +* Jungtaek Lim ([@HeartSaVioR](https://github.com/HeartSaVioR)) +* Aaron Dossett ([@dossett](https://github.com/dossett)) +* Matthias J. Sax ([@mjsax](https://github.com/mjsax)) +* Arun Mahadevan ([@arunmahadevan](https://github.com/arunmahadevan)) +* Boyang Jerry Peng ([@jerrypeng](https://github.com/jerrypeng)) +* Zhuo Liu ([@zhuoliu](https://github.com/zhuoliu)) +* Haohui Mai ([@haohui](https://github.com/haohui)) +* Longda Feng ([@longda](https://github.com/longdafeng)) +* Xin Wang ([@vesense](https://github.com/vesense)) +* Hugo da Cruz Louro ([@hmcl](https://github.com/hmcl)) ## Contributors @@ -154,7 +164,6 @@ under the License. * Parth-Brahmbhatt ([@Parth-Brahmbhatt](https://github.com/Parth-Brahmbhatt)) * Adrian Petrescu ([@apetresc](https://github.com/apetresc)) * DashengJu ([@dashengju](https://github.com/dashengju)) -* Jungtaek Lim ([@HeartSaVioR](https://github.com/HeartSaVioR)) * Li Jiahong ([@Gvain](https://github.com/Gvain)) * Aaron Levin ([@aaronlevin](https://github.com/aaronlevin)) * Masatake Iwasaki ([@iwasakims](https://github.com/iwasakims)) @@ -196,11 +205,56 @@ under the License. * iBuddha ([@iBuddha](https://github.com/iBuddha)) * Dave Katten ([@dkatten](https://github.com/dkatten)) * Mark Davis ([@markdav](https://github.com/markdav)) -* Zhuo Liu ([@zhuoliu](https://github.com/zhuoliu)) * jangie ([@jangie](https://github.com/jangie)) * Hailei Zhang ([@Hailei](https://github.com/Hailei)) +* Nikhil Singh ([@snikhil5](https://github.com/snikhil5)) +* Paul Poulosky ([@ppoulosk](https://github.com/ppoulosk)) +* Brendan W. Lyon ([@lyonbrw](https://github.com/lyonbrw)) +* Shyam Rajendran ([@bourneagain](https://github.com/bourneagain)) +* Reza Farivar ([@rfarivar](https://github.com/rfarivar)) +* Hugo Louro ([@hmcl](https://github.com/hmcl)) +* Charles Chan ([@charleswhchan](https://github.com/charleswhchan)) +* Chuanlei Ni ([@chuanlei](https://github.com/chuanlei)) +* Xingyu Su ([@errordaiwa](https://github.com/errordaiwa)) +* Adrian Seungjin Lee ([@sweetest](https://github.com/sweetest)) +* Randy Gelhausen ([@randerzander](https://github.com/randerzander)) +* Gabor Liptak ([@gliptak](https://github.com/glibtak)) +* Yvonne Ironberg ([@YvonneIronberg](https://github.com/YvonneIronberg)) +* Li Wang ([@wangli1426](https://github.com/wangli1426)) +* Rohan Agarwal ([@rohanag12](https://github.com/rohanag12)) +* Alex Panov ([@alexpanov](https://github.com/alexpanov)) +* Sanket Reddy ([@redsanket](https://github.com/redsanket)) +* Drew Robb ([@drewrobb](https://github.com/drewrobb)) +* Frantz Mazoyer ([@fmazoyer](https://github.com/fmazoyer)) +* Dean de Bree ([@ddebree](https://github.com/ddebree)) +* Renkai Ge ([@Renkai](https://github.com/Renkai)) +* Aaron Coburn ([@acoburn](https://github.com/acoburn)) +* Rick Kellogg ([@rmkellogg](https://github.com/rmkellogg)) +* Abhishek Agarwal ([@abhishekagarwal87](https://github.com/abhishekagarwal87)) +* chenyuzhao ([@danny0405](https://github.com/danny0405)) +* Michael Schonfeld ([@schonfeld](https://github.com/schonfeld)) +* Erik Weathers ([@erikdw](https://github.com/erikdw)) +* Bryan Shell ([@shellbj](https://github.com/shellbj)) +* Dmytro Dragan ([@3Dragan](https://github.com/3Dragan)) +* Ningyu An ([@jetan9](https://github.com/jetan9)) +* Pete Prokopowicz ([@prokopowicz](https://github.com/prokopowicz)) +* Priyank Shah ([@priyank5485](https://github.com/priyank5485)) +* Joshua Martell ([@jmartell7](https://github.com/jmartell7)) +* Matthew Tieman ([@mjtieman](https://github.com/mjtieman)) +* Chuck Burgess ([@ashnazg](https://github.com/ashnazg)) +* Tom Graves ([@tgravescs](https://github.com/tgravescs)) +* Dror Weiss ([@drorweiss](https://github.com/drorweiss)) +* Victor Wong ([@victor-wong](https://github.com/victor-wong)) +* David Wimsey ([@dwimsey](https://github.com/dwimsey)) +* Florian Hussonnois ([@fhussonnois](https://github.com/fhussonnois)) +* Ilya Ostrovskiy ([@iostat](https://github.com/iostat)) +* Satish Duggana ([@satishd](https://github.com/satishd)) +* Seth Ammons ([@sethgrid](https://github.com/sethgrid)) +* Aaron Dixon ([@atdixon](https://github.com/atdixon)) +* Roshan Naik([@roshannaik](https://github.com/roshannaik)) ## Acknowledgements YourKit is kindly supporting open source projects with its full-featured Java Profiler. YourKit, LLC is the creator of innovative and intelligent tools for profiling Java and .NET applications. Take a look at YourKit's leading software products: [YourKit Java Profiler](http://www.yourkit.com/java/profiler/index.jsp) and [YourKit .NET Profiler](http://www.yourkit.com/.net/profiler/index.jsp). + diff --git a/SECURITY.md b/SECURITY.md index c231547d742..5aa3bd0417e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,31 +1,32 @@ # Running Apache Storm Securely Apache Storm offers a range of configuration options when trying to secure -your cluster. By default all authentication and authorization is disabled but -can be turned on as needed. +your cluster. By default all authentication and authorization is disabled but +can be turned on as needed. Many of these features only became available in +Storm-0.10. ## Firewall/OS level Security You can still have a secure storm cluster without turning on formal -Authentication and Authorization. But to do so usually requires +Authentication and Authorization. But to do so usually requires configuring your Operating System to restrict the operations that can be done. This is generally a good idea even if you plan on running your cluster with Auth. -The exact detail of how to setup these precautions varies a lot and is beyond +The exact details of how to setup these precautions varies a lot and is beyond the scope of this document. It is generally a good idea to enable a firewall and restrict incoming network connections to only those originating from the cluster itself and from trusted -hosts and services, a complete list of ports storm uses are below. +hosts and services. Towards this end, a complete list of ports storm uses are below. -If the data your cluster is processing is sensitive it might be best to setup +If the data your cluster is processing is sensitive it might be best to set up IPsec to encrypt all traffic being sent between the hosts in the cluster. ### Ports | Default Port | Storm Config | Client Hosts/Processes | Server | |--------------|--------------|------------------------|--------| -| 2181 | `storm.zookeeper.port` | Nimbus, Supervisors, and Worker processes | Zookeeper | +| 2181 | `storm.zookeeper.port` | Nimbus, Supervisors, and Worker processes | ZooKeeper | | 6627 | `nimbus.thrift.port` | Storm clients, Supervisors, and UI | Nimbus | | 8080 | `ui.port` | Client Web Browsers | UI | | 8000 | `logviewer.port` | Client Web Browsers | Logviewer | @@ -34,6 +35,9 @@ IPsec to encrypt all traffic being sent between the hosts in the cluster. | 3774 | `drpc.http.port` | External HTTP DRPC Clients | DRPC | | 670{0,1,2,3} | `supervisor.slots.ports` | Worker Processes | Worker Processes | +Note that the Worker Processes ports above are just the default ones, the actual +ports for your setup may vary. + ### UI/Logviewer @@ -41,24 +45,24 @@ The UI and logviewer processes provide a way to not only see what a cluster is doing, but also manipulate running topologies. In general these processes should not be exposed except to users of the cluster. -Some form of Authentication is typically required, with using java servlet filters +Some form of Authentication is typically required; e.g., by using java servlet filters ```yaml ui.filter: "filter.class" ui.filter.params: "param1":"value1" ``` -or by restricting the UI/log viewers ports to only accept connections from local -hosts, and then front them with another web server, like Apache httpd, that can -authenticate/authorize incoming connections and -proxy the connection to the storm process. To make this work the ui process must have -logviewer.port set to the port of the proxy in its storm.yaml, while the logviewers -must have it set to the actual port that they are going to bind to. - -The servlet filters are preferred because it allows individual topologies to -specificy who is and who is not allowed to access the pages associated with -them. - -Storm UI can be configured to use AuthenticationFilter from hadoop-auth. +or by restricting the UI/log-viewers ports to only accept connections from localhost, +and then front them with another web server, like Apache httpd, that can +authenticate/authorize incoming connections and proxy the connection to the storm process. +To make this work the ui process must have logviewer.port set to the port of the proxy +in its `storm.yaml`, while the logviewers must have it set to the actual port that they +are going to bind to. + +The servlet filters are preferred because they allow individual topologies to +specify who is (and who is not) allowed to access the pages associated with +each topology. + +The Storm UI can be configured to use `AuthenticationFilter` from hadoop-auth. ```yaml ui.filter: "org.apache.hadoop.security.authentication.server.AuthenticationFilter" ui.filter.params: @@ -67,66 +71,93 @@ ui.filter.params: "kerberos.keytab": "/vagrant/keytabs/http.keytab" "kerberos.name.rules": "RULE:[2:$1@$0]([jt]t@.*EXAMPLE.COM)s/.*/$MAPRED_USER/ RULE:[2:$1@$0]([nd]n@.*EXAMPLE.COM)s/.*/$HDFS_USER/DEFAULT" ``` -make sure to create a principal 'HTTP/{hostname}' (here hostname should be the one where UI daemon runs +make sure to create a principal `HTTP/{hostname}` (here hostname should be the host where the UI daemon runs). + +Once configured, you must do `kinit` before accessing the UI. -Once configured users needs to do kinit before accessing UI. -Ex: +Here's an example of accessing Storm's API after the setup above: +```bash curl -i --negotiate -u:anyUser -b ~/cookiejar.txt -c ~/cookiejar.txt http://storm-ui-hostname:8080/api/v1/cluster/summary +``` -1. Firefox: Goto about:config and search for network.negotiate-auth.trusted-uris double-click to add value "http://storm-ui-hostname:8080" -2. Google-chrome: start from command line with: google-chrome --auth-server-whitelist="*storm-ui-hostname" --auth-negotiate-delegate-whitelist="*storm-ui-hostname" -3. IE: Configure trusted websites to include "storm-ui-hostname" and allow negotiation for that website +1. Firefox: Go to `about:config` and search for `network.negotiate-auth.trusted-uris` double-click to add value "http://storm-ui-hostname:8080" +2. Google-chrome: start from command line with: `google-chrome --auth-server-whitelist="*storm-ui-hostname" --auth-negotiate-delegate-whitelist="*storm-ui-hostname"` +3. IE: Configure trusted websites to include "storm-ui-hostname" and allow negotiation for that website -**Caution**: In AD MIT Keberos setup the key size is bigger than the default UI jetty server request header size. Make sure you set ui.header.buffer.bytes to 65536 in storm.yaml. More details are on [STORM-633](https://issues.apache.org/jira/browse/STORM-633) +**Caution**: In AD MIT Kerberos setup, the key size is bigger than the default UI jetty server request header size. So make sure you set `ui.header.buffer.bytes` to 65536 in `storm.yaml`. More details are in [STORM-633](https://issues.apache.org/jira/browse/STORM-633) -## UI / DRPC SSL +## UI / DRPC SSL -Both UI and DRPC allows users to configure ssl . +Both UI and DRPC allow users to configure ssl. ### UI -For UI users needs to set following config in storm.yaml. Generating keystores with proper keys and certs should be taken care by the user before this step. +For UI, set the following config in `storm.yaml`. Generating keystores with proper keys and certs should be taken care of by the user before this step. -1. ui.https.port -2. ui.https.keystore.type (example "jks") -3. ui.https.keystore.path (example "/etc/ssl/storm_keystore.jks") -4. ui.https.keystore.password (keystore password) -5. ui.https.key.password (private key password) +1. `ui.https.port` +2. `ui.https.keystore.type` (example "jks") +3. `ui.https.keystore.path` (example "/etc/ssl/storm_keystore.jks") +4. `ui.https.keystore.password` (keystore password) +5. `ui.https.key.password` (private key password) -optional config -6. ui.https.truststore.path (example "/etc/ssl/storm_truststore.jks") -7. ui.https.truststore.password (truststore password) -8. ui.https.truststore.type (example "jks") +Optional config: -If users want to setup 2-way auth -9. ui.https.want.client.auth (If this set to true server requests for client certifcate authentication, but keeps the connection if no authentication provided) -10. ui.https.need.client.auth (If this set to true server requires client to provide authentication) +1. `ui.https.truststore.path` (example "/etc/ssl/storm_truststore.jks") +2. `ui.https.truststore.password` (truststore password) +3. `ui.https.truststore.type` (example "jks") +To set up 2-way authentication: +1. `ui.https.want.client.auth` (If this set to true, server requests for client certificate authentication, but keeps the connection even if no authentication is provided) +2. `ui.https.need.client.auth` (If this set to true, server requires the client to provide authentication) ### DRPC -similarly to UI , users need to configure following for DRPC +Similarly to the UI configuration above, set the following config to configure SSL for DRPC: + +1. `drpc.https.port` +2. `drpc.https.keystore.type` (example "jks") +3. `drpc.https.keystore.path` (example "/etc/ssl/storm_keystore.jks") +4. `drpc.https.keystore.password` (keystore password) +5. `drpc.https.key.password` (private key password) + +Optional config: -1. drpc.https.port -2. drpc.https.keystore.type (example "jks") -3. drpc.https.keystore.path (example "/etc/ssl/storm_keystore.jks") -4. drpc.https.keystore.password (keystore password) -5. drpc.https.key.password (private key password) +1. `drpc.https.truststore.path` (example "/etc/ssl/storm_truststore.jks") +2. `drpc.https.truststore.password` (truststore password) +3. `drpc.https.truststore.type` (example "jks") + +To set up 2-way authentication: + +1. `drpc.https.want.client.auth` (If this set to true, server requests for client certificate authentication, but keeps the connection even if no authentication is provided) +2. `drpc.https.need.client.auth` (If this set to true, server requires the client to provide authentication) + +#### GENERATE CERTIFICATES FOR LOCAL TESTING SSL SETUP + +Run the following script and fill in the values and passwords when prompted. The `keyalg` must be set to `RSA` + +```bash +#!/bin/bash -optional config -6. drpc.https.truststore.path (example "/etc/ssl/storm_truststore.jks") -7. drpc.https.truststore.password (truststore password) -8. drpc.https.truststore.type (example "jks") +DIR=/Users/user/certs/dir/ -If users want to setup 2-way auth -9. drpc.https.want.client.auth (If this set to true server requests for client certifcate authentication, but keeps the connection if no authentication provided) -10. drpc.https.need.client.auth (If this set to true server requires client to provide authentication) +keytool -keystore $DIR/server.keystore.jks -alias localhost -validity 365 -keyalg RSA -genkey +openssl req -new -x509 -keyout $DIR/ca-key -out $DIR/ca-cert -days 365 +keytool -keystore $DIR/server.truststore.jks -alias CARoot -import -file $DIR/ca-cert +keytool -keystore $DIR/client.truststore.jks -alias CARoot -import -file $DIR/ca-cert +keytool -keystore $DIR/server.keystore.jks -alias localhost -certreq -file $DIR/cert-file + +openssl x509 -req -CA $DIR/ca-cert -CAkey $DIR/ca-key -in $DIR/cert-file -out $DIR/cert-signed -days 365 -CAcreateserial -passin pass:test12 + +keytool -keystore $DIR/server.keystore.jks -alias CARoot -import -file $DIR/ca-cert + +keytool -keystore $DIR/server.keystore.jks -alias localhost -import -file $DIR/cert-signed +``` ## Authentication (Kerberos) @@ -139,14 +170,13 @@ this document and it is assumed that you have done that already. ### Create Headless Principals and keytabs -Each Zookeeper Server, Nimbus, and DRPC server will need a service principal, which, by convention, includes the FQDN of the host it will run on. Be aware that the zookeeper user *MUST* be zookeeper. -The supervisors and UI also need a principal to run as, but because they are outgoing connections they do not need to be service principals. -The following is an example of how to setup kerberos principals, but the -details may vary depending on your KDC and OS. +Each ZooKeeper Server, Nimbus, and DRPC server will need a service principal, which, by convention, includes the FQDN of the host it will run on. Be aware that the ZooKeeper user *MUST* be `zookeeper`. +The supervisors and UI also need a principal to run as, but because they are outgoing connections they do not need to be service principals. +The following is an example of how to set up kerberos principals, but the details may vary depending on your KDC and OS. ```bash -# Zookeeper (Will need one of these for each box in teh Zk ensamble) +# ZooKeeper (Will need one of these for each box in the ZK ensemble) sudo kadmin.local -q 'addprinc zookeeper/zk1.example.com@STORM.EXAMPLE.COM' sudo kadmin.local -q "ktadd -k /tmp/zk.keytab zookeeper/zk1.example.com@STORM.EXAMPLE.COM" # Nimbus and DRPC @@ -157,25 +187,25 @@ sudo kadmin.local -q 'addprinc storm@STORM.EXAMPLE.COM' sudo kadmin.local -q "ktadd -k /tmp/storm.keytab storm@STORM.EXAMPLE.COM" ``` -be sure to distribute the keytab(s) to the appropriate boxes and set the FS permissions so that only the headless user running ZK, or storm has access to them. +be sure to distribute the keytab(s) to the appropriate boxes and set the FS permissions so that only the headless user running ZK, or storm, has access to them. #### Storm Kerberos Configuration -Both storm and Zookeeper use jaas configuration files to log the user in. +Both storm and ZooKeeper use jaas configuration files to log the user in. Each jaas file may have multiple sections for different interfaces being used. -To enable Kerberos authentication in storm you need to set the following storm.yaml configs +To enable Kerberos authentication in storm you need to set the following `storm.yaml` configs ```yaml -storm.thrift.transport: "backtype.storm.security.auth.kerberos.KerberosSaslTransportPlugin" +storm.thrift.transport: "org.apache.storm.security.auth.kerberos.KerberosSaslTransportPlugin" java.security.auth.login.config: "/path/to/jaas.conf" ``` -Nimbus and the supervisor processes will also connect to ZooKeeper(ZK) and we want to configure them to use Kerberos for authentication with ZK. To do this append +Nimbus and the supervisor processes will also connect to ZooKeeper (ZK) and we want to configure them to use Kerberos for authentication with ZK. To do this append ``` -Djava.security.auth.login.config=/path/to/jaas.conf ``` -to the childopts of nimbus, ui, and supervisor. Here is an example given the default childopts settings at the time of writing: +to the childopts of nimbus, ui, and supervisor. Here is an example given the default childopts settings at the time of this doc's writing: ```yaml nimbus.childopts: "-Xmx1024m -Djava.security.auth.login.config=/path/to/jaas.conf" @@ -184,10 +214,10 @@ supervisor.childopts: "-Xmx256m -Djava.security.auth.login.config=/path/to/jaas. ``` The jaas.conf file should look something like the following for the storm nodes. -The StormServer section is used by nimbus and the DRPC Nodes. It does not need to be included on supervisor nodes. -The StormClient section is used by all storm clients that want to talk to nimbus, including the ui, logviewer, and supervisor. We will use this section on the gateways as well but the structure of that will be a bit different. -The Client section is used by processes wanting to talk to zookeeper and really only needs to be included with nimbus and the supervisors. -The Server section is used by the zookeeper servers. +The StormServer section is used by nimbus and the DRPC nodes. It does not need to be included on supervisor nodes. +The StormClient section is used by all storm clients that want to talk to nimbus, including the ui, logviewer, and supervisor. We will use this section on the gateways as well, but the structure of that will be a bit different. +The Client section is used by processes wanting to talk to ZooKeeper and really only needs to be included with nimbus and the supervisors. +The Server section is used by the ZooKeeper servers. Having unused sections in the jaas is not a problem. ``` @@ -269,11 +299,11 @@ Server { Nimbus also will translate the principal into a local user name, so that other services can use this name. To configure this for Kerberos authentication set ``` -storm.principal.tolocal: "backtype.storm.security.auth.KerberosPrincipalToLocal" +storm.principal.tolocal: "org.apache.storm.security.auth.KerberosPrincipalToLocal" ``` This only needs to be done on nimbus, but it will not hurt on any node. -We also need to inform the topology who the supervisor daemon and the nimbus daemon are running as from a ZooKeeper perspective. +We also need to inform the topology who the supervisor daemon and the nimbus daemon are running as, from a ZooKeeper perspective. ``` storm.zookeeper.superACL: "sasl:${nimbus-user}" @@ -285,20 +315,20 @@ Here *nimbus-user* is the Kerberos user that nimbus uses to authenticate with Zo Complete details of how to setup a secure ZK are beyond the scope of this document. But in general you want to enable SASL authentication on each server, and optionally strip off host and realm -``` +```ini authProvider.1 = org.apache.zookeeper.server.auth.SASLAuthenticationProvider kerberos.removeHostFromPrincipal = true kerberos.removeRealmFromPrincipal = true ``` And you want to include the jaas.conf on the command line when launching the server so it can use it can find the keytab. -``` +```bash -Djava.security.auth.login.config=/jaas/zk_jaas.conf ``` #### Gateways -Ideally the end user will only need to run kinit before interacting with storm. To make this happen seamlessly we need the default jaas.conf on the gateways to be something like +Ideally the end user will only need to run `kinit` before interacting with storm. To make this happen seamlessly we need the default jaas.conf on the gateways to be something like ``` StormClient { @@ -318,44 +348,47 @@ The end user can override this if they have a headless user that has a keytab. The preferred authorization plug-in for nimbus is The *SimpleACLAuthorizer*. To use the *SimpleACLAuthorizer*, set the following: ```yaml -nimbus.authorizer: "backtype.storm.security.auth.authorizer.SimpleACLAuthorizer" +nimbus.authorizer: "org.apache.storm.security.auth.authorizer.SimpleACLAuthorizer" ``` DRPC has a separate authorizer configuration for it. Do not use SimpleACLAuthorizer for DRPC. -The *SimpleACLAuthorizer* plug-in needs to know who the supervisor users are, and it needs to know about all of the administrator users, including the user running the ui daemon. +The *SimpleACLAuthorizer* plug-in needs to know who the supervisor users are, and it needs to know about all of the administrator users, including the user running the ui daemon. These are set through *nimbus.supervisor.users* and *nimbus.admins* respectively. Each can either be a full Kerberos principal name, or the name of the user with host and realm stripped off. -The Log servers have their own authorization configurations. These are set through *logs.users* and *logs.groups*. These should be set to the admin users or groups for all of the nodes in the cluster. +The Log servers have their own authorization configurations. These are set through *logs.users* and *logs.groups*. These should be set to the admin users or groups for all of the nodes in the cluster. -When a topology is submitted, the submitting user can specify users in this list as well. The users and groups specified-in addition to the users in the cluster-wide setting-will be granted access to the submitted topology's worker logs in the logviewers. +When a topology is submitted, the submitting user can specify users in this list as well. The users and groups specified (in addition to the users in the cluster-wide setting) will be granted access to the submitted topology's worker logs in the logviewers. ### Supervisors headless User and group Setup -To ensure isolation of users in multi-tenancy, there is need to run supervisors and headless user and group unique to execution on the supervisor nodes. To enable this follow below steps. -1. Add headlessuser to all supervisor hosts. +To ensure isolation of users in multi-tenancy, the supervisors must run under a headless user and unique group: + +1. Add your chosen "headless user" to all supervisor hosts. 2. Create unique group and make it the primary group for the headless user on the supervisor nodes. -3. The set following properties on storm for these supervisor nodes. +3. Then set following properties on storm for these supervisor nodes. ### Multi-tenant Scheduler -To support multi-tenancy better we have written a new scheduler. To enable this scheduler set. +To support multi-tenancy better we have written a new scheduler. To enable this scheduler set: ```yaml -storm.scheduler: "backtype.storm.scheduler.multitenant.MultitenantScheduler" +storm.scheduler: "org.apache.storm.scheduler.multitenant.MultitenantScheduler" ``` -Be aware that many of the features of this scheduler rely on storm authentication. Without them the scheduler will not know what the user is and will not isolate topologies properly. +Be aware that many of the features of this scheduler rely on storm authentication. Without storm authentication, the scheduler will not know what the user is, and thus will not isolate topologies properly. + +The goal of the multi-tenant scheduler is to provide a way to isolate topologies from one another, but it also allows you to limit the total resources that an individual user can have in the cluster. -The goal of the multi-tenant scheduler is to provide a way to isolate topologies from one another, but to also limit the resources that an individual user can have in the cluster. +The scheduler config can be set either through `storm.yaml` or through a separate config file called `multitenant-scheduler.yaml` (which should be placed in the same directory as `storm.yaml`). Though it *is* preferable to use `multitenant-scheduler.yaml`, because it can be updated without needing to restart nimbus. -The scheduler currently has one config that can be set either through =storm.yaml= or through a separate config file called =multitenant-scheduler.yaml= that should be placed in the same directory as =storm.yaml=. It is preferable to use =multitenant-scheduler.yaml= because it can be updated without needing to restart nimbus. +There is currently only one config option: -There is currently only one config in =multitenant-scheduler.yaml=, =multitenant.scheduler.user.pools= is a map from the user name, to the maximum number of nodes that user is guaranteed to be able to use for their topologies. +* `multitenant.scheduler.user.pools`: a map from the user name to the maximum number of nodes that the user is guaranteed to be able to use for their topologies. For example: ```yaml -multitenant.scheduler.user.pools: +multitenant.scheduler.user.pools: "evans": 10 "derek": 10 ``` @@ -367,29 +400,27 @@ By default storm runs workers as the user that is running the supervisor. This supervisor.run.worker.as.user: true ``` -There are several files that go along with this that are needed to be configured properly to make storm secure. +There are several files that go along with this that need to be configured properly to make storm secure. -The worker-launcher executable is a special program that allows the supervisor to launch workers as different users. For this to work it needs to be owned by root, but with the group set to be a group that only teh supervisor headless user is a part of. -It also needs to have 6550 permissions. -There is also a worker-launcher.cfg file, usually located under /etc/ that should look something like the following +The `worker-launcher` executable is a special program that allows the supervisor to launch workers as different users. For this to work, `worker-launcher` needs to be owned by root, but with the group set to be a group that only the supervisor headless user is a part of. `worker-launcher` also needs to have `6550` octal permissions. There is also a `worker-launcher.cfg` file, usually located under /etc/, that should look something like the following: -``` +```ini storm.worker-launcher.group=$(worker_launcher_group) min.user.id=$(min_user_id) ``` -where worker_launcher_group is the same group the supervisor is a part of, and min.user.id is set to the first real user id on the system. -This config file also needs to be owned by root and not have world or group write permissions. +where `worker_launcher_group` is the same group the supervisor user is a part of, and `min.user.id` is set to the first real user id on the system. +This config file also needs to be owned by root and *not* have world nor group write permissions. ### Impersonating a user A storm client may submit requests on behalf of another user. For example, if a `userX` submits an oozie workflow and as part of workflow execution if user `oozie` wants to submit a topology on behalf of `userX` -it can do so by leveraging the impersonation feature.In order to submit topology as some other user , you can use `StormSubmitter.submitTopologyAs` API. Alternatively you can use `NimbusClient.getConfiguredClientAs` -to get a nimbus client as some other user and perform any nimbus action(i.e. kill/rebalance/activate/deactivate) using this client. +it can do so by leveraging the impersonation feature. In order to submit a topology as some other user, you can use the `StormSubmitter.submitTopologyAs` API. Alternatively you can use `NimbusClient.getConfiguredClientAs` +to get a nimbus client as some other user and perform any nimbus action (i.e., kill/rebalance/activate/deactivate) using this client. -To ensure only authorized users can perform impersonation you should start nimbus with `nimbus.impersonation.authorizer` set to `backtype.storm.security.auth.authorizer.ImpersonationAuthorizer`. +To ensure only authorized users can perform impersonation, you should start nimbus with `nimbus.impersonation.authorizer` set to `org.apache.storm.security.auth.authorizer.ImpersonationAuthorizer`. The `ImpersonationAuthorizer` uses `nimbus.impersonation.acl` as the acl to authorize users. Following is a sample nimbus config for supporting impersonation: ```yaml -nimbus.impersonation.authorizer: backtype.storm.security.auth.authorizer.ImpersonationAuthorizer +nimbus.impersonation.authorizer: org.apache.storm.security.auth.authorizer.ImpersonationAuthorizer nimbus.impersonation.acl: impersonating_user1: hosts: @@ -403,7 +434,7 @@ nimbus.impersonation.acl: [comma separated list of groups whose users impersonating_user2 is allowed to impersonate] ``` -To support the oozie use case following config can be supplied: +To support the oozie use-case, the following config can be supplied: ```yaml nimbus.impersonation.acl: oozie: @@ -415,56 +446,52 @@ nimbus.impersonation.acl: ### Automatic Credentials Push and Renewal Individual topologies have the ability to push credentials (tickets and tokens) to workers so that they can access secure services. Exposing this to all of the users can be a pain for them. -To hide this from them in the common case plugins can be used to populate the credentials, unpack them on the other side into a java Subject, and also allow Nimbus to renew the credentials if needed. -These are controlled by the following configs. topology.auto-credentials is a list of java plugins, all of which must implement IAutoCredentials interface, that populate the credentials on gateway -and unpack them on the worker side. On a kerberos secure cluster they should be set by default to point to backtype.storm.security.auth.kerberos.AutoTGT. -nimbus.credential.renewers.classes should also be set to this value so that nimbus can periodically renew the TGT on behalf of the user. +To hide this from them, in the common case plugins can be used to populate the credentials, unpack them on the other side into a java Subject, and also allow Nimbus to renew the credentials if needed. +These are controlled by the following configs: -nimbus.credential.renewers.freq.secs controls how often the renewer will poll to see if anything needs to be renewed, but the default should be fine. +* `topology.auto-credentials`: a list of java plugins, all of which must implement IAutoCredentials interface, that populate the credentials on gateway and unpack them on the worker side. On a kerberos secure cluster they should be set by default to point to `org.apache.storm.security.auth.kerberos.AutoTGT`. `nimbus.credential.renewers.classes` should also be set to this value so that nimbus can periodically renew the TGT on behalf of the user. +* `nimbus.credential.renewers.freq.secs`: controls how often the renewer will poll to see if anything needs to be renewed, but the default should be fine. -In addition Nimbus itself can be used to get credentials on behalf of the user submitting topologies. This can be configures using nimbus.autocredential.plugins.classes which is a list -of fully qualified class names ,all of which must implement INimbusCredentialPlugin. Nimbus will invoke the populateCredentials method of all the configured implementation as part of topology -submission. You should use this config with topology.auto-credentials and nimbus.credential.renewers.classes so the credentials can be populated on worker side and nimbus can automatically renew -them. Currently there are 2 examples of using this config, AutoHDFS and AutoHBase which auto populates hdfs and hbase delegation tokens for topology submitter so they don't have to distribute keytabs -on all possible worker hosts. +In addition Nimbus itself can be used to get credentials on behalf of the user submitting topologies. This can be configures using: +* `nimbus.autocredential.plugins.classes`: a list of fully qualified class names, all of which must implement `INimbusCredentialPlugin`. Nimbus will invoke the populateCredentials method of all the configured implementation as part of topology +submission. You should use this config with `topology.auto-credentials` and `nimbus.credential.renewers.classes` so the credentials can be populated on the worker side and nimbus can automatically renew them. Currently there are 2 examples of using this config: AutoHDFS and AutoHBase, which auto-populate hdfs and hbase delegation tokens for topology submitter so they don't have to distribute keytabs on all possible worker hosts. ### Limits -By default storm allows any sized topology to be submitted. But ZK and others have limitations on how big a topology can actually be. The following configs allow you to limit the maximum size a topology can be. +By default storm allows any sized topology to be submitted. But ZooKeeper and other components have limitations on how big a topology can actually be. The following configs allow you to limit the maximum size a topology can be. | YAML Setting | Description | |------------|----------------------| -| nimbus.slots.perTopology | The maximum number of slots/workers a topology can use. | -| nimbus.executors.perTopology | The maximum number of executors/threads a topology can use. | +| `nimbus.slots.perTopology` | The maximum number of slots/workers any topology can use. | +| `nimbus.executors.perTopology` | The maximum number of executors/threads any topology can use. | ### Log Cleanup The Logviewer daemon now is also responsible for cleaning up old log files for dead topologies. | YAML Setting | Description | |--------------|-------------------------------------| -| logviewer.cleanup.age.mins | How old (by last modification time) must a worker's log be before that log is considered for clean-up. (Living workers' logs are never cleaned up by the logviewer: Their logs are rolled via logback.) | -| logviewer.cleanup.interval.secs | Interval of time in seconds that the logviewer cleans up worker logs. | +| `logviewer.cleanup.age.mins` | How old (by last modification time) must a worker's log be before that log is considered ready for clean-up. (Living workers' logs are never cleaned up by the logviewer: their logs are rolled via some standard logging service (e.g. log4j2 in 0.11).) | +| `logviewer.cleanup.interval.secs` | Interval of time in seconds that the logviewer cleans up worker logs. | ### Allowing specific users or groups to access storm - With SimpleACLAuthorizer any user with valid kerberos ticket can deploy a topology or do further operations such as activate, deactivate , access cluster information. - One can restrict this access by specifying nimbus.users or nimbus.groups. If nimbus.users configured only the users in the list can deploy a topology or access cluster. - Similarly nimbus.groups restrict storm cluster access to users who belong to those groups. - - To configure specify the following config in storm.yaml +With SimpleACLAuthorizer any user with a valid kerberos ticket can deploy a topology or do further operations such as activate, deactivate, access cluster information, etc. +One can restrict this access by specifying `nimbus.users` or `nimbus.groups` in `storm.yaml`. If `nimbus.users` is configured then only the users in the list can deploy a topology or access the cluster. +Similarly `nimbus.groups` restrict storm cluster access to users who belong to those groups. + +E.g.: ```yaml -nimbus.users: +nimbus.users: - "testuser" ``` -or +or ```yaml -nimbus.groups: +nimbus.groups: - "storm" ``` - ### DRPC Hopefully more on this soon diff --git a/STORM-UI-REST-API.md b/STORM-UI-REST-API.md deleted file mode 100644 index 01595d25854..00000000000 --- a/STORM-UI-REST-API.md +++ /dev/null @@ -1,703 +0,0 @@ -# Storm UI REST API - -The Storm UI daemon provides a REST API that allows you to interact with a Storm cluster, which includes retrieving -metrics data and configuration information as well as management operations such as starting or stopping topologies. - - -# Data format - -The REST API returns JSON responses and supports JSONP. -Clients can pass a callback query parameter to wrap JSON in the callback function. - - -# Using the UI REST API - -_Note: It is recommended to ignore undocumented elements in the JSON response because future versions of Storm may not_ -_support those elements anymore._ - - -## REST API Base URL - -The REST API is part of the UI daemon of Storm (started by `storm ui`) and thus runs on the same host and port as the -Storm UI (the UI daemon is often run on the same host as the Nimbus daemon). The port is configured by `ui.port`, -which is set to `8080` by default (see [defaults.yaml](conf/defaults.yaml)). - -The API base URL would thus be: - - http://:/api/v1/... - -You can use a tool such as `curl` to talk to the REST API: - - # Request the cluster configuration. - # Note: We assume ui.port is configured to the default value of 8080. - $ curl http://:8080/api/v1/cluster/configuration - -##Impersonating a user in secure environment -In a secure environment an authenticated user can impersonate another user. To impersonate a user the caller must pass -`doAsUser` param or header with value set to the user that the request needs to be performed as. Please see SECURITY.MD -to learn more about how to setup impersonation ACLs and authorization. The rest API uses the same configs and acls that -are used by nimbus. - -Examples: - -```no-highlight - 1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1425844354\?doAsUser=testUSer1 - 2. curl 'http://localhost:8080/api/v1/topology/wordcount-1-1425844354/activate' -X POST -H 'doAsUser:testUSer1' -``` - -## GET Operations - -### /api/v1/cluster/configuration (GET) - -Returns the cluster configuration. - -Sample response (does not include all the data fields): - -```json - { - "dev.zookeeper.path": "/tmp/dev-storm-zookeeper", - "topology.tick.tuple.freq.secs": null, - "topology.builtin.metrics.bucket.size.secs": 60, - "topology.fall.back.on.java.serialization": true, - "topology.max.error.report.per.interval": 5, - "zmq.linger.millis": 5000, - "topology.skip.missing.kryo.registrations": false, - "storm.messaging.netty.client_worker_threads": 1, - "ui.childopts": "-Xmx768m", - "storm.zookeeper.session.timeout": 20000, - "nimbus.reassign": true, - "topology.trident.batch.emit.interval.millis": 500, - "storm.messaging.netty.flush.check.interval.ms": 10, - "nimbus.monitor.freq.secs": 10, - "logviewer.childopts": "-Xmx128m", - "java.library.path": "/usr/local/lib:/opt/local/lib:/usr/lib", - "topology.executor.send.buffer.size": 1024, - } -``` - -### /api/v1/cluster/summary (GET) - -Returns cluster summary information such as nimbus uptime or number of supervisors. - -Response fields: - -|Field |Value|Description -|--- |--- |--- -|stormVersion|String| Storm version| -|nimbusUptime|String| Shows how long the cluster is running| -|supervisors|Integer| Number of supervisors running| -|topologies| Integer| Number of topologies running| -|slotsTotal| Integer|Total number of available worker slots| -|slotsUsed| Integer| Number of worker slots used| -|slotsFree| Integer |Number of worker slots available| -|executorsTotal| Integer |Total number of executors| -|tasksTotal| Integer |Total tasks| - -Sample response: - -```json - { - "stormVersion": "0.9.2-incubating-SNAPSHOT", - "nimbusUptime": "3m 53s", - "supervisors": 1, - "slotsTotal": 4, - "slotsUsed": 3, - "slotsFree": 1, - "executorsTotal": 28, - "tasksTotal": 28 - } -``` - -### /api/v1/supervisor/summary (GET) - -Returns summary information for all supervisors. - -Response fields: - -|Field |Value|Description| -|--- |--- |--- -|id| String | Supervisor's id| -|host| String| Supervisor's host name| -|uptime| String| Shows how long the supervisor is running| -|slotsTotal| Integer| Total number of available worker slots for this supervisor| -|slotsUsed| Integer| Number of worker slots used on this supervisor| - -Sample response: - -```json -{ - "supervisors": [ - { - "id": "0b879808-2a26-442b-8f7d-23101e0c3696", - "host": "10.11.1.7", - "uptime": "5m 58s", - "slotsTotal": 4, - "slotsUsed": 3 - } - ] -} -``` - -### /api/v1/topology/summary (GET) - -Returns summary information for all topologies. - -Response fields: - -|Field |Value | Description| -|--- |--- |--- -|id| String| Topology Id| -|name| String| Topology Name| -|status| String| Topology Status| -|uptime| String| Shows how long the topology is running| -|tasksTotal| Integer |Total number of tasks for this topology| -|workersTotal| Integer |Number of workers used for this topology| -|executorsTotal| Integer |Number of executors used for this topology| - -Sample response: - -```json -{ - "topologies": [ - { - "id": "WordCount3-1-1402960825", - "name": "WordCount3", - "status": "ACTIVE", - "uptime": "6m 5s", - "tasksTotal": 28, - "workersTotal": 3, - "executorsTotal": 28 - } - ] -} -``` - -### /api/v1/topology/:id (GET) - -Returns topology information and statistics. Substitute id with topology id. - -Request parameters: - -|Parameter |Value |Description | -|----------|--------|-------------| -|id |String (required)| Topology Id | -|window |String. Default value :all-time| Window duration for metrics in seconds| -|sys |String. Values 1 or 0. Default value 0| Controls including sys stats part of the response| - - -Response fields: - -|Field |Value |Description| -|--- |--- |--- -|id| String| Topology Id| -|name| String |Topology Name| -|uptime| String |How long the topology has been running| -|status| String |Current status of the topology, e.g. "ACTIVE"| -|tasksTotal| Integer |Total number of tasks for this topology| -|workersTotal| Integer |Number of workers used for this topology| -|executorsTotal| Integer |Number of executors used for this topology| -|msgTimeout| Integer | Number of seconds a tuple has before the spout considers it failed | -|windowHint| String | window param value in "hh mm ss" format. Default value is "All Time"| -|topologyStats| Array | Array of all the topology related stats per time window| -|topologyStats.windowPretty| String |Duration passed in HH:MM:SS format| -|topologyStats.window| String |User requested time window for metrics| -|topologyStats.emitted| Long |Number of messages emitted in given window| -|topologyStats.trasferred| Long |Number messages transferred in given window| -|topologyStats.completeLatency| String (double value returned in String format) |Total latency for processing the message| -|topologyStats.acked| Long |Number of messages acked in given window| -|topologyStats.failed| Long |Number of messages failed in given window| -|spouts| Array | Array of all the spout components in the topology| -|spouts.spoutId| String |Spout id| -|spouts.executors| Integer |Number of executors for the spout| -|spouts.emitted| Long |Number of messages emitted in given window | -|spouts.completeLatency| String (double value returned in String format) |Total latency for processing the message| -|spouts.transferred| Long |Total number of messages transferred in given window| -|spouts.tasks| Integer |Total number of tasks for the spout| -|spouts.lastError| String |Shows the last error happened in a spout| -|spouts.errorLapsedSecs| Integer | Number of seconds elapsed since that last error happened in a spout| -|spouts.errorWorkerLogLink| String | Link to the worker log that reported the exception | -|spouts.acked| Long |Number of messages acked| -|spouts.failed| Long |Number of messages failed| -|bolts| Array | Array of bolt components in the topology| -|bolts.boltId| String |Bolt id| -|bolts.capacity| String (double value returned in String format) |This value indicates number of messages executed * average execute latency / time window| -|bolts.processLatency| String (double value returned in String format) |Average time of the bolt to ack a message after it was received| -|bolts.executeLatency| String (double value returned in String format) |Average time to run the execute method of the bolt| -|bolts.executors| Integer |Number of executor tasks in the bolt component| -|bolts.tasks| Integer |Number of instances of bolt| -|bolts.acked| Long |Number of tuples acked by the bolt| -|bolts.failed| Long |Number of tuples failed by the bolt| -|bolts.lastError| String |Shows the last error occurred in the bolt| -|bolts.errorLapsedSecs| Integer |Number of seconds elapsed since that last error happened in a bolt| -|bolts.errorWorkerLogLink| String | Link to the worker log that reported the exception | -|bolts.emitted| Long |Number of tuples emitted| - -Examples: - -```no-highlight - 1. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825 - 2. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825?sys=1 - 3. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825?window=600 -``` - -Sample response: - -```json - { - "name": "WordCount3", - "id": "WordCount3-1-1402960825", - "workersTotal": 3, - "window": "600", - "status": "ACTIVE", - "tasksTotal": 28, - "executorsTotal": 28, - "uptime": "29m 19s", - "msgTimeout": 30, - "windowHint": "10m 0s", - "topologyStats": [ - { - "windowPretty": "10m 0s", - "window": "600", - "emitted": 397960, - "transferred": 213380, - "completeLatency": "0.000", - "acked": 213460, - "failed": 0 - }, - { - "windowPretty": "3h 0m 0s", - "window": "10800", - "emitted": 1190260, - "transferred": 638260, - "completeLatency": "0.000", - "acked": 638280, - "failed": 0 - }, - { - "windowPretty": "1d 0h 0m 0s", - "window": "86400", - "emitted": 1190260, - "transferred": 638260, - "completeLatency": "0.000", - "acked": 638280, - "failed": 0 - }, - { - "windowPretty": "All time", - "window": ":all-time", - "emitted": 1190260, - "transferred": 638260, - "completeLatency": "0.000", - "acked": 638280, - "failed": 0 - } - ], - "spouts": [ - { - "executors": 5, - "emitted": 28880, - "completeLatency": "0.000", - "transferred": 28880, - "acked": 0, - "spoutId": "spout", - "tasks": 5, - "lastError": "", - "errorLapsedSecs": null, - "failed": 0 - } - ], - "bolts": [ - { - "executors": 12, - "emitted": 184580, - "transferred": 0, - "acked": 184640, - "executeLatency": "0.048", - "tasks": 12, - "executed": 184620, - "processLatency": "0.043", - "boltId": "count", - "lastError": "", - "errorLapsedSecs": null, - "capacity": "0.003", - "failed": 0 - }, - { - "executors": 8, - "emitted": 184500, - "transferred": 184500, - "acked": 28820, - "executeLatency": "0.024", - "tasks": 8, - "executed": 28780, - "processLatency": "2.112", - "boltId": "split", - "lastError": "", - "errorLapsedSecs": null, - "capacity": "0.000", - "failed": 0 - } - ], - "configuration": { - "storm.id": "WordCount3-1-1402960825", - "dev.zookeeper.path": "/tmp/dev-storm-zookeeper", - "topology.tick.tuple.freq.secs": null, - "topology.builtin.metrics.bucket.size.secs": 60, - "topology.fall.back.on.java.serialization": true, - "topology.max.error.report.per.interval": 5, - "zmq.linger.millis": 5000, - "topology.skip.missing.kryo.registrations": false, - "storm.messaging.netty.client_worker_threads": 1, - "ui.childopts": "-Xmx768m", - "storm.zookeeper.session.timeout": 20000, - "nimbus.reassign": true, - "topology.trident.batch.emit.interval.millis": 500, - "storm.messaging.netty.flush.check.interval.ms": 10, - "nimbus.monitor.freq.secs": 10, - "logviewer.childopts": "-Xmx128m", - "java.library.path": "/usr/local/lib:/opt/local/lib:/usr/lib", - "topology.executor.send.buffer.size": 1024, - "storm.local.dir": "storm-local", - "storm.messaging.netty.buffer_size": 5242880, - "supervisor.worker.start.timeout.secs": 120, - "topology.enable.message.timeouts": true, - "nimbus.cleanup.inbox.freq.secs": 600, - "nimbus.inbox.jar.expiration.secs": 3600, - "drpc.worker.threads": 64, - "topology.worker.shared.thread.pool.size": 4, - "nimbus.host": "hw10843.local", - "storm.messaging.netty.min_wait_ms": 100, - "storm.zookeeper.port": 2181, - "transactional.zookeeper.port": null, - "topology.executor.receive.buffer.size": 1024, - "transactional.zookeeper.servers": null, - "storm.zookeeper.root": "/storm", - "storm.zookeeper.retry.intervalceiling.millis": 30000, - "supervisor.enable": true, - "storm.messaging.netty.server_worker_threads": 1 - } -} -``` - - -### /api/v1/topology/:id/component/:component (GET) - -Returns detailed metrics and executor information - -|Parameter |Value |Description | -|----------|--------|-------------| -|id |String (required)| Topology Id | -|component |String (required)| Component Id | -|window |String. Default value :all-time| window duration for metrics in seconds| -|sys |String. Values 1 or 0. Default value 0| controls including sys stats part of the response| - -Response fields: - -|Field |Value |Description| -|--- |--- |--- -|id | String | Component id| -|name | String | Topology name| -|componentType | String | component type: SPOUT or BOLT| -|windowHint| String | window param value in "hh mm ss" format. Default value is "All Time"| -|executors| Integer |Number of executor tasks in the component| -|componentErrors| Array of Errors | List of component errors| -|componentErrors.time| Long | Timestamp when the exception occurred | -|componentErrors.errorHost| String | host name for the error| -|componentErrors.errorPort| String | port for the error| -|componentErrors.error| String |Shows the error happened in a component| -|componentErrors.errorLapsedSecs| Integer | Number of seconds elapsed since the error happened in a component | -|componentErrors.errorWorkerLogLink| String | Link to the worker log that reported the exception | -|topologyId| String | Topology id| -|tasks| Integer |Number of instances of component| -|window |String. Default value "All Time" | window duration for metrics in seconds| -|spoutSummary or boltStats| Array |Array of component stats. **Please note this element tag can be spoutSummary or boltStats depending on the componentType**| -|spoutSummary.windowPretty| String |Duration passed in HH:MM:SS format| -|spoutSummary.window| String | window duration for metrics in seconds| -|spoutSummary.emitted| Long |Number of messages emitted in given window | -|spoutSummary.completeLatency| String (double value returned in String format) |Total latency for processing the message| -|spoutSummary.transferred| Long |Total number of messages transferred in given window| -|spoutSummary.acked| Long |Number of messages acked| -|spoutSummary.failed| Long |Number of messages failed| -|boltStats.windowPretty| String |Duration passed in HH:MM:SS format| -|boltStats..window| String | window duration for metrics in seconds| -|boltStats.transferred| Long |Total number of messages transferred in given window| -|boltStats.processLatency| String (double value returned in String format) |Average time of the bolt to ack a message after it was received| -|boltStats.acked| Long |Number of messages acked| -|boltStats.failed| Long |Number of messages failed| - -Examples: - -```no-highlight -1. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/component/spout -2. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/component/spout?sys=1 -3. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/component/spout?window=600 -``` - -Sample response: - -```json -{ - "name": "WordCount3", - "id": "spout", - "componentType": "spout", - "windowHint": "10m 0s", - "executors": 5, - "componentErrors":[{"time": 1406006074000, - "errorHost": "10.11.1.70", - "errorPort": 6701, - "errorWorkerLogLink": "http://10.11.1.7:8000/log?file=worker-6701.log", - "errorLapsedSecs": 16, - "error": "java.lang.RuntimeException: java.lang.StringIndexOutOfBoundsException: Some Error\n\tat backtype.storm.utils.DisruptorQueue.consumeBatchToCursor(DisruptorQueue.java:128)\n\tat backtype.storm.utils.DisruptorQueue.consumeBatchWhenAvailable(DisruptorQueue.java:99)\n\tat backtype.storm.disruptor$consume_batch_when_available.invoke(disruptor.clj:80)\n\tat backtype...more.." - }], - "topologyId": "WordCount3-1-1402960825", - "tasks": 5, - "window": "600", - "spoutSummary": [ - { - "windowPretty": "10m 0s", - "window": "600", - "emitted": 28500, - "transferred": 28460, - "completeLatency": "0.000", - "acked": 0, - "failed": 0 - }, - { - "windowPretty": "3h 0m 0s", - "window": "10800", - "emitted": 127640, - "transferred": 127440, - "completeLatency": "0.000", - "acked": 0, - "failed": 0 - }, - { - "windowPretty": "1d 0h 0m 0s", - "window": "86400", - "emitted": 127640, - "transferred": 127440, - "completeLatency": "0.000", - "acked": 0, - "failed": 0 - }, - { - "windowPretty": "All time", - "window": ":all-time", - "emitted": 127640, - "transferred": 127440, - "completeLatency": "0.000", - "acked": 0, - "failed": 0 - } - ], - "outputStats": [ - { - "stream": "__metrics", - "emitted": 40, - "transferred": 0, - "completeLatency": "0", - "acked": 0, - "failed": 0 - }, - { - "stream": "default", - "emitted": 28460, - "transferred": 28460, - "completeLatency": "0", - "acked": 0, - "failed": 0 - } - ], - "executorStats": [ - { - "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6701.log", - "emitted": 5720, - "port": 6701, - "completeLatency": "0.000", - "transferred": 5720, - "host": "10.11.1.7", - "acked": 0, - "uptime": "43m 4s", - "id": "[24-24]", - "failed": 0 - }, - { - "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6703.log", - "emitted": 5700, - "port": 6703, - "completeLatency": "0.000", - "transferred": 5700, - "host": "10.11.1.7", - "acked": 0, - "uptime": "42m 57s", - "id": "[25-25]", - "failed": 0 - }, - { - "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6702.log", - "emitted": 5700, - "port": 6702, - "completeLatency": "0.000", - "transferred": 5680, - "host": "10.11.1.7", - "acked": 0, - "uptime": "42m 57s", - "id": "[26-26]", - "failed": 0 - }, - { - "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6701.log", - "emitted": 5700, - "port": 6701, - "completeLatency": "0.000", - "transferred": 5680, - "host": "10.11.1.7", - "acked": 0, - "uptime": "43m 4s", - "id": "[27-27]", - "failed": 0 - }, - { - "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6703.log", - "emitted": 5680, - "port": 6703, - "completeLatency": "0.000", - "transferred": 5680, - "host": "10.11.1.7", - "acked": 0, - "uptime": "42m 57s", - "id": "[28-28]", - "failed": 0 - } - ] -} -``` - -## POST Operations - - -### /api/v1/uploadTopology (POST) - -uploads a topology. - - -|Parameter |Value |Description | -|----------|--------|-------------| -|topologyConfig |String (required)| topology json config | -|topologyJar |String (required)| topology jar file | - -Sample topologyConfig json: -```json -{"topologyMainClass": "storm.starter.WordCountTopology", "topologyMainClassArgs": ["wordcount1"]} -``` - -Examples: - -```no-highlight -curl -i -b ~/cookiejar.txt -c ~/cookiejar.txt -X POST --F topologyConfig='{"topologyMainClass": "storm.starter.WordCountTopology", "topologyMainClassArgs": ["wordcount1"]}' --F topologyJar=@examples/storm-starter/storm-starter-topologies-0.10.0-SNAPSHOT.jar -http://localhost:8080/api/v1/uploadTopology -``` - -Sample Response: - -```json -{"status":"success"} -``` - -### /api/v1/topology/:id/activate (POST) - -Activates a topology. - -|Parameter |Value |Description | -|----------|--------|-------------| -|id |String (required)| Topology Id | - -Sample Response: - -```json -{"topologyOperation":"activate","topologyId":"wordcount-1-1420308665","status":"success"} -``` - - -### /api/v1/topology/:id/deactivate (POST) - -Deactivates a topology. - -|Parameter |Value |Description | -|----------|--------|-------------| -|id |String (required)| Topology Id | - -Sample Response: - -```json -{"topologyOperation":"deactivate","topologyId":"wordcount-1-1420308665","status":"success"} -``` - - -### /api/v1/topology/:id/rebalance/:wait-time (POST) - -Rebalances a topology. - -|Parameter |Value |Description | -|----------|--------|-------------| -|id |String (required)| Topology Id | -|wait-time |String (required)| Wait time before rebalance happens | -|rebalanceOptions| Json (optional) | topology rebalance options | - - -Sample rebalanceOptions json: - -```json -{"rebalanceOptions" : {"numWorkers" : 2, "executors" : {"spout" :4, "count" : 10}}, "callback" : "foo"} -``` - -Examples: - -```no-highlight -curl -i -b ~/cookiejar.txt -c ~/cookiejar.txt -X POST --H "Content-Type: application/json" --d '{"rebalanceOptions": {"numWorkers": 2, "executors": { "spout" : "5", "split": 7, "count": 5 }}, "callback":"foo"}' -http://localhost:8080/api/v1/topology/wordcount-1-1420308665/rebalance/0 -``` - -Sample Response: - -```json -{"topologyOperation":"rebalance","topologyId":"wordcount-1-1420308665","status":"success"} -``` - - - -### /api/v1/topology/:id/kill/:wait-time (POST) - -Kills a topology. - -|Parameter |Value |Description | -|----------|--------|-------------| -|id |String (required)| Topology Id | -|wait-time |String (required)| Wait time before rebalance happens | - -Caution: Small wait times (0-5 seconds) may increase the probability of triggering the bug reported in -[STORM-112](https://issues.apache.org/jira/browse/STORM-112), which may result in broker Supervisor -daemons. - -Sample Response: - -```json -{"topologyOperation":"kill","topologyId":"wordcount-1-1420308665","status":"success"} -``` - -## API errors - -The API returns 500 HTTP status codes in case of any errors. - -Sample response: - -```json -{ - "error": "Internal Server Error", - "errorMessage": "java.lang.NullPointerException\n\tat clojure.core$name.invoke(core.clj:1505)\n\tat backtype.storm.ui.core$component_page.invoke(core.clj:752)\n\tat backtype.storm.ui.core$fn__7766.invoke(core.clj:782)\n\tat compojure.core$make_route$fn__5755.invoke(core.clj:93)\n\tat compojure.core$if_route$fn__5743.invoke(core.clj:39)\n\tat compojure.core$if_method$fn__5736.invoke(core.clj:24)\n\tat compojure.core$routing$fn__5761.invoke(core.clj:106)\n\tat clojure.core$some.invoke(core.clj:2443)\n\tat compojure.core$routing.doInvoke(core.clj:106)\n\tat clojure.lang.RestFn.applyTo(RestFn.java:139)\n\tat clojure.core$apply.invoke(core.clj:619)\n\tat compojure.core$routes$fn__5765.invoke(core.clj:111)\n\tat ring.middleware.reload$wrap_reload$fn__6880.invoke(reload.clj:14)\n\tat backtype.storm.ui.core$catch_errors$fn__7800.invoke(core.clj:836)\n\tat ring.middleware.keyword_params$wrap_keyword_params$fn__6319.invoke(keyword_params.clj:27)\n\tat ring.middleware.nested_params$wrap_nested_params$fn__6358.invoke(nested_params.clj:65)\n\tat ring.middleware.params$wrap_params$fn__6291.invoke(params.clj:55)\n\tat ring.middleware.multipart_params$wrap_multipart_params$fn__6386.invoke(multipart_params.clj:103)\n\tat ring.middleware.flash$wrap_flash$fn__6675.invoke(flash.clj:14)\n\tat ring.middleware.session$wrap_session$fn__6664.invoke(session.clj:43)\n\tat ring.middleware.cookies$wrap_cookies$fn__6595.invoke(cookies.clj:160)\n\tat ring.adapter.jetty$proxy_handler$fn__6112.invoke(jetty.clj:16)\n\tat ring.adapter.jetty.proxy$org.mortbay.jetty.handler.AbstractHandler$0.handle(Unknown Source)\n\tat org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)\n\tat org.mortbay.jetty.Server.handle(Server.java:326)\n\tat org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)\n\tat org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:928)\n\tat org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:549)\n\tat org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:212)\n\tat org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)\n\tat org.mortbay.jetty.bio.SocketConnector$Connection.run(SocketConnector.java:228)\n\tat org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)\n" -} -``` diff --git a/TODO b/TODO deleted file mode 100644 index 23c88376666..00000000000 --- a/TODO +++ /dev/null @@ -1,178 +0,0 @@ -Use cases: - -1. number of steps between 2 people in a graph (topology with cycles?) - - -################# - -* Repackage jzmq and zmq as a leiningen "native dep" - - this might be good, since the native dep can package builds for all different systems/os's? - - -* Deploy design: - -- storm swap {name} {jar} {class} -- it's allowed to use resources equal to current running topology plus number of free resources -- starts in deactivated mode -- add TOPOLOGY_STARTUP_TIME config for the delay until nimbus activates a topology after launching it -- for swap, after the startup time, deactivate the other topology, wait the TOPOLOGY_MESSAGE_TIMEOUT_SECS, and then activate the other topology -- should be able to decrease the message timeout for killing or swapping (add optional thrift parameter) -- or just make it part of the config? -- add killWithOptions, swap, swapWithOptions - -* Storm UI, stats, debugging, diagnosis tools --- need to be able to hide system streams/components from the calculations (another query param and should be default) --- need to optimize (slowness is probably on nimbus end of querying zk, consider adding heartbeat caching into nimbus) --- add margins --- add titles so its easier to distinguish the various pages --- right align all table columns except for the leftmost - -* Unit test the core pieces that have stabilized their APIs - -- process simulator -- virtual ports -- supervisor -- utils -- test worker/tasks - -* implement pseudo-distributed mode -- this is for testing the distributed parts of the code - - perhaps i can use pallet/vmfest for this - -* Need integration tests that run on an actual storm cluster (scp code/process code/zookeeper code not tested in unit tests) - -* bolts with none grouping can be pushed into a bolt. e.g. A -> B -> C - A -> D -> E - -If A -> B and A -> D are shuffle grouping = none, and B -> C and D -> E are not, then both can be run in A, b's branch goes to C and D's branch goes to E - - -* Failure design - -Add fail method to outputcollector -Fail sends fail message to Acker for those anchors, which sends fail message back to spout. -Whenever spout fails a tuple, it emits it in its failure stream... - -Add fail method to drpc... Causes blocked thread to throw exception - -* Have worker heartbeat with its task ids, nimbus verifies - if wrong, reassign tasks? -- detect and ignore stray tasks -Each worker can choose a unique id for itself when heart beating -- nimbus deletes those that aren't in topology - -* Subscriptions design - --- new kind of spout: "subscription spout" - --> goal is to sync it's data across the tasks that subscribe to its streams - --> after doing a grouping, remembers what task it sent the tuple to (regardless of grouping). if a task dies, it knows its subscriptions and asks to be resynced - --> normal operation is to push to tasks, but pull done when a task starts up (b/c previous task died or something) - --> need to be able to add tuples to subscription or take tuples away (this is protocol with who you're subscribing to - e.g. rocket) - --> subscriptions can only happen in a spout because it requires persistent state - --> when subscription spout task dies, it polls the source (e.g. rocket) for all the subscription info - --> ideally you'd set things up to have one subscription spout per rocket server - --> TODO: Need some way to delete subscriptions -> part of tuple or extra metadata on tuple (extra metadata seems cleaner) - --> add isSubscription() method to Tuple as well as a getSubscriptionType() [which returns ADD or REMOVE] - --> when a spout starts up, it also needs to push all of its subscription info - --> acks are irrelevant for subscription tuples -- how should acks be managed as an abstraction? - -- maybe the synchronized state is done for you -- you just access the state directly and receive a callback whenever it changes? - -- so don't use tuples... - --> subscriptions break all the abstractions, perhaps I should generalize spouts and factor acking as a library on top of storm. subscriptions would just be another kind of library? -> no, it seems to break abstractions anyway (like keeping task -> tuples in memory) - --> maybe call it "syncspout" - --> if just do syncing (don't expose tuples directly?) - --> have a "SubscribedState" class that takes care of indexing/etc. --> expose it through topologycontext? - -- need a way to distinguish between states of different streams - -- has "add" and "remove" methods - -- bolt can give a statemanager object that implements add and remove in the prepare method - -- add(Tuple tuple) - -- remove(Tuple tuple) - --> synchronize protocol (when spout or source of data dies): - --> send how many tuples are going to be sent - --> send the tuples - --> OR: pack everything together into a single message (could be hard b/c where tuples are supposed to go is abstracted away) - --> tie everything together with a unique ID - --> once task receives everything, has info needed to remove tuples - --> statespout should do long-polling with timeout - --> to do subscriptions, the state should contain something like [url, subscriber]. some bolt appends subscriber to tuples, group by subscriber, and send info back - --> how to to fields grouping with an even distribution? - --> ********* tasks need to block on startup until they're synchronized ********* - --> send sync messages in a loop until it's synchronized - --> add a task.synchronize.poll.freq.secs config (default to 10 seconds) - --> need to buffer other messages as topology is waiting for synchronization messages (use disk?) - --> could use acking system to know if a piece of state gets fully synchronized and communicate this with user - --> perhaps expose this through a special stream? (the state status stream -> similar to failure streams) - --> should be able to do updates of existing state - --> use case: have a knob that you can set externally - --> this isn't really any better than just using zookeeper directly - - -_myState = context.setSubscribedState(_myState) - -StateSpout { - //does a timeout long poll and emits new add or remove state tuples (add and remove on the output collector) - nextTuple(StateSpoutOutputCollector) //collector has add and remove methods add(id, tuple). remove(id) - //emits all the tuples into the output collector (in the background, will also send ids and counts to tasks so they know how to synchronize) - //called on startup - //collector can have a synchronize method in case the source of data (e.g., rocket) craps out - synchronize(SynchronizationOutputCollector) //collector only has add(id, tuple) method -} - -//task startup (in prepare method) [this is automatic] -for(int taskId: statespoutids) { - emitDirect(SYNC_STREAM, tuple()) -} - -statespout synchronization(): - id = uuid() - //getAlLStateTuples calls synchronize on the spout to get the tuples - for(Tuple t: getAllStateTuplesFromSource()) { - List tasks = emit(cons(id, t)); - .. keep track of id -> tasks -> count - for(task: all output tasks) { - emitDirect(task, id, count) - } - } - -for synchronization to work, task needs to keep track of which tasks sent it tuples, and compare against only that set on synchronization - -Need a way to propogate information back up the topology - "subscriptions" -e.g. browser -> rocket -> bolt -> bolt -> bolt. - -example: #retweets for a subscribed set of tweet ids - -storm topology - - -> tweet spout (A) -> group on original id -> count (B) -> rocket - -subscriptions: rocket -> count (B) tweet id (need to group) -> spout (need to go to all) - --- how does it work when stuff dies downstream or upstream? do people ask what the subscriptions are? or do you push your subscriptions up? a combination? - --- maybe subscriptions are a "constant" spout? e..g, continuously emits and refreshes to make sure every task has the tuple. this seem amporphous and hard to implement... nimbus would need to refire all constant spouts whenever there's a reassignment that affects the flow of data. subscriptions seem more natural - --- subscriptions are a special kind of stream that are driven by being asked to send it. e..g, rocket is a spout that emits subscription/unsubscription tuples. they only send it when they get something new, or are asked as to what all the subscriptions are - --- maybe you just need a system stream to know when tasks are created. when you see that a downstream task is created, you know to fire subscriptions to it if its subscribed to your subscriptions stream? - how does this interplay with all the grouping types... you almost want to do a grouping and only send what to tasks that would have received. spouts would need to be able to subscribe to streams as well - -(use 'backtype.storm.testing) -;;(start-simulating-time!) -(def cluster (mk-local-storm-cluster)) -(use 'backtype.storm.bootstrap) (bootstrap) -(import '[backtype.storm.testing TestWordCounter TestWordSpout TestGlobalCount TestAggregatesCounter]) -(def spout (feeder-spout ["word"])) -(def topology (thrift/mk-topology - {1 (thrift/mk-spout-spec spout :parallelism-hint 3)} - {2 (thrift/mk-bolt-spec {1 ["word"]} (TestWordCounter.) :parallelism-hint 4) - 3 (thrift/mk-bolt-spec {1 :global} (TestGlobalCount.)) - 4 (thrift/mk-bolt-spec {2 :global} (TestAggregatesCounter.)) - })) -(submit-local-topology (:nimbus cluster) "test" {TOPOLOGY-WORKERS 4 TOPOLOGY-DEBUG true} topology) - - -* clean up project - - remove log4j dir and instead generate it in the deploy (it's only used in bin/storm -> create a console one and put into bin/) - - include system component / stream information in the topologycontext and clean up system specific code all over the place - -* Very rare errors - -weird nullptr exceptions: -(tasks i) on send-fn -no virtual port socket for outbound task (in worker) - diff --git a/bin/flight.bash b/bin/flight.bash new file mode 100755 index 00000000000..36b98ba7f7e --- /dev/null +++ b/bin/flight.bash @@ -0,0 +1,154 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +JDKPATH=$JAVA_HOME +BINPATH="/usr/bin" +USER=`whoami` + +#SETTINGS=/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jfr/profile.jfc +SETTINGS=profile + +platform='unknown' +unamestr=`uname` +if [[ "$unamestr" == 'Linux' ]]; then + platform='linux' +elif [[ "$unamestr" == 'Darwin' ]]; then + platform='darwin' +elif [[ "$unamestr" == 'FreeBSD' ]]; then + platform='freebsd' +fi + +if [[ $platform == 'linux' ]]; then + BINPATH="$JDKPATH/bin" +elif [[ $platform == 'darwin' ]]; then + BINPATH="/usr/bin" +fi + +function start_record { + # start_record pid + already_recording=false + for rid in `get_recording_ids $1`; do + already_recording=true + break; + done + if [ "$already_recording" = false ]; then + $BINPATH/jcmd $1 JFR.start settings=${SETTINGS} + fi +} + +function dump_record { + for rid in `get_recording_ids $1`; do + FILENAME=recording-$1-${rid}-${NOW}.jfr + $BINPATH/jcmd $1 JFR.dump recording=$rid filename="$2/${FILENAME}" + done +} + +function jstack_record { + FILENAME=jstack-$1-${NOW}.txt + $BINPATH/jstack $1 > "$2/${FILENAME}" 2>&1 +} + +function jmap_record { + FILENAME=recording-$1-${NOW}.bin + $BINPATH/jmap -dump:format=b,file="$2/${FILENAME}" $1 +} + +function stop_record { + for rid in `get_recording_ids $1`; do + FILENAME=recording-$1-${rid}-${NOW}.jfr + $BINPATH/jcmd $1 JFR.dump recording=$rid filename="$2/${FILENAME}" + $BINPATH/jcmd $1 JFR.stop recording=$rid + done +} + +function get_recording_ids { + $BINPATH/jcmd $1 JFR.check | perl -n -e '/recording=([0-9]+)/ && print "$1 "' +} + +function usage_and_quit { + echo "Usage: $0 pid start [profile_settings]" + echo " $0 pid dump target_dir" + echo " $0 pid stop target_dir" + echo " $0 pid jstack target_dir" + echo " $0 pid jmap target_dir" + echo " $0 pid kill" + exit -1 +} + +# Before using this script: make sure FlightRecorder is enabled + +if [ "$#" -le 1 ]; then + echo "Wrong number of arguments.." + usage_and_quit + +fi +# call this script with the process pid, example: "./flight PID start" or "./flight PID stop" +PID="$1" +CMD="$2" + +if /bin/ps -p $PID > /dev/null +then + if [[ $platform == 'linux' ]]; then + USER=`/bin/ps -ouser --noheader $PID` + elif [[ $platform == 'darwin' ]]; then + USER=`/bin/ps -ouser $PID` + fi +else + echo "No such pid running: $PID" + usage_and_quit +fi + +if [ "$CMD" != "start" ] && [ "$CMD" != "kill" ]; then + if [[ $3 ]] && [[ -d $3 ]] + then + TARGETDIR="$3" + mkdir -p ${TARGETDIR} + else + echo "Missing target directory" + usage_and_quit + fi +fi + +NOW=`date +'%Y%m%d-%H%M%S'` +if [ "$CMD" = "" ]; then + usage_and_quit +elif [ "$CMD" = "kill" ]; then + echo "Killing process with pid: $PID" + kill -9 ${PID} +elif [ "$CMD" = "start" ]; then + if [[ $3 ]] + then + SETTINGS=$3 + fi + start_record ${PID} +elif [ "$CMD" = "stop" ]; then + echo "Capturing dump before stopping in dir $TARGETDIR" + stop_record ${PID} ${TARGETDIR} +elif [ "$CMD" = "jstack" ]; then + echo "Capturing dump in dir $TARGETDIR" + jstack_record ${PID} ${TARGETDIR} +elif [ "$CMD" = "jmap" ]; then + echo "Capturing dump in dir $TARGETDIR" + jmap_record ${PID} ${TARGETDIR} +elif [ "$CMD" = "dump" ]; then + echo "Capturing dump in dir $TARGETDIR" + dump_record ${PID} ${TARGETDIR} +else + usage_and_quit +fi + + diff --git a/bin/storm b/bin/storm index 809a83a6eb8..0963065ede6 100755 --- a/bin/storm +++ b/bin/storm @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -52,18 +52,16 @@ STORM_BIN_DIR=`dirname ${PRG}` export STORM_BASE_DIR=`cd ${STORM_BIN_DIR}/..;pwd` #check to see if the conf dir is given as an optional argument -if [ $# -gt 1 ] -then - if [ "--config" = "$1" ] - then - conf_file=$2 - if [ ! -f "$conf_file" ]; then - echo "Error: Cannot find configuration directory: $conf_file" - exit 1 - fi - STORM_CONF_FILE=$conf_file - STORM_CONF_DIR=`dirname $conf_file` +if [ $# -gt 1 ]; then + if [ "--config" = "$1" ]; then + conf_file=$2 + if [ ! -f "$conf_file" ]; then + echo "Error: Cannot find configuration directory: $conf_file" + exit 1 fi + STORM_CONF_FILE=$conf_file + STORM_CONF_DIR=`dirname $conf_file` + fi fi export STORM_CONF_DIR="${STORM_CONF_DIR:-$STORM_BASE_DIR/conf}" diff --git a/bin/storm-config.cmd b/bin/storm-config.cmd index 67940237e7a..f5e677fd647 100644 --- a/bin/storm-config.cmd +++ b/bin/storm-config.cmd @@ -83,37 +83,51 @@ if not defined STORM_LOG_DIR ( ) @rem -@rem retrieve storm.logback.conf.dir from conf file +@rem retrieve storm.log4j2.conf.dir from conf file @rem -"%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" backtype.storm.command.config_value storm.logback.conf.dir > %CMD_TEMP_FILE% - +if not defined CMD_TEMP_FILE ( + set CMD_TEMP_FILE=tmpfile +) + +"%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" org.apache.storm.command.config_value storm.log4j2.conf.dir > %CMD_TEMP_FILE% + FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( FOR /F "tokens=1,* delims= " %%a in ("%%i") do ( if %%a == VALUE: ( - set STORM_LOGBACK_CONFIGURATION_DIR=%%b + set STORM_LOG4J2_CONFIGURATION_DIR=%%b del /F %CMD_TEMP_FILE%) ) ) -) +) + +@rem +@rem if we have a dir with relative path, make it absolute +@rem + +if not %STORM_LOG4J2_CONFIGURATION_DIR% == nil ( + if exist %STORM_HOME%\%STORM_LOG4J2_CONFIGURATION_DIR% ( + set STORM_LOG4J2_CONFIGURATION_DIR=%STORM_HOME%\%STORM_LOG4J2_CONFIGURATION_DIR% + ) +) @rem -@rem if STORM_LOGBACK_CONFIGURATION_DIR was defined, also set STORM_LOGBACK_CONFIGURATION_FILE +@rem if STORM_LOG4J2_CONFIGURATION_DIR was defined, also set STORM_LOG4J2_CONFIGURATION_FILE @rem -if not %STORM_LOGBACK_CONFIGURATION_DIR% == nil ( - set STORM_LOGBACK_CONFIGURATION_FILE=%STORM_LOGBACK_CONFIGURATION_DIR%\cluster.xml -) +if not %STORM_LOG4J2_CONFIGURATION_DIR% == nil ( + set STORM_LOG4J2_CONFIGURATION_FILE="file:///%STORM_LOG4J2_CONFIGURATION_DIR%\cluster.xml" +) @rem @rem otherwise, fall back to default @rem -if not defined STORM_LOGBACK_CONFIGURATION_FILE ( - set STORM_LOGBACK_CONFIGURATION_FILE=%STORM_HOME%\log4j2\cluster.xml +if not defined STORM_LOG4J2_CONFIGURATION_FILE ( + set STORM_LOG4J2_CONFIGURATION_FILE="file:///%STORM_HOME%\log4j2\cluster.xml" ) -"%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" backtype.storm.command.config_value java.library.path > %CMD_TEMP_FILE% +"%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" org.apache.storm.command.config_value java.library.path > %CMD_TEMP_FILE% FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( FOR /F "tokens=1,* delims= " %%a in ("%%i") do ( @@ -125,8 +139,12 @@ FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( :storm_opts - set STORM_OPTS=-Dstorm.options= -Dstorm.home=%STORM_HOME% -Djava.library.path=%JAVA_LIBRARY_PATH%;%JAVA_HOME%\bin;%JAVA_HOME%\lib;%JAVA_HOME%\jre\bin;%JAVA_HOME%\jre\lib - set STORM_OPTS=%STORM_OPTS% -Dlog4j.configurationFile=%STORM_LOGBACK_CONFIGURATION_FILE% + if "%set_storm_options%"=="true" ( + set STORM_OPTS=-Dstorm.options= + ) + + set STORM_OPTS=%STORM_OPTS% -Dstorm.home=%STORM_HOME% -Djava.library.path=%JAVA_LIBRARY_PATH%;%JAVA_HOME%\bin;%JAVA_HOME%\lib;%JAVA_HOME%\jre\bin;%JAVA_HOME%\jre\lib + set STORM_OPTS=%STORM_OPTS% -Dlog4j.configurationFile=%STORM_LOG4J2_CONFIGURATION_FILE% set STORM_OPTS=%STORM_OPTS% -Dstorm.log.dir=%STORM_LOG_DIR% del /F %CMD_TEMP_FILE% diff --git a/bin/storm-kafka-monitor b/bin/storm-kafka-monitor new file mode 100755 index 00000000000..a51052d2b04 --- /dev/null +++ b/bin/storm-kafka-monitor @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Resolve links - $0 may be a softlink +PRG="${0}" + +while [ -h "${PRG}" ]; do + ls=`ls -ld "${PRG}"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "${PRG}"`/"$link" + fi +done + +STORM_BIN_DIR=`dirname ${PRG}` +export STORM_BASE_DIR=`cd ${STORM_BIN_DIR}/..;pwd` +export STORM_CONF_DIR="${STORM_CONF_DIR:-$STORM_BASE_DIR/conf}" + +if [ -f "${STORM_CONF_DIR}/storm-env.sh" ]; then + . "${STORM_CONF_DIR}/storm-env.sh" +fi + +STORM_JAAS_CONF_PARAM="" +JAAS_FILE="${STORM_CONF_DIR}/storm_jaas.conf" +if [ -f $JAAS_FILE ]; then + STORM_JAAS_CONF_PARAM="-Djava.security.auth.login.config=${JAAS_FILE}" +fi +# Which java to use +if [ -z "$JAVA_HOME" ]; then + JAVA="java" +else + JAVA="$JAVA_HOME/bin/java" +fi +exec $JAVA $STORM_JAAS_CONF_PARAM -cp $STORM_BASE_DIR/toollib/storm-kafka-monitor-*.jar org.apache.storm.kafka.monitor.KafkaOffsetLagUtil "$@" \ No newline at end of file diff --git a/bin/storm.cmd b/bin/storm.cmd index ad1a81fc77f..23d65c6ddc2 100644 --- a/bin/storm.cmd +++ b/bin/storm.cmd @@ -37,16 +37,21 @@ :main setlocal enabledelayedexpansion + set storm-command=%1 + + if not "%storm-command%" == "jar" ( + set set_storm_options=true + ) + call %~dp0storm-config.cmd - set storm-command=%1 if not defined storm-command ( goto print_usage ) call :make_command_arguments %* - set shellcommands=classpath help version + set shellcommands=classpath help for %%i in ( %shellcommands% ) do ( if %storm-command% == %%i set shellcommand=true ) @@ -55,7 +60,7 @@ goto :eof ) - set corecommands=activate deactivate dev-zookeeper drpc kill list nimbus logviewer rebalance remoteconfvalue repl shell supervisor ui + set corecommands=activate deactivate dev-zookeeper drpc kill list nimbus logviewer rebalance remoteconfvalue repl shell supervisor ui version for %%i in ( %corecommands% ) do ( if %storm-command% == %%i set corecommand=true ) @@ -66,19 +71,45 @@ ) if %storm-command% == jar ( - set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% -Dstorm.jar=%2 - set CLASSPATH=%CLASSPATH%;%2 - set CLASS=%3 - set args=%4 + set config-options= + goto start :start shift - if [%4] == [] goto done - set args=%args% %4 + if [%1] == [] goto done + + if '%1'=='-c' ( + set c-opt=first + goto start + ) + + if "%c-opt%"=="first" ( + set config-options=%config-options%,%1 + set c-opt=second + goto start + ) + + if "%c-opt%"=="second" ( + set config-options=%config-options%=%~1 + set c-opt= + goto start + ) + + set args=%args% %1 goto start :done - set storm-command-arguments=%args% + for /F "tokens=1,2,*" %%a in ("%args%") do ( + set first-arg=%%a + set second-arg=%%b + set remaining-args=%%c + ) + set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% -Dstorm.jar=%first-arg% + set STORM_OPTS=%STORM_OPTS% -Dstorm.options=%config-options% + set CLASSPATH=%CLASSPATH%;%first-arg% + set CLASS=%second-arg% + set storm-command-arguments=%remaining-args% + ) if not defined STORM_LOG_FILE ( @@ -94,7 +125,7 @@ :activate - set CLASS=backtype.storm.command.activate + set CLASS=org.apache.storm.command.activate set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof @@ -103,18 +134,18 @@ goto :eof :deactivate - set CLASS=backtype.storm.command.deactivate + set CLASS=org.apache.storm.command.deactivate set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof :dev-zookeeper - set CLASS=backtype.storm.command.dev_zookeeper + set CLASS=org.apache.storm.command.dev_zookeeper set STORM_OPTS=%STORM_SERVER_OPTS% %STORM_OPTS% goto :eof :drpc - set CLASS=backtype.storm.daemon.drpc - "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" backtype.storm.command.config_value drpc.childopts > %CMD_TEMP_FILE% + set CLASS=org.apache.storm.daemon.drpc + "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" org.apache.storm.command.config_value drpc.childopts > %CMD_TEMP_FILE% FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( FOR /F "tokens=1,* delims= " %%a in ("%%i") do ( if %%a == VALUE: ( @@ -129,18 +160,18 @@ goto :eof :kill - set CLASS=backtype.storm.command.kill_topology + set CLASS=org.apache.storm.command.kill_topology set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof :list - set CLASS=backtype.storm.command.list + set CLASS=org.apache.storm.command.list set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof :logviewer - set CLASS=backtype.storm.daemon.logviewer - "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" backtype.storm.command.config_value logviewer.childopts > %CMD_TEMP_FILE% + set CLASS=org.apache.storm.daemon.logviewer + "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" org.apache.storm.command.config_value logviewer.childopts > %CMD_TEMP_FILE% FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( FOR /F "tokens=1,* delims= " %%a in ("%%i") do ( if %%a == VALUE: ( @@ -151,8 +182,8 @@ goto :eof :nimbus - set CLASS=backtype.storm.daemon.nimbus - "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" backtype.storm.command.config_value nimbus.childopts > %CMD_TEMP_FILE% + set CLASS=org.apache.storm.daemon.nimbus + "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" org.apache.storm.command.config_value nimbus.childopts > %CMD_TEMP_FILE% FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( FOR /F "tokens=1,* delims= " %%a in ("%%i") do ( if %%a == VALUE: ( @@ -163,12 +194,12 @@ goto :eof :rebalance - set CLASS=backtype.storm.command.rebalance + set CLASS=org.apache.storm.command.rebalance set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof :remoteconfvalue - set CLASS=backtype.storm.command.config_value + set CLASS=org.apache.storm.command.config_value set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof @@ -178,13 +209,13 @@ goto :eof :shell - set CLASS=backtype.storm.command.shell_submission + set CLASS=org.apache.storm.command.shell_submission set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof :supervisor - set CLASS=backtype.storm.daemon.supervisor - "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" backtype.storm.command.config_value supervisor.childopts > %CMD_TEMP_FILE% + set CLASS=org.apache.storm.daemon.supervisor.Supervisor + "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" org.apache.storm.command.config_value supervisor.childopts > %CMD_TEMP_FILE% FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( FOR /F "tokens=1,* delims= " %%a in ("%%i") do ( if %%a == VALUE: ( @@ -195,9 +226,9 @@ goto :eof :ui - set CLASS=backtype.storm.ui.core + set CLASS=org.apache.storm.ui.core set CLASSPATH=%CLASSPATH%;%STORM_HOME% - "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" backtype.storm.command.config_value ui.childopts > %CMD_TEMP_FILE% + "%JAVA%" -client -Dstorm.options= -Dstorm.conf.file= -cp "%CLASSPATH%" org.apache.storm.command.config_value ui.childopts > %CMD_TEMP_FILE% FOR /F "delims=" %%i in (%CMD_TEMP_FILE%) do ( FOR /F "tokens=1,* delims= " %%a in ("%%i") do ( if %%a == VALUE: ( @@ -208,7 +239,7 @@ goto :eof :version - set CLASS=backtype.storm.utils.VersionInfo + set CLASS=org.apache.storm.utils.VersionInfo set STORM_OPTS=%STORM_CLIENT_OPTS% %STORM_OPTS% goto :eof diff --git a/bin/storm.py b/bin/storm.py index ba893045c41..108e573534e 100755 --- a/bin/storm.py +++ b/bin/storm.py @@ -15,13 +15,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import print_function import os -import sys import random -import subprocess as sub import re import shlex +import tempfile +import uuid +import subprocess as sub +import json + +import sys + try: # python 3 from urllib.parse import quote_plus @@ -76,7 +82,8 @@ def init_storm_env(): STORM_LIB_DIR = os.path.join(STORM_DIR, "lib") STORM_BIN_DIR = os.path.join(STORM_DIR, "bin") -STORM_LOG4J_CONF_DIR = os.path.join(STORM_DIR, "log4j2") +STORM_LOG4J2_CONF_DIR = os.path.join(STORM_DIR, "log4j2") +STORM_SUPERVISOR_LOG_FILE = os.getenv('STORM_SUPERVISOR_LOG_FILE', "supervisor.log") init_storm_env() @@ -85,8 +92,14 @@ def init_storm_env(): JAR_JVM_OPTS = shlex.split(os.getenv('STORM_JAR_JVM_OPTS', '')) JAVA_HOME = os.getenv('JAVA_HOME', None) JAVA_CMD = 'java' if not JAVA_HOME else os.path.join(JAVA_HOME, 'bin', 'java') +if JAVA_HOME and not os.path.exists(JAVA_CMD): + print("ERROR: JAVA_HOME is invalid. Could not find bin/java at %s." % JAVA_HOME) + sys.exit(1) STORM_EXT_CLASSPATH = os.getenv('STORM_EXT_CLASSPATH', None) STORM_EXT_CLASSPATH_DAEMON = os.getenv('STORM_EXT_CLASSPATH_DAEMON', None) +DEP_JARS_OPTS = [] +DEP_ARTIFACTS_OPTS = [] +DEP_ARTIFACTS_REPOSITORIES_OPTS = [] def get_config_opts(): global CONFIG_OPTS @@ -95,12 +108,17 @@ def get_config_opts(): if not os.path.exists(STORM_LIB_DIR): print("******************************************") print("The storm client can only be run from within a release. You appear to be trying to run the client from a checkout of Storm's source code.") - print("\nYou can download a Storm release at http://storm-project.net/downloads.html") + print("\nYou can download a Storm release at http://storm.apache.org/downloads.html") print("******************************************") sys.exit(1) def get_jars_full(adir): - files = os.listdir(adir) + files = [] + if os.path.isdir(adir): + files = os.listdir(adir) + elif os.path.exists(adir): + files = [adir] + ret = [] for f in files: if f.endswith(".jar"): @@ -114,9 +132,11 @@ def get_classpath(extrajars, daemon=True): if daemon: ret.extend(get_jars_full(STORM_DIR + "/extlib-daemon")) if STORM_EXT_CLASSPATH != None: - ret.extend(STORM_EXT_CLASSPATH) + for path in STORM_EXT_CLASSPATH.split(os.pathsep): + ret.extend(get_jars_full(path)) if daemon and STORM_EXT_CLASSPATH_DAEMON != None: - ret.extend(STORM_EXT_CLASSPATH_DAEMON) + for path in STORM_EXT_CLASSPATH_DAEMON.split(os.pathsep): + ret.extend(get_jars_full(path)) ret.extend(extrajars) return normclasspath(os.pathsep.join(ret)) @@ -124,7 +144,7 @@ def confvalue(name, extrapaths, daemon=True): global CONFFILE command = [ JAVA_CMD, "-client", get_config_opts(), "-Dstorm.conf.file=" + CONFFILE, - "-cp", get_classpath(extrapaths, daemon), "backtype.storm.command.config_value", name + "-cp", get_classpath(extrapaths, daemon), "org.apache.storm.command.config_value", name ] p = sub.Popen(command, stdout=sub.PIPE) output, errors = p.communicate() @@ -138,6 +158,42 @@ def confvalue(name, extrapaths, daemon=True): return " ".join(tokens[1:]) return "" +def resolve_dependencies(artifacts, artifact_repositories): + if len(artifacts) == 0: + return {} + + print("Resolving dependencies on demand: artifacts (%s) with repositories (%s)" % (artifacts, artifact_repositories)) + sys.stdout.flush() + + # TODO: should we move some external modules to outer place? + + # storm-submit module doesn't rely on storm-core and relevant libs + extrajars = get_jars_full(STORM_DIR + "/external/storm-submit-tools") + classpath = normclasspath(os.pathsep.join(extrajars)) + + command = [ + JAVA_CMD, "-client", "-cp", classpath, "org.apache.storm.submit.command.DependencyResolverMain", + ",".join(artifacts), ",".join(artifact_repositories) + ] + + p = sub.Popen(command, stdout=sub.PIPE) + output, errors = p.communicate() + if p.returncode != 0: + raise RuntimeError("dependency handler returns non-zero code: code<%s> syserr<%s>" % (p.returncode, errors)) + + # python 3 + if not isinstance(output, str): + output = output.decode('utf-8') + + # For debug purpose, uncomment when you need to debug DependencyResolver + #print("Resolved dependencies: %s" % output) + + try: + out_dict = json.loads(output) + return out_dict + except: + raise RuntimeError("dependency handler returns non-json response: sysout<%s>", output) + def print_localconfvalue(name): """Syntax: [storm localconfvalue conf-name] @@ -159,7 +215,7 @@ def print_remoteconfvalue(name): print(name + ": " + confvalue(name, [CLUSTER_CONF_DIR])) def parse_args(string): - r"""Takes a string of whitespace-separated tokens and parses it into a list. + """Takes a string of whitespace-separated tokens and parses it into a list. Whitespace inside tokens may be quoted with single quotes, double quotes or backslash (similar to command-line arguments in bash). @@ -193,13 +249,20 @@ def exec_storm_class(klass, jvmtype="-server", jvmopts=[], extrajars=[], args=[] "-cp", get_classpath(extrajars, daemon), ] + jvmopts + [klass] + list(args) print("Running: " + " ".join(all_args)) + sys.stdout.flush() + exit_code = 0 if fork: - os.spawnvp(os.P_WAIT, JAVA_CMD, all_args) + exit_code = os.spawnvp(os.P_WAIT, JAVA_CMD, all_args) elif is_windows(): # handling whitespaces in JAVA_CMD - sub.call(all_args) + try: + ret = sub.check_output(all_args, stderr=sub.STDOUT) + print(ret) + except sub.CalledProcessError as e: + sys.exit(e.returncode) else: os.execvp(JAVA_CMD, all_args) + return exit_code def jar(jarfile, klass, *args): """Syntax: [storm jar topology-jar-path class ...] @@ -207,16 +270,103 @@ def jar(jarfile, klass, *args): Runs the main method of class with the specified arguments. The storm jars and configs in ~/.storm are put on the classpath. The process is configured so that StormSubmitter - (http://storm.incubator.apache.org/apidocs/backtype/storm/StormSubmitter.html) + (http://storm.apache.org/releases/current/javadocs/org/apache/storm/StormSubmitter.html) will upload the jar at topology-jar-path when the topology is submitted. + + When you want to ship other jars which is not included to application jar, you can pass them to --jars option with comma-separated string. + For example, --jars "your-local-jar.jar,your-local-jar2.jar" will load your-local-jar.jar and your-local-jar2.jar. + And when you want to ship maven artifacts and its transitive dependencies, you can pass them to --artifacts with comma-separated string. + You can also exclude some dependencies like what you're doing in maven pom. + Please add exclusion artifacts with '^' separated string after the artifact. + For example, --artifacts "redis.clients:jedis:2.9.0,org.apache.kafka:kafka_2.10:0.8.2.2^org.slf4j:slf4j-log4j12" will load jedis and kafka artifact and all of transitive dependencies but exclude slf4j-log4j12 from kafka. + + When you need to pull the artifacts from other than Maven Central, you can pass remote repositories to --artifactRepositories option with comma-separated string. + Repository format is "^". '^' is taken as separator because URL allows various characters. + For example, --artifactRepositories "jboss-repository^http://repository.jboss.com/maven2,HDPRepo^http://repo.hortonworks.com/content/groups/public/" will add JBoss and HDP repositories for dependency resolver. + + Complete example of options is here: `./bin/storm jar example/storm-starter/storm-starter-topologies-*.jar org.apache.storm.starter.RollingTopWords blobstore-remote2 remote --jars "./external/storm-redis/storm-redis-1.1.0.jar,./external/storm-kafka/storm-kafka-1.1.0.jar" --artifacts "redis.clients:jedis:2.9.0,org.apache.kafka:kafka_2.10:0.8.2.2^org.slf4j:slf4j-log4j12" --artifactRepositories "jboss-repository^http://repository.jboss.com/maven2,HDPRepo^http://repo.hortonworks.com/content/groups/public/"` + + When you pass jars and/or artifacts options, StormSubmitter will upload them when the topology is submitted, and they will be included to classpath of both the process which runs the class, and also workers for that topology. + """ + global DEP_JARS_OPTS, DEP_ARTIFACTS_OPTS, DEP_ARTIFACTS_REPOSITORIES_OPTS + + local_jars = DEP_JARS_OPTS + artifact_to_file_jars = resolve_dependencies(DEP_ARTIFACTS_OPTS, DEP_ARTIFACTS_REPOSITORIES_OPTS) + + transform_class = confvalue("client.jartransformer.class", [CLUSTER_CONF_DIR]) + if (transform_class != None and transform_class != "nil"): + tmpjar = os.path.join(tempfile.gettempdir(), uuid.uuid1().hex+".jar") + exec_storm_class("org.apache.storm.daemon.ClientJarTransformerRunner", args=[transform_class, jarfile, tmpjar], fork=True, daemon=False) + extra_jars = [tmpjar, USER_CONF_DIR, STORM_BIN_DIR] + extra_jars.extend(local_jars) + extra_jars.extend(artifact_to_file_jars.values()) + topology_runner_exit_code = exec_storm_class( + klass, + jvmtype="-client", + extrajars=extra_jars, + args=args, + daemon=False, + fork=True, + jvmopts=JAR_JVM_OPTS + ["-Dstorm.jar=" + tmpjar] + + ["-Dstorm.dependency.jars=" + ",".join(local_jars)] + + ["-Dstorm.dependency.artifacts=" + json.dumps(artifact_to_file_jars)]) + os.remove(tmpjar) + sys.exit(topology_runner_exit_code) + else: + extra_jars=[jarfile, USER_CONF_DIR, STORM_BIN_DIR] + extra_jars.extend(local_jars) + extra_jars.extend(artifact_to_file_jars.values()) + exec_storm_class( + klass, + jvmtype="-client", + extrajars=extra_jars, + args=args, + daemon=False, + jvmopts=JAR_JVM_OPTS + ["-Dstorm.jar=" + jarfile] + + ["-Dstorm.dependency.jars=" + ",".join(local_jars)] + + ["-Dstorm.dependency.artifacts=" + json.dumps(artifact_to_file_jars)]) + +def sql(sql_file, topology_name): + """Syntax: [storm sql sql-file topology-name], or [storm sql sql-file --explain] when activating explain mode + + Compiles the SQL statements into a Trident topology and submits it to Storm. + If user activates explain mode, SQL Runner analyzes each query statement and shows query plan instead of submitting topology. + + --jars and --artifacts, and --artifactRepositories options available for jar are also applied to sql command. + Please refer "help jar" to see how to use --jars and --artifacts, and --artifactRepositories options. + You normally want to pass these options since you need to set data source to your sql which is an external storage in many cases. """ + global DEP_JARS_OPTS, DEP_ARTIFACTS_OPTS, DEP_ARTIFACTS_REPOSITORIES_OPTS + + local_jars = DEP_JARS_OPTS + artifact_to_file_jars = resolve_dependencies(DEP_ARTIFACTS_OPTS, DEP_ARTIFACTS_REPOSITORIES_OPTS) + + sql_core_jars = get_jars_full(STORM_DIR + "/external/sql/storm-sql-core") + sql_runtime_jars = get_jars_full(STORM_DIR + "/external/sql/storm-sql-runtime") + + # include storm-sql-runtime jar(s) to local jar list + local_jars.extend(sql_runtime_jars) + + extrajars=[USER_CONF_DIR, STORM_BIN_DIR] + extrajars.extend(local_jars) + extrajars.extend(artifact_to_file_jars.values()) + + # include this for running StormSqlRunner, but not for generated topology + extrajars.extend(sql_core_jars) + + if topology_name == "--explain": + args = ["--file", sql_file, "--explain"] + else: + args = ["--file", sql_file, "--topology", topology_name] + exec_storm_class( - klass, + "org.apache.storm.sql.StormSqlRunner", jvmtype="-client", - extrajars=[jarfile, USER_CONF_DIR, STORM_BIN_DIR], + extrajars=extrajars, args=args, daemon=False, - jvmopts=JAR_JVM_OPTS + ["-Dstorm.jar=" + jarfile]) + jvmopts=["-Dstorm.dependency.jars=" + ",".join(local_jars)] + + ["-Dstorm.dependency.artifacts=" + json.dumps(artifact_to_file_jars)]) def kill(*args): """Syntax: [storm kill topology-name [-w wait-time-secs]] @@ -228,8 +378,11 @@ def kill(*args): the workers and clean up their state. You can override the length of time Storm waits between deactivation and shutdown with the -w flag. """ + if not args: + print_usage(command="kill") + sys.exit(2) exec_storm_class( - "backtype.storm.command.kill_topology", + "org.apache.storm.command.kill_topology", args=args, jvmtype="-client", extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) @@ -240,8 +393,49 @@ def upload_credentials(*args): Uploads a new set of credentials to a running topology """ + if not args: + print_usage(command="upload_credentials") + sys.exit(2) + exec_storm_class( + "org.apache.storm.command.upload_credentials", + args=args, + jvmtype="-client", + extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) + +def blobstore(*args): + """Syntax: [storm blobstore cmd] + + list [KEY...] - lists blobs currently in the blob store + cat [-f FILE] KEY - read a blob and then either write it to a file, or STDOUT (requires read access). + create [-f FILE] [-a ACL ...] [--replication-factor NUMBER] KEY - create a new blob. Contents comes from a FILE + or STDIN. ACL is in the form [uo]:[username]:[r-][w-][a-] can be comma separated list. + update [-f FILE] KEY - update the contents of a blob. Contents comes from + a FILE or STDIN (requires write access). + delete KEY - delete an entry from the blob store (requires write access). + set-acl [-s ACL] KEY - ACL is in the form [uo]:[username]:[r-][w-][a-] can be comma + separated list (requires admin access). + replication --read KEY - Used to read the replication factor of the blob. + replication --update --replication-factor NUMBER KEY where NUMBER > 0. It is used to update the + replication factor of a blob. + For example, the following would create a mytopo:data.tgz key using the data + stored in data.tgz. User alice would have full access, bob would have + read/write access and everyone else would have read access. + storm blobstore create mytopo:data.tgz -f data.tgz -a u:alice:rwa,u:bob:rw,o::r + """ exec_storm_class( - "backtype.storm.command.upload_credentials", + "org.apache.storm.command.blobstore", + args=args, + jvmtype="-client", + extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) + +def heartbeats(*args): + """Syntax: [storm heartbeats [cmd]] + + list PATH - lists heartbeats nodes under PATH currently in the ClusterState. + get PATH - Get the heartbeat data at PATH + """ + exec_storm_class( + "org.apache.storm.command.heartbeats", args=args, jvmtype="-client", extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) @@ -251,8 +445,44 @@ def activate(*args): Activates the specified topology's spouts. """ + if not args: + print_usage(command="activate") + sys.exit(2) exec_storm_class( - "backtype.storm.command.activate", + "org.apache.storm.command.activate", + args=args, + jvmtype="-client", + extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) + +def set_log_level(*args): + """ + Dynamically change topology log levels + + Syntax: [storm set_log_level -l [logger name]=[log level][:optional timeout] -r [logger name] topology-name] + where log level is one of: + ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF + and timeout is integer seconds. + + e.g. + ./bin/storm set_log_level -l ROOT=DEBUG:30 topology-name + + Set the root logger's level to DEBUG for 30 seconds + + ./bin/storm set_log_level -l com.myapp=WARN topology-name + + Set the com.myapp logger's level to WARN for 30 seconds + + ./bin/storm set_log_level -l com.myapp=WARN -l com.myOtherLogger=ERROR:123 topology-name + + Set the com.myapp logger's level to WARN indifinitely, and com.myOtherLogger + to ERROR for 123 seconds + + ./bin/storm set_log_level -r com.myOtherLogger topology-name + + Clears settings, resetting back to the original level + """ + exec_storm_class( + "org.apache.storm.command.set_log_level", args=args, jvmtype="-client", extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) @@ -263,7 +493,7 @@ def listtopos(*args): List the running topologies and their statuses. """ exec_storm_class( - "backtype.storm.command.list", + "org.apache.storm.command.list", args=args, jvmtype="-client", extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) @@ -273,8 +503,11 @@ def deactivate(*args): Deactivates the specified topology's spouts. """ + if not args: + print_usage(command="deactivate") + sys.exit(2) exec_storm_class( - "backtype.storm.command.deactivate", + "org.apache.storm.command.deactivate", args=args, jvmtype="-client", extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) @@ -300,19 +533,67 @@ def rebalance(*args): Use the -n and -e switches to change the number of workers or number of executors of a component respectively. """ + if not args: + print_usage(command="rebalance") + sys.exit(2) exec_storm_class( - "backtype.storm.command.rebalance", + "org.apache.storm.command.rebalance", args=args, jvmtype="-client", extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) +def get_errors(*args): + """Syntax: [storm get-errors topology-name] + + Get the latest error from the running topology. The returned result contains + the key value pairs for component-name and component-error for the components in error. + The result is returned in json format. + """ + if not args: + print_usage(command="get_errors") + sys.exit(2) + exec_storm_class( + "org.apache.storm.command.get_errors", + args=args, + jvmtype="-client", + extrajars=[USER_CONF_DIR, os.path.join(STORM_DIR, "bin")]) + +def healthcheck(*args): + """Syntax: [storm node-health-check] + + Run health checks on the local supervisor. + """ + exec_storm_class( + "org.apache.storm.command.HealthCheck", + args=args, + jvmtype="-client", + extrajars=[USER_CONF_DIR, os.path.join(STORM_DIR, "bin")]) + +def kill_workers(*args): + """Syntax: [storm kill_workers] + + Kill the workers running on this supervisor. This command should be run + on a supervisor node. If the cluster is running in secure mode, then user needs + to have admin rights on the node to be able to successfully kill all workers. + """ + exec_storm_class( + "org.apache.storm.command.KillWorkers", + args=args, + jvmtype="-client", + extrajars=[USER_CONF_DIR, os.path.join(STORM_DIR, "bin")]) + def shell(resourcesdir, command, *args): + """Syntax: [storm shell resourcesdir command args] + + Archives resources to jar and uploads jar to Nimbus, and executes following arguments on "local". Useful for non JVM languages. + eg: `storm shell resources/ python topology.py arg1 arg2` + """ tmpjarpath = "stormshell" + str(random.randint(0, 10000000)) + ".jar" os.system("jar cf %s %s" % (tmpjarpath, resourcesdir)) runnerargs = [tmpjarpath, command] runnerargs.extend(args) exec_storm_class( - "backtype.storm.command.shell_submission", + "org.apache.storm.command.shell_submission", args=runnerargs, jvmtype="-client", extrajars=[USER_CONF_DIR], @@ -328,26 +609,29 @@ def repl(): cppaths = [CLUSTER_CONF_DIR] exec_storm_class("clojure.main", jvmtype="-client", extrajars=cppaths) -def get_log4j_conf_dir(): +def get_log4j2_conf_dir(): cppaths = [CLUSTER_CONF_DIR] - storm_log4j_conf_dir = confvalue("storm.logback.conf.dir", cppaths) - if(storm_log4j_conf_dir == None or storm_log4j_conf_dir == "nil"): - storm_log4j_conf_dir = STORM_LOG4J_CONF_DIR - return storm_log4j_conf_dir - -def nimbus(klass="backtype.storm.daemon.nimbus"): + storm_log4j2_conf_dir = confvalue("storm.log4j2.conf.dir", cppaths) + if(storm_log4j2_conf_dir == None or storm_log4j2_conf_dir == "nil"): + storm_log4j2_conf_dir = STORM_LOG4J2_CONF_DIR + elif(not os.path.isabs(storm_log4j2_conf_dir)): + storm_log4j2_conf_dir = os.path.join(STORM_DIR, storm_log4j2_conf_dir) + return storm_log4j2_conf_dir + +def nimbus(klass="org.apache.storm.daemon.nimbus"): """Syntax: [storm nimbus] Launches the nimbus daemon. This command should be run under supervision with a tool like daemontools or monit. See Setting up a Storm cluster for more information. - (http://storm.incubator.apache.org/documentation/Setting-up-a-Storm-cluster) + (http://storm.apache.org/documentation/Setting-up-a-Storm-cluster) """ cppaths = [CLUSTER_CONF_DIR] jvmopts = parse_args(confvalue("nimbus.childopts", cppaths)) + [ "-Dlogfile.name=nimbus.log", - "-Dlog4j.configurationFile=" + os.path.join(get_log4j_conf_dir(), "cluster.xml"), + "-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", + "-Dlog4j.configurationFile=" + os.path.join(get_log4j2_conf_dir(), "cluster.xml"), ] exec_storm_class( klass, @@ -356,19 +640,41 @@ def nimbus(klass="backtype.storm.daemon.nimbus"): extrajars=cppaths, jvmopts=jvmopts) -def supervisor(klass="backtype.storm.daemon.supervisor"): +def pacemaker(klass="org.apache.storm.pacemaker.pacemaker"): + """Syntax: [storm pacemaker] + + Launches the Pacemaker daemon. This command should be run under + supervision with a tool like daemontools or monit. + + See Setting up a Storm cluster for more information. + (http://storm.apache.org/documentation/Setting-up-a-Storm-cluster) + """ + cppaths = [CLUSTER_CONF_DIR] + jvmopts = parse_args(confvalue("pacemaker.childopts", cppaths)) + [ + "-Dlogfile.name=pacemaker.log", + "-Dlog4j.configurationFile=" + os.path.join(get_log4j2_conf_dir(), "cluster.xml"), + ] + exec_storm_class( + klass, + jvmtype="-server", + daemonName="pacemaker", + extrajars=cppaths, + jvmopts=jvmopts) + +def supervisor(klass="org.apache.storm.daemon.supervisor.Supervisor"): """Syntax: [storm supervisor] Launches the supervisor daemon. This command should be run under supervision with a tool like daemontools or monit. See Setting up a Storm cluster for more information. - (http://storm.incubator.apache.org/documentation/Setting-up-a-Storm-cluster) + (http://storm.apache.org/documentation/Setting-up-a-Storm-cluster) """ cppaths = [CLUSTER_CONF_DIR] jvmopts = parse_args(confvalue("supervisor.childopts", cppaths)) + [ - "-Dlogfile.name=supervisor.log", - "-Dlog4j.configurationFile=" + os.path.join(get_log4j_conf_dir(), "cluster.xml"), + "-Dlogfile.name=" + STORM_SUPERVISOR_LOG_FILE, + "-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", + "-Dlog4j.configurationFile=" + os.path.join(get_log4j2_conf_dir(), "cluster.xml"), ] exec_storm_class( klass, @@ -385,15 +691,16 @@ def ui(): should be run under supervision with a tool like daemontools or monit. See Setting up a Storm cluster for more information. - (http://storm.incubator.apache.org/documentation/Setting-up-a-Storm-cluster) + (http://storm.apache.org/documentation/Setting-up-a-Storm-cluster) """ cppaths = [CLUSTER_CONF_DIR] jvmopts = parse_args(confvalue("ui.childopts", cppaths)) + [ "-Dlogfile.name=ui.log", - "-Dlog4j.configurationFile=" + os.path.join(get_log4j_conf_dir(), "cluster.xml") + "-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", + "-Dlog4j.configurationFile=" + os.path.join(get_log4j2_conf_dir(), "cluster.xml") ] exec_storm_class( - "backtype.storm.ui.core", + "org.apache.storm.ui.core", jvmtype="-server", daemonName="ui", jvmopts=jvmopts, @@ -407,15 +714,16 @@ def logviewer(): tool like daemontools or monit. See Setting up a Storm cluster for more information. - (http://storm.incubator.apache.org/documentation/Setting-up-a-Storm-cluster) + (http://storm.apache.org/documentation/Setting-up-a-Storm-cluster) """ cppaths = [CLUSTER_CONF_DIR] jvmopts = parse_args(confvalue("logviewer.childopts", cppaths)) + [ "-Dlogfile.name=logviewer.log", - "-Dlog4j.configurationFile=" + os.path.join(get_log4j_conf_dir(), "cluster.xml") + "-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", + "-Dlog4j.configurationFile=" + os.path.join(get_log4j2_conf_dir(), "cluster.xml") ] exec_storm_class( - "backtype.storm.daemon.logviewer", + "org.apache.storm.daemon.logviewer", jvmtype="-server", daemonName="logviewer", jvmopts=jvmopts, @@ -428,15 +736,16 @@ def drpc(): with a tool like daemontools or monit. See Distributed RPC for more information. - (http://storm.incubator.apache.org/documentation/Distributed-RPC) + (http://storm.apache.org/documentation/Distributed-RPC) """ cppaths = [CLUSTER_CONF_DIR] jvmopts = parse_args(confvalue("drpc.childopts", cppaths)) + [ "-Dlogfile.name=drpc.log", - "-Dlog4j.configurationFile=" + os.path.join(get_log4j_conf_dir(), "cluster.xml") + "-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector", + "-Dlog4j.configurationFile=" + os.path.join(get_log4j2_conf_dir(), "cluster.xml") ] exec_storm_class( - "backtype.storm.daemon.drpc", + "org.apache.storm.daemon.drpc", jvmtype="-server", daemonName="drpc", jvmopts=jvmopts, @@ -451,7 +760,7 @@ def dev_zookeeper(): """ cppaths = [CLUSTER_CONF_DIR] exec_storm_class( - "backtype.storm.command.dev_zookeeper", + "org.apache.storm.command.dev_zookeeper", jvmtype="-server", extrajars=[CLUSTER_CONF_DIR]) @@ -462,7 +771,7 @@ def version(): """ cppaths = [CLUSTER_CONF_DIR] exec_storm_class( - "backtype.storm.utils.VersionInfo", + "org.apache.storm.utils.VersionInfo", jvmtype="-client", extrajars=[CLUSTER_CONF_DIR]) @@ -485,7 +794,7 @@ def monitor(*args): watch-item is 'emitted'; """ exec_storm_class( - "backtype.storm.command.monitor", + "org.apache.storm.command.monitor", args=args, jvmtype="-client", extrajars=[USER_CONF_DIR, STORM_BIN_DIR]) @@ -495,7 +804,7 @@ def print_commands(): """Print all client commands and link to documentation""" print("Commands:\n\t" + "\n\t".join(sorted(COMMANDS.keys()))) print("\nHelp: \n\thelp \n\thelp ") - print("\nDocumentation for the storm client can be found at http://storm.incubator.apache.org/documentation/Command-line-client.html\n") + print("\nDocumentation for the storm client can be found at http://storm.apache.org/documentation/Command-line-client.html\n") print("Configs can be overridden using one or more -c flags, e.g. \"storm list -c nimbus.host=nimbus.mycompany.com\"\n") def print_usage(command=None): @@ -519,7 +828,9 @@ def unknown_command(*args): "remoteconfvalue": print_remoteconfvalue, "repl": repl, "classpath": print_classpath, "activate": activate, "deactivate": deactivate, "rebalance": rebalance, "help": print_usage, "list": listtopos, "dev-zookeeper": dev_zookeeper, "version": version, "monitor": monitor, - "upload-credentials": upload_credentials} + "upload-credentials": upload_credentials, "pacemaker": pacemaker, "heartbeats": heartbeats, "blobstore": blobstore, + "get-errors": get_errors, "set_log_level": set_log_level, "kill_workers": kill_workers, + "node-health-check": healthcheck, "sql": sql} def parse_config(config_list): global CONFIG_OPTS @@ -528,30 +839,42 @@ def parse_config(config_list): CONFIG_OPTS.append(config) def parse_config_opts(args): - curr = args[:] - curr.reverse() - config_list = [] - args_list = [] - - while len(curr) > 0: - token = curr.pop() - if token == "-c": - config_list.append(curr.pop()) - elif token == "--config": - global CONFFILE - CONFFILE = curr.pop() - else: - args_list.append(token) + curr = args[:] + curr.reverse() + config_list = [] + args_list = [] + jars_list = [] + artifacts_list = [] + artifact_repositories_list = [] + + while len(curr) > 0: + token = curr.pop() + if token == "-c": + config_list.append(curr.pop()) + elif token == "--config": + global CONFFILE + CONFFILE = curr.pop() + elif token == "--jars": + jars_list.extend(curr.pop().split(',')) + elif token == "--artifacts": + artifacts_list.extend(curr.pop().split(',')) + elif token == "--artifactRepositories": + artifact_repositories_list.extend(curr.pop().split(',')) + else: + args_list.append(token) - return config_list, args_list + return config_list, jars_list, artifacts_list, artifact_repositories_list, args_list def main(): if len(sys.argv) <= 1: print_usage() sys.exit(-1) - global CONFIG_OPTS - config_list, args = parse_config_opts(sys.argv[1:]) + global CONFIG_OPTS, DEP_JARS_OPTS, DEP_ARTIFACTS_OPTS, DEP_ARTIFACTS_REPOSITORIES_OPTS + config_list, jars_list, artifacts_list, artifact_repositories_list, args = parse_config_opts(sys.argv[1:]) parse_config(config_list) + DEP_JARS_OPTS = jars_list + DEP_ARTIFACTS_OPTS = artifacts_list + DEP_ARTIFACTS_REPOSITORIES_OPTS = artifact_repositories_list COMMAND = args[0] ARGS = args[1:] (COMMANDS.get(COMMAND, unknown_command))(*ARGS) diff --git a/conf/defaults.yaml b/conf/defaults.yaml index 875543f8e0e..f89211b078d 100644 --- a/conf/defaults.yaml +++ b/conf/defaults.yaml @@ -23,6 +23,7 @@ java.library.path: "/usr/local/lib:/opt/local/lib:/usr/lib" ### storm.* configs are general configurations # the local dir is where jars are kept storm.local.dir: "storm-local" +storm.log4j2.conf.dir: "log4j2" storm.zookeeper.servers: - "localhost" storm.zookeeper.port: 2181 @@ -34,12 +35,16 @@ storm.zookeeper.retry.interval: 1000 storm.zookeeper.retry.intervalceiling.millis: 30000 storm.zookeeper.auth.user: null storm.zookeeper.auth.password: null +storm.exhibitor.port: 8080 +storm.exhibitor.poll.uripath: "/exhibitor/v1/cluster/list" storm.cluster.mode: "distributed" # can be distributed or local storm.local.mode.zmq: false -storm.thrift.transport: "backtype.storm.security.auth.SimpleTransportPlugin" -storm.principal.tolocal: "backtype.storm.security.auth.DefaultPrincipalToLocal" -storm.group.mapping.service: "backtype.storm.security.auth.ShellBasedGroupsMapping" -storm.messaging.transport: "backtype.storm.messaging.netty.Context" +storm.thrift.transport: "org.apache.storm.security.auth.SimpleTransportPlugin" +storm.thrift.socket.timeout.ms: 600000 +storm.principal.tolocal: "org.apache.storm.security.auth.DefaultPrincipalToLocal" +storm.group.mapping.service: "org.apache.storm.security.auth.ShellBasedGroupsMapping" +storm.group.mapping.service.params: null +storm.messaging.transport: "org.apache.storm.messaging.netty.Context" storm.nimbus.retry.times: 5 storm.nimbus.retry.interval.millis: 2000 storm.nimbus.retry.intervalceiling.millis: 60000 @@ -47,10 +52,16 @@ storm.auth.simple-white-list.users: [] storm.auth.simple-acl.users: [] storm.auth.simple-acl.users.commands: [] storm.auth.simple-acl.admins: [] -storm.meta.serialization.delegate: "backtype.storm.serialization.GzipThriftSerializationDelegate" +storm.cluster.state.store: "org.apache.storm.cluster_state.zookeeper_state_factory" +storm.meta.serialization.delegate: "org.apache.storm.serialization.GzipThriftSerializationDelegate" +storm.codedistributor.class: "org.apache.storm.codedistributor.LocalFileSystemCodeDistributor" +storm.workers.artifacts.dir: "workers-artifacts" +storm.health.check.dir: "healthchecks" +storm.health.check.timeout.ms: 5000 +storm.disable.symlinks: false ### nimbus.* configs are for the master -nimbus.host: "localhost" +nimbus.seeds : ["localhost"] nimbus.thrift.port: 6627 nimbus.thrift.threads: 64 nimbus.thrift.max_buffer_size: 1048576 @@ -60,11 +71,15 @@ nimbus.supervisor.timeout.secs: 60 nimbus.monitor.freq.secs: 10 nimbus.cleanup.inbox.freq.secs: 600 nimbus.inbox.jar.expiration.secs: 3600 +nimbus.code.sync.freq.secs: 120 nimbus.task.launch.secs: 120 -nimbus.reassign: true nimbus.file.copy.expiration.secs: 600 -nimbus.topology.validator: "backtype.storm.nimbus.DefaultTopologyValidator" +nimbus.topology.validator: "org.apache.storm.nimbus.DefaultTopologyValidator" +topology.min.replication.count: 1 +topology.max.replication.wait.time.sec: 60 nimbus.credential.renewers.freq.secs: 600 +nimbus.queue.size: 100000 +scheduler.display.resource: false ### ui.* configs are for the master ui.host: 0.0.0.0 @@ -75,12 +90,15 @@ ui.filter: null ui.filter.params: null ui.users: null ui.header.buffer.bytes: 4096 -ui.http.creds.plugin: backtype.storm.security.auth.DefaultHttpCredentialsPlugin +ui.http.creds.plugin: org.apache.storm.security.auth.DefaultHttpCredentialsPlugin +ui.http.x-frame-options: DENY logviewer.port: 8000 logviewer.childopts: "-Xmx128m" logviewer.cleanup.age.mins: 10080 logviewer.appender.name: "A1" +logviewer.max.sum.worker.logs.size.mb: 4096 +logviewer.max.per.worker.logs.size.mb: 2048 logs.users: null @@ -96,7 +114,7 @@ drpc.http.port: 3774 drpc.https.port: -1 drpc.https.keystore.password: "" drpc.https.keystore.type: "JKS" -drpc.http.creds.plugin: backtype.storm.security.auth.DefaultHttpCredentialsPlugin +drpc.http.creds.plugin: org.apache.storm.security.auth.DefaultHttpCredentialsPlugin drpc.authorizer.acl.filename: "drpc-auth-acl.yaml" drpc.authorizer.acl.strict: false @@ -104,6 +122,22 @@ transactional.zookeeper.root: "/transactional" transactional.zookeeper.servers: null transactional.zookeeper.port: null +## blobstore configs +supervisor.blobstore.class: "org.apache.storm.blobstore.NimbusBlobStore" +supervisor.blobstore.download.thread.count: 5 +supervisor.blobstore.download.max_retries: 3 +supervisor.localizer.cache.target.size.mb: 10240 +supervisor.localizer.cleanup.interval.ms: 600000 + +nimbus.blobstore.class: "org.apache.storm.blobstore.LocalFsBlobStore" +nimbus.blobstore.expiration.secs: 600 + +storm.blobstore.inputstream.buffer.size.bytes: 65536 +client.blobstore.class: "org.apache.storm.blobstore.NimbusBlobStore" +storm.blobstore.replication.factor: 3 +# For secure mode we would want to change this config to true +storm.blobstore.acl.validation.enabled: false + ### supervisor.* configs are for node supervisors # Define the amount of workers that can be run on this machine. Each worker is assigned a port to use for communication supervisor.slots.ports: @@ -118,7 +152,7 @@ supervisor.worker.start.timeout.secs: 120 #how long between heartbeats until supervisor considers that worker dead and tries to restart it supervisor.worker.timeout.secs: 30 #how many seconds to sleep for before shutting down threads on worker -supervisor.worker.shutdown.sleep.secs: 1 +supervisor.worker.shutdown.sleep.secs: 3 #how frequently the supervisor checks on the status of the processes it's monitoring and restarts if necessary supervisor.monitor.frequency.secs: 3 #how frequently the supervisor heartbeats to the cluster state (for nimbus) @@ -126,19 +160,40 @@ supervisor.heartbeat.frequency.secs: 5 supervisor.enable: true supervisor.supervisors: [] supervisor.supervisors.commands: [] - +supervisor.memory.capacity.mb: 3072.0 +#By convention 1 cpu core should be about 100, but this can be adjusted if needed +# using 100 makes it simple to set the desired value to the capacity measurement +# for single threaded bolts +supervisor.cpu.capacity: 400.0 ### worker.* configs are for task workers -worker.childopts: "-Xmx768m" +worker.heap.memory.mb: 768 +worker.childopts: "-Xmx%HEAP-MEM%m -XX:+PrintGCDetails -Xloggc:artifacts/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=artifacts/heapdump" worker.gc.childopts: "" + +# Unlocking commercial features requires a special license from Oracle. +# See http://www.oracle.com/technetwork/java/javase/terms/products/index.html +# For this reason, profiler features are disabled by default. +worker.profiler.enabled: false +worker.profiler.childopts: "-XX:+UnlockCommercialFeatures -XX:+FlightRecorder" +worker.profiler.command: "flight.bash" worker.heartbeat.frequency.secs: 1 +# check whether dynamic log levels can be reset from DEBUG to INFO in workers +worker.log.level.reset.poll.secs: 30 + # control how many worker receiver threads we need per worker topology.worker.receiver.thread.count: 1 task.heartbeat.frequency.secs: 3 task.refresh.poll.secs: 10 task.credentials.poll.secs: 30 +task.backpressure.poll.secs: 30 + +# now should be null by default +topology.backpressure.enable: false +backpressure.disruptor.high.watermark: 0.9 +backpressure.disruptor.low.watermark: 0.4 zmq.threads: 1 zmq.linger.millis: 5000 @@ -157,12 +212,13 @@ storm.messaging.netty.min_wait_ms: 100 storm.messaging.netty.transfer.batch.size: 262144 # Sets the backlog value to specify when the channel binds to a local address storm.messaging.netty.socket.backlog: 500 -# We check with this interval that whether the Netty channel is writable and try to write pending messages if it is. -storm.messaging.netty.flush.check.interval.ms: 10 # By default, the Netty SASL authentication is set to false. Users can override and set it true for a specific topology. storm.messaging.netty.authentication: false +# Default plugin to use for automatic network topology discovery +storm.network.topography.plugin: org.apache.storm.networktopography.DefaultRackDNSToSwitchMapping + # default number of seconds group mapping service will cache user group storm.group.mapping.service.cache.duration.secs: 120 @@ -171,10 +227,12 @@ topology.enable.message.timeouts: true topology.debug: false topology.workers: 1 topology.acker.executors: null +topology.eventlogger.executors: 0 topology.tasks: null # maximum amount of time a message has to complete before it's considered failed topology.message.timeout.secs: 30 -topology.multilang.serializer: "backtype.storm.multilang.JsonSerializer" +topology.multilang.serializer: "org.apache.storm.multilang.JsonSerializer" +topology.shellbolt.max.pending: 100 topology.skip.missing.kryo.registrations: false topology.max.task.parallelism: null topology.max.spout.pending: null @@ -183,22 +241,55 @@ topology.stats.sample.rate: 0.05 topology.builtin.metrics.bucket.size.secs: 60 topology.fall.back.on.java.serialization: true topology.worker.childopts: null +topology.worker.logwriter.childopts: "-Xmx64m" topology.executor.receive.buffer.size: 1024 #batched topology.executor.send.buffer.size: 1024 #individual messages topology.transfer.buffer.size: 1024 # batched topology.tick.tuple.freq.secs: null topology.worker.shared.thread.pool.size: 4 -topology.disruptor.wait.strategy: "com.lmax.disruptor.BlockingWaitStrategy" -topology.spout.wait.strategy: "backtype.storm.spout.SleepSpoutWaitStrategy" +topology.spout.wait.strategy: "org.apache.storm.spout.SleepSpoutWaitStrategy" topology.sleep.spout.wait.strategy.time.ms: 1 topology.error.throttle.interval.secs: 10 topology.max.error.report.per.interval: 5 -topology.kryo.factory: "backtype.storm.serialization.DefaultKryoFactory" -topology.tuple.serializer: "backtype.storm.serialization.types.ListDelegateSerializer" +topology.kryo.factory: "org.apache.storm.serialization.DefaultKryoFactory" +topology.tuple.serializer: "org.apache.storm.serialization.types.ListDelegateSerializer" topology.trident.batch.emit.interval.millis: 500 topology.testing.always.try.serialize: false topology.classpath: null topology.environment: null topology.bolts.outgoing.overflow.buffer.enable: false +topology.disruptor.wait.timeout.millis: 1000 +topology.disruptor.batch.size: 100 +topology.disruptor.batch.timeout.millis: 1 +topology.disable.loadaware.messaging: false +topology.state.checkpoint.interval.ms: 1000 + +# Configs for Resource Aware Scheduler +# topology priority describing the importance of the topology in decreasing importance starting from 0 (i.e. 0 is the highest priority and the priority importance decreases as the priority number increases). +# Recommended range of 0-29 but no hard limit set. +topology.priority: 29 +topology.component.resources.onheap.memory.mb: 128.0 +topology.component.resources.offheap.memory.mb: 0.0 +topology.component.cpu.pcore.percent: 10.0 +topology.worker.max.heap.size.mb: 768.0 +topology.scheduler.strategy: "org.apache.storm.scheduler.resource.strategies.scheduling.DefaultResourceAwareStrategy" +resource.aware.scheduler.eviction.strategy: "org.apache.storm.scheduler.resource.strategies.eviction.DefaultEvictionStrategy" +resource.aware.scheduler.priority.strategy: "org.apache.storm.scheduler.resource.strategies.priority.DefaultSchedulingPriorityStrategy" dev.zookeeper.path: "/tmp/dev-storm-zookeeper" + +pacemaker.host: "localhost" +pacemaker.port: 6699 +pacemaker.base.threads: 10 +pacemaker.max.threads: 50 +pacemaker.thread.timeout: 10 +pacemaker.childopts: "-Xmx1024m" +pacemaker.auth.method: "NONE" +pacemaker.kerberos.users: [] + +#default storm daemon metrics reporter plugins +storm.daemon.metrics.reporter.plugins: + - "org.apache.storm.daemon.metrics.reporters.JmxPreparableReporter" + +# configuration of cluster metrics consumer +storm.cluster.metrics.consumer.publish.interval.secs: 60 diff --git a/conf/storm-env.sh b/conf/storm-env.sh index d66aea00ed8..ff2f449c39b 100644 --- a/conf/storm-env.sh +++ b/conf/storm-env.sh @@ -19,6 +19,6 @@ # Set Storm specific environment variables here. # The java implementation to use. -export JAVA_HOME=${JAVA_HOME} +#export JAVA_HOME=/path/to/jdk/home # export STORM_CONF_DIR="" diff --git a/conf/storm.yaml.example b/conf/storm.yaml.example index 5fd35f89f26..8d54e194669 100644 --- a/conf/storm.yaml.example +++ b/conf/storm.yaml.example @@ -19,7 +19,7 @@ # - "server1" # - "server2" # -# nimbus.host: "nimbus" +# nimbus.seeds: ["host1", "host2", "host3"] # # # ##### These may optionally be filled in: @@ -39,10 +39,37 @@ # - "server2" ## Metrics Consumers +## max.retain.metric.tuples +## - task queue will be unbounded when max.retain.metric.tuples is equal or less than 0. +## whitelist / blacklist +## - when none of configuration for metric filter are specified, it'll be treated as 'pass all'. +## - you need to specify either whitelist or blacklist, or none of them. You can't specify both of them. +## - you can specify multiple whitelist / blacklist with regular expression +## expandMapType: expand metric with map type as value to multiple metrics +## - set to true when you would like to apply filter to expanded metrics +## - default value is false which is backward compatible value +## metricNameSeparator: separator between origin metric name and key of entry from map +## - only effective when expandMapType is set to true # topology.metrics.consumer.register: -# - class: "backtype.storm.metric.LoggingMetricsConsumer" +# - class: "org.apache.storm.metric.LoggingMetricsConsumer" +# max.retain.metric.tuples: 100 # parallelism.hint: 1 # - class: "org.mycompany.MyMetricsConsumer" +# max.retain.metric.tuples: 100 +# whitelist: +# - "execute.*" +# - "^__complete-latency$" # parallelism.hint: 1 # argument: # - endpoint: "metrics-collector.mycompany.org" +# expandMapType: true +# metricNameSeparator: "." + +## Cluster Metrics Consumers +# storm.cluster.metrics.consumer.register: +# - class: "org.apache.storm.metric.LoggingClusterMetricsConsumer" +# - class: "org.mycompany.MyMetricsConsumer" +# argument: +# - endpoint: "metrics-collector.mycompany.org" +# +# storm.cluster.metrics.consumer.publish.interval.secs: 60 \ No newline at end of file diff --git a/conf/user-resource-pools-example.yaml b/conf/user-resource-pools-example.yaml new file mode 100644 index 00000000000..829a6be9df4 --- /dev/null +++ b/conf/user-resource-pools-example.yaml @@ -0,0 +1,26 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource.aware.scheduler.user.pools: + jerry: + cpu: 1000 + memory: 8192.0 + derek: + cpu: 10000.0 + memory: 32768 + bobby: + cpu: 5000.0 + memory: 16384.0 \ No newline at end of file diff --git a/dev-tools/github/__init__.py b/dev-tools/github/__init__.py index 2c533d00e94..48f397b0b71 100755 --- a/dev-tools/github/__init__.py +++ b/dev-tools/github/__init__.py @@ -13,108 +13,127 @@ import getpass import base64 -import urllib import urllib2 from datetime import datetime +import re + try: - import json + import json except ImportError: - import simplejson as json + import simplejson as json + def mstr(obj): - if (obj == None): - return "" - return unicode(obj) + if obj is None: + return "" + return unicode(obj) + + +def git_time(obj): + if obj is None: + return None + return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S") -def gittime(obj): - if (obj == None): - return None - return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S") class GitPullRequest: - """Pull Request from Git""" - def __init__(self, data, parent): - self.data = data - self.parent = parent - - def html_url(self): - return self.data["html_url"] - - def title(self): - return self.data["title"] - - def number(self): - return self.data["number"] - - #TODO def review_comments - - def user(self): - return mstr(self.data["user"]["login"]) - - def fromBranch(self): - return mstr(self.data["head"]["ref"]) - - def fromRepo(self): - return mstr(self.data["head"]["repo"]["clone_url"]) - - def merged(self): - return self.data["merged_at"] != None - - def raw(self): - return self.data - - def created_at(self): - return gittime(self.data["created_at"]) - - def updated_at(self): - return gittime(self.data["updated_at"]) - - def merged_at(self): - return gittime(self.data["merged_at"]) - - def __str__(self): - return self.html_url() - - def __repr__(self): - return self.html_url() + """Pull Request from Git""" + + storm_jira_number = re.compile("STORM-[0-9]+", re.I) + + def __init__(self, data, parent): + self.data = data + self.parent = parent + + def html_url(self): + return self.data["html_url"] + + def title(self): + return self.data["title"] + + def trimmed_title(self): + limit = 40 + title = self.data["title"] + return title if len(title) < limit else title[0:limit] + "..." + + def number(self): + return self.data["number"] + + # TODO def review_comments + + def user(self): + return mstr(self.data["user"]["login"]) + + def from_branch(self): + return mstr(self.data["head"]["ref"]) + + def from_repo(self): + return mstr(self.data["head"]["repo"]["clone_url"]) + + def merged(self): + return self.data["merged_at"] is not None + + def raw(self): + return self.data + + def created_at(self): + return git_time(self.data["created_at"]) + + def updated_at(self): + return git_time(self.data["updated_at"]) + + def merged_at(self): + return git_time(self.data["merged_at"]) + + def has_jira_id(self): + return GitPullRequest.storm_jira_number.search(self.title()) + + def jira_id(self): + return GitPullRequest.storm_jira_number.search(self.title()).group(0).upper() + + def __str__(self): + return self.html_url() + + def __repr__(self): + return self.html_url() + class GitHub: - """Github API""" - def __init__(self, options): - self.headers = {} - if options.gituser: - gitpassword = getpass.getpass("github.com user " + options.gituser+":") - authstr = base64.encodestring('%s:%s' % (options.gituser, gitpassword)).replace('\n', '') - self.headers["Authorization"] = "Basic "+authstr - - def pulls(self, user, repo, type="all"): - page=1 - ret = [] - while True: - url = "https://api.github.com/repos/"+user+"/"+repo+"/pulls?state="+type+"&page="+str(page) - - req = urllib2.Request(url,None,self.headers) - result = urllib2.urlopen(req) - contents = result.read() - if result.getcode() != 200: - raise Exception(result.getcode() + " != 200 "+ contents) - got = json.loads(contents) - for part in got: - ret.append(GitPullRequest(part, self)) - if len(got) == 0: - return ret - page = page + 1 - - def openPulls(self, user, repo): - return self.pulls(user, repo, "open") - - def pull(self, user, repo, number): - url = "https://api.github.com/repos/"+user+"/"+repo+"/pulls/"+number - req = urllib2.Request(url,None,self.headers) - result = urllib2.urlopen(req) - contents = result.read() - if result.getcode() != 200: - raise Exception(result.getcode() + " != 200 "+ contents) - got = json.loads(contents) - return GitPullRequest(got, self) + """Github API""" + + def __init__(self, options): + self.headers = {} + if options.gituser: + gitpassword = getpass.getpass("github.com user " + options.gituser + ":") + authstr = base64.encodestring('%s:%s' % (options.gituser, gitpassword)).replace('\n', '') + self.headers["Authorization"] = "Basic " + authstr + + def pulls(self, user, repo, type="all"): + page = 1 + ret = [] + while True: + url = "https://api.github.com/repos/" + user + "/" + repo + "/pulls?state=" + type + "&page=" + str(page) + + req = urllib2.Request(url, None, self.headers) + result = urllib2.urlopen(req) + contents = result.read() + if result.getcode() != 200: + raise Exception(result.getcode() + " != 200 " + contents) + got = json.loads(contents) + for part in got: + ret.append(GitPullRequest(part, self)) + if len(got) == 0: + return ret + page = page + 1 + + def open_pulls(self, user, repo): + return self.pulls(user, repo, "open") + def pull(self, user, repo, number): + url = "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + number + req = urllib2.Request(url, None, self.headers) + result = urllib2.urlopen(req) + contents = result.read() + if result.getcode() != 200: + raise Exception(result.getcode() + " != 200 " + contents) + got = json.loads(contents) + return GitPullRequest(got, self) diff --git a/dev-tools/jira-github-join.py b/dev-tools/jira-github-join.py index d2526e6d59d..fe0daf3d810 100755 --- a/dev-tools/jira-github-join.py +++ b/dev-tools/jira-github-join.py @@ -12,69 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -from jira import JiraRepo -from github import GitHub, mstr -import re from optparse import OptionParser from datetime import datetime +from github import GitHub +from jira import JiraRepo +from report.report_builder import CompleteReportBuilder -def daydiff(a, b): - return (a - b).days def main(): - parser = OptionParser(usage="usage: %prog [options]") - parser.add_option("-g", "--github-user", dest="gituser", - type="string", help="github user, if not supplied no auth is used", metavar="USER") - - (options, args) = parser.parse_args() - - jrepo = JiraRepo("https://issues.apache.org/jira/rest/api/2") - github = GitHub(options) - - openPullRequests = github.openPulls("apache","storm") - stormJiraNumber = re.compile("STORM-[0-9]+", re.I) - openJiras = jrepo.openJiras("STORM") - - jira2Pulls = {} - pullWithoutJira = [] - pullWithBadJira = [] - - for pull in openPullRequests: - found = stormJiraNumber.search(pull.title()) - if found: - jiraNum = found.group(0).upper() - if not (jiraNum in openJiras): - pullWithBadJira.append(pull) - else: - if jira2Pulls.get(jiraNum) == None: - jira2Pulls[jiraNum] = [] - jira2Pulls[jiraNum].append(pull) - else: - pullWithoutJira.append(pull); - - now = datetime.utcnow() - print "Pull requests that need a JIRA:" - print "Pull URL\tPull Title\tPull Age\tPull Update Age" - for pull in pullWithoutJira: - print ("%s\t%s\t%s\t%s"%(pull.html_url(), pull.title(), daydiff(now, pull.created_at()), daydiff(now, pull.updated_at()))).encode("UTF-8") - - print "\nPull with bad or closed JIRA:" - print "Pull URL\tPull Title\tPull Age\tPull Update Age" - for pull in pullWithBadJira: - print ("%s\t%s\t%s\t%s"%(pull.html_url(), pull.title(), daydiff(now, pull.created_at()), daydiff(now, pull.updated_at()))).encode("UTF-8") - - print "\nOpen JIRA to Pull Requests and Possible Votes, vote detection is very approximate:" - print "JIRA\tPull Requests\tJira Summary\tJIRA Age\tPull Age\tJIRA Update Age\tPull Update Age" - print "\tComment Vote\tComment Author\tPull URL\tComment Age" - for key, value in jira2Pulls.items(): - print ("%s\t%s\t%s\t%s\t%s\t%s\t%s"%(key, mstr(value), openJiras[key].getSummary(), - daydiff(now, openJiras[key].getCreated()), daydiff(now, value[0].created_at()), - daydiff(now, openJiras[key].getUpdated()), daydiff(now, value[0].updated_at()))).encode("UTF-8") - for comment in openJiras[key].getComments(): - #print comment.raw() - if comment.hasVote(): - print (("\t%s\t%s\t%s\t%s")%(comment.getVote(), comment.getAuthor(), comment.getPull(), daydiff(now, comment.getCreated()))).encode("UTF-8") + parser = OptionParser(usage="usage: %prog [options]") + parser.add_option("-g", "--github-user", dest="gituser", + type="string", help="github User, if not supplied no auth is used", metavar="USER") -if __name__ == "__main__": - main() + (options, args) = parser.parse_args() + + jira_repo = JiraRepo("https://issues.apache.org/jira/rest/api/2") + github_repo = GitHub(options) + print "Report generated on: %s (GMT)" % (datetime.strftime(datetime.utcnow(), "%Y-%m-%d %H:%M:%S")) + + report_builder = CompleteReportBuilder(jira_repo, github_repo) + report_builder.report.print_all() + + +if __name__ == "__main__": + main() diff --git a/dev-tools/jira/__init__.py b/dev-tools/jira/__init__.py index 15380aa89ba..c98ae312e4e 100755 --- a/dev-tools/jira/__init__.py +++ b/dev-tools/jira/__init__.py @@ -15,218 +15,271 @@ import urllib import urllib2 from datetime import datetime + try: - import json + import json except ImportError: - import simplejson as json + import simplejson as json + def mstr(obj): - if (obj == None): - return "" - return unicode(obj) + if obj is None: + return "" + return unicode(obj) + def jiratime(obj): - if (obj == None): - return None - return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S") + if obj is None: + return None + return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S") + +# Regex pattern definitions +github_user = re.compile("Git[Hh]ub user ([\w-]+)") +github_pull = re.compile("https://github.com/[^/\s]+/[^/\s]+/pull/[0-9]+") +has_vote = re.compile("\s+([-+][01])\s*") +is_diff = re.compile("--- End diff --") + -githubUser = re.compile("Git[Hh]ub user ([\w-]+)") -githubPull = re.compile("https://github.com/[^/\s]+/[^/\s]+/pull/[0-9]+") -hasVote = re.compile("\s+([-+][01])\s*") -isDiff = re.compile("--- End diff --") +def search_group(reg, txt, group): + m = reg.search(txt) + if m is None: + return None + return m.group(group) -def searchGroup(reg, txt, group): - m = reg.search(txt) - if m == None: - return None - return m.group(group) class JiraComment: - """A comment on a JIRA""" + """A comment on a JIRA""" - def __init__(self, data): - self.data = data - self.author = mstr(self.data['author']['name']) - self.githubAuthor = None - self.githubPull = None - self.githubComment = (self.author == "githubbot") - body = self.getBody() - if isDiff.search(body) != None: - self.vote = None - else: - self.vote = searchGroup(hasVote, body, 1) + def __init__(self, data): + self.data = data + self.author = mstr(self.data['author']['name']) + self.github_author = None + self.githubPull = None + self.githubComment = (self.author == "githubbot") + body = self.get_body() + if is_diff.search(body) is not None: + self.vote = None + else: + self.vote = search_group(has_vote, body, 1) - if self.githubComment: - self.githubAuthor = searchGroup(githubUser, body, 1) - self.githubPull = searchGroup(githubPull, body, 0) - + if self.githubComment: + self.github_author = search_group(github_user, body, 1) + self.githubPull = search_group(github_pull, body, 0) - def getAuthor(self): - if self.githubAuthor != None: - return self.githubAuthor - return self.author + def get_author(self): + if self.github_author is not None: + return self.github_author + return self.author - def getBody(self): - return mstr(self.data['body']) + def get_body(self): + return mstr(self.data['body']) - def getPull(self): - return self.githubPull + def get_pull(self): + return self.githubPull - def raw(self): - return self.data + def has_github_pull(self): + return self.githubPull is not None - def hasVote(self): - return self.vote != None + def raw(self): + return self.data - def getVote(self): - return self.vote + def has_vote(self): + return self.vote is not None + + def get_vote(self): + return self.vote + + def get_created(self): + return jiratime(self.data['created']) - def getCreated(self): - return jiratime(self.data['created']) class Jira: - """A single JIRA""" - - def __init__(self, data, parent): - self.key = data['key'] - self.fields = data['fields'] - self.parent = parent - self.notes = None - self.comments = None - - def getId(self): - return mstr(self.key) - - def getDescription(self): - return mstr(self.fields['description']) - - def getReleaseNote(self): - if (self.notes == None): - field = self.parent.fieldIdMap['Release Note'] - if (self.fields.has_key(field)): - self.notes=mstr(self.fields[field]) - else: - self.notes=self.getDescription() - return self.notes - - def getPriority(self): - ret = "" - pri = self.fields['priority'] - if(pri != None): - ret = pri['name'] - return mstr(ret) - - def getAssigneeEmail(self): - ret = "" - mid = self.fields['assignee'] - if mid != None: - ret = mid['emailAddress'] - return mstr(ret) - - - def getAssignee(self): - ret = "" - mid = self.fields['assignee'] - if(mid != None): - ret = mid['displayName'] - return mstr(ret) - - def getComponents(self): - return " , ".join([ comp['name'] for comp in self.fields['components'] ]) - - def getSummary(self): - return self.fields['summary'] - - def getType(self): - ret = "" - mid = self.fields['issuetype'] - if(mid != None): - ret = mid['name'] - return mstr(ret) - - def getReporter(self): - ret = "" - mid = self.fields['reporter'] - if(mid != None): - ret = mid['displayName'] - return mstr(ret) - - def getProject(self): - ret = "" - mid = self.fields['project'] - if(mid != None): - ret = mid['key'] - return mstr(ret) - - def getCreated(self): - return jiratime(self.fields['created']) - - def getUpdated(self): - return jiratime(self.fields['updated']) - - def getComments(self): - if self.comments == None: - jiraId = self.getId() - comments = [] - at=0 - end=1 - count=100 - while (at < end): - params = urllib.urlencode({'startAt':at, 'maxResults':count}) - resp = urllib2.urlopen(self.parent.baseUrl+"/issue/"+jiraId+"/comment?"+params) - data = json.loads(resp.read()) - if (data.has_key('errorMessages')): - raise Exception(data['errorMessages']) - at = data['startAt'] + data['maxResults'] - end = data['total'] - for item in data['comments']: - j = JiraComment(item) - comments.append(j) - self.comments = comments - return self.comments - - def raw(self): - return self.fields + """A single JIRA""" + + def __init__(self, data, parent): + self.key = data['key'] + self.fields = data['fields'] + self.parent = parent + self.notes = None + self.comments = None + + def get_id(self): + return mstr(self.key) + + def get_description(self): + return mstr(self.fields['description']) + + def getReleaseNote(self): + if self.notes is None: + field = self.parent.fieldIdMap['Release Note'] + if self.fields.has_key(field): + self.notes = mstr(self.fields[field]) + else: + self.notes = self.get_description() + return self.notes + + def get_status(self): + ret = "" + status = self.fields['status'] + if status is not None: + ret = status['name'] + return mstr(ret) + + def get_priority(self): + ret = "" + pri = self.fields['priority'] + if pri is not None: + ret = pri['name'] + return mstr(ret) + + def get_assignee_email(self): + ret = "" + mid = self.fields['assignee'] + if mid is not None: + ret = mid['emailAddress'] + return mstr(ret) + + def get_assignee(self): + ret = "" + mid = self.fields['assignee'] + if mid is not None: + ret = mid['displayName'] + return mstr(ret) + + def get_components(self): + return " , ".join([comp['name'] for comp in self.fields['components']]) + + def get_summary(self): + return self.fields['summary'] + + def get_trimmed_summary(self): + limit = 40 + summary = self.fields['summary'] + return summary if len(summary) < limit else summary[0:limit] + "..." + + def get_type(self): + ret = "" + mid = self.fields['issuetype'] + if mid is not None: + ret = mid['name'] + return mstr(ret) + + def get_reporter(self): + ret = "" + mid = self.fields['reporter'] + if mid is not None: + ret = mid['displayName'] + return mstr(ret) + + def get_project(self): + ret = "" + mid = self.fields['project'] + if mid is not None: + ret = mid['key'] + return mstr(ret) + + def get_created(self): + return jiratime(self.fields['created']) + + def get_updated(self): + return jiratime(self.fields['updated']) + + def get_comments(self): + if self.comments is None: + jiraId = self.get_id() + comments = [] + at = 0 + end = 1 + count = 100 + while (at < end): + params = urllib.urlencode({'startAt': at, 'maxResults': count}) + resp = urllib2.urlopen(self.parent.baseUrl + "/issue/" + jiraId + "/comment?" + params) + data = json.loads(resp.read()) + if (data.has_key('errorMessages')): + raise Exception(data['errorMessages']) + at = data['startAt'] + data['maxResults'] + end = data['total'] + for item in data['comments']: + j = JiraComment(item) + comments.append(j) + self.comments = comments + return self.comments + + def has_voted_comment(self): + for comment in self.get_comments(): + if comment.has_vote(): + return True + return False + + def get_trimmed_comments(self, limit=40): + comments = self.get_comments() + return comments if len(comments) < limit else comments[0:limit] + "..." + + def raw(self): + return self.fields + + def storm_jira_cmp(x, y): + xn = x.get_id().split("-")[1] + yn = y.get_id().split("-")[1] + return int(xn) - int(yn) + class JiraRepo: - """A Repository for JIRAs""" - - def __init__(self, baseUrl): - self.baseUrl = baseUrl - resp = urllib2.urlopen(baseUrl+"/field") - data = json.loads(resp.read()) - - self.fieldIdMap = {} - for part in data: - self.fieldIdMap[part['name']] = part['id'] - - def get(self, id): - resp = urllib2.urlopen(self.baseUrl+"/issue/"+id) - data = json.loads(resp.read()) - if (data.has_key('errorMessages')): - raise Exception(data['errorMessages']) - j = Jira(data, self) - return j - - def query(self, query): - jiras = {} - at=0 - end=1 - count=100 - while (at < end): - params = urllib.urlencode({'jql': query, 'startAt':at, 'maxResults':count}) - #print params - resp = urllib2.urlopen(self.baseUrl+"/search?%s"%params) - data = json.loads(resp.read()) - if (data.has_key('errorMessages')): - raise Exception(data['errorMessages']) - at = data['startAt'] + data['maxResults'] - end = data['total'] - for item in data['issues']: - j = Jira(item, self) - jiras[j.getId()] = j - return jiras - - def openJiras(self, project): - return self.query("project = "+project+" AND resolution = Unresolved"); + """A Repository for JIRAs""" + + def __init__(self, baseUrl): + self.baseUrl = baseUrl + resp = urllib2.urlopen(baseUrl + "/field") + data = json.loads(resp.read()) + + self.fieldIdMap = {} + for part in data: + self.fieldIdMap[part['name']] = part['id'] + + def get(self, id): + resp = urllib2.urlopen(self.baseUrl + "/issue/" + id) + data = json.loads(resp.read()) + if (data.has_key('errorMessages')): + raise Exception(data['errorMessages']) + j = Jira(data, self) + return j + + def query(self, query): + jiras = {} + at = 0 + end = 1 + count = 100 + while (at < end): + params = urllib.urlencode({'jql': query, 'startAt': at, 'maxResults': count}) + # print params + resp = urllib2.urlopen(self.baseUrl + "/search?%s" % params) + data = json.loads(resp.read()) + if (data.has_key('errorMessages')): + raise Exception(data['errorMessages']) + at = data['startAt'] + data['maxResults'] + end = data['total'] + for item in data['issues']: + j = Jira(item, self) + jiras[j.get_id()] = j + return jiras + + def unresolved_jiras(self, project): + """ + :param project: The JIRA project to search for unresolved issues + :return: All JIRA issues that have the field resolution = Unresolved + """ + return self.query("project = " + project + " AND resolution = Unresolved") + + def open_jiras(self, project): + """ + :param project: The JIRA project to search for open issues + :return: All JIRA issues that have the field status = Open + """ + return self.query("project = " + project + " AND status = Open") + def in_progress_jiras(self, project): + """ + :param project: The JIRA project to search for In Progress issues + :return: All JIRA issues that have the field status = 'In Progress' + """ + return self.query("project = " + project + " AND status = 'In Progress'") diff --git a/dev-tools/report/__init__.py b/dev-tools/report/__init__.py new file mode 100644 index 00000000000..e931fe08fad --- /dev/null +++ b/dev-tools/report/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/dev-tools/report/formatter.py b/dev-tools/report/formatter.py new file mode 100644 index 00000000000..81f574ded94 --- /dev/null +++ b/dev-tools/report/formatter.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +def encode(obj, encoding='UTF-8'): + """ + Check if the object supports encode() method, and if so, encodes it. + Encoding defaults to UTF-8. + For example objects of type 'int' do not support encode + """ + return obj.encode(encoding) if 'encode' in dir(obj) else obj + +class Formatter: + def __init__(self, fields_tuple=(), row_tuple=(), min_width_tuple=None): + # Format to pass as first argument to the print function, e.g. '%s%s%s' + self.format = "" + # data_format will be of the form ['{!s:43}'],'{!s:39}','{!s:11}','{!s:25}'] + # the widths are determined from the data in order to print output with nice format + # Each entry of the data_format list will be used by the advanced string formatter: + # "{!s:43}".format("Text") + # Advanced string formatter as detailed in here: https://www.python.org/dev/peps/pep-3101/ + self.data_format = [] + Formatter._assert(fields_tuple, row_tuple, min_width_tuple) + self._build_format_tuples(fields_tuple, row_tuple, min_width_tuple) + + @staticmethod + def _assert(o1, o2, o3): + if len(o1) != len(o2) and (o3 is not None and len(o2) != len(o3)): + raise RuntimeError("Object collections must have the same length. " + "len(o1)={0}, len(o2)={1}, len(o3)={2}" + .format(len(o1), len(o2), -1 if o3 is None else len(o3))) + + # determines the widths from the data in order to print output with nice format + @staticmethod + def _find_sizes(fields_tuple, row_tuple, min_width_tuple): + sizes = [] + padding = 3 + for i in range(0, len(row_tuple)): + max_len = max(len(encode(fields_tuple[i])), len(str(encode(row_tuple[i])))) + if min_width_tuple is not None: + max_len = max(max_len, min_width_tuple[i]) + sizes += [max_len + padding] + return sizes + + def _build_format_tuples(self, fields_tuple, row_tuple, min_width_tuple): + sizes = Formatter._find_sizes(fields_tuple, row_tuple, min_width_tuple) + + for i in range(0, len(row_tuple)): + self.format += "%s" + self.data_format += ["{!s:" + str(sizes[i]) + "}"] + + # Returns a tuple where each entry has a string that is the result of + # statements with the pattern "{!s:43}".format("Text") + def row_str_format(self, row_tuple): + format_with_values = [str(self.data_format[0].format(encode(row_tuple[0])))] + for i in range(1, len(row_tuple)): + format_with_values += [str(self.data_format[i].format(encode(row_tuple[i])))] + return tuple(format_with_values) diff --git a/dev-tools/report/report.py b/dev-tools/report/report.py new file mode 100644 index 00000000000..477a4989bfe --- /dev/null +++ b/dev-tools/report/report.py @@ -0,0 +1,253 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +from github import mstr +from jira import Jira +from formatter import Formatter, encode + + +def daydiff(a, b): + return (a - b).days + + +class Report: + now = datetime.utcnow() + def __init__(self, header=''): + self.header = header + + # if padding starts with - it puts padding before contents, otherwise after + @staticmethod + def _build_tuple(contents, padding=''): + if padding is not '': + out = [] + for i in range(len(contents)): + out += [padding[1:] + str(contents[i])] if padding[0] is '-' else [str(contents[i]) + padding] + return tuple(out) + return contents + + # calls the native print function with the following format. Text1,Text2,... has the correct spacing + # print ("%s%s%s" % ("Text1, Text2, Text3)) + def print_(self, formatter, row_tuple): + print (formatter.format % formatter.row_str_format(row_tuple)) + +class JiraReport(Report): + def __init__(self, issues, header=''): + Report.__init__(self, header) + self.issues = issues + + def view(self, excluded): + issues_view = dict(self.issues) + for key in excluded: + issues_view.pop(key, None) + return issues_view + + def keys_view(self, excluded): + return self.view(excluded).keys().sort(Jira.storm_jira_cmp, reverse=True) + + def values_view(self, excluded=None): + temp_dic = dict(self.issues) if excluded is None else self.view(excluded) + values = temp_dic.values() + values.sort(Jira.storm_jira_cmp, reverse=True) + return values + + @staticmethod + def _row_tuple(jira): + return (jira.get_id(), jira.get_trimmed_summary(), daydiff(Report.now, jira.get_created()), + daydiff(Report.now, jira.get_updated())) + + def _min_width_tuple(self): + return -1, 43, -1, -1 + + def print_report(self): + print "%s (Count = %s) " % (self.header, len(self.issues)) + jiras = self.values_view() + fields_tuple = ('Jira Id', 'Summary', 'Created', 'Last Updated (Days)') + row_tuple = self._row_tuple(jiras[0]) + + formatter = Formatter(fields_tuple, row_tuple, self._min_width_tuple()) + + self.print_(formatter, fields_tuple) + + for jira in jiras: + row_tuple = self._row_tuple(jira) + self.print_(formatter, row_tuple) + + @staticmethod + def build_jira_url(jira_id): + BASE_URL = "https://issues.apache.org/jira/browse/" + return BASE_URL + jira_id + + +class GitHubReport(Report): + def __init__(self, pull_requests=None, header=''): + Report.__init__(self, header) + + if pull_requests is None: + self.pull_requests = [] + self.type = '' + else: + self.pull_requests = pull_requests + self.type = type + + def _row_tuple(self, pull): + return self._build_tuple( + (pull.html_url(), pull.trimmed_title(), daydiff(Report.now, pull.created_at()), + daydiff(Report.now, pull.updated_at()), pull.user()), '') + + def _min_width_tuple(self): + return -1, 43, -1, -1, -1 + + def print_report(self): + print "%s (Count = %s) " % (self.header, len(self.pull_requests)) + + fields_tuple = self._build_tuple(('URL', 'Title', 'Created', 'Last Updated (Days)', 'User'), '') + if len(self.pull_requests) > 0: + row_tuple = self._row_tuple(self.pull_requests[0]) + + formatter = Formatter(fields_tuple, row_tuple, self._min_width_tuple()) + + self.print_(formatter, fields_tuple) + for pull in self.pull_requests: + row_tuple = self._row_tuple(pull) + self.print_(formatter, row_tuple) + + def jira_ids(self): + """ + :return: sorted list of JIRA ids present in Git pull requests + """ + jira_ids = list() + for pull in self.pull_requests: + jira_ids.append(pull.jira_id()) + return sorted(jira_ids) + +class JiraGitHubCombinedReport(Report): + def __init__(self, jira_report, github_report, header='', print_comments=False): + Report.__init__(self, header) + self.jira_report = jira_report + self.github_report = github_report + self.print_comments = print_comments + + def _jira_comments(self, jira_id): + return None if jira_id is None else self.jira_report.issues[jira_id].get_comments() + + def _idx_1st_comment_with_vote(self): + g = 0 + for pull in self.github_report.pull_requests: + c = 0 + for comment in self._jira_comments(pull.jira_id()): + if comment.has_vote(): + return(g,) + (c,) + c += 1 + g += 1 + + def _pull_request(self, pull_idx): + pull = self.github_report.pull_requests[pull_idx] + return pull + + def _jira_id(self, pull_idx): + pull = self._pull_request(pull_idx) + return encode(pull.jira_id()) + + def _jira_issue(self, jira_id): + return self.jira_report.issues[jira_id] + + def _row_tuple(self, pull_idx): + pull = self._pull_request(pull_idx) + jira_id = self._jira_id(pull_idx) + jira_issue = self._jira_issue(jira_id) + + return (jira_id, mstr(pull), jira_issue.get_trimmed_summary(), + daydiff(Report.now, jira_issue.get_created()), + daydiff(Report.now, pull.created_at()), + daydiff(Report.now, jira_issue.get_updated()), + daydiff(Report.now, pull.updated_at()), + jira_issue.get_status(), pull.user()) + + def _row_tuple_1(self, pull_idx, comment_idx): + row_tuple_1 = None + jira_id = self._jira_id(pull_idx) + jira_comments = self._jira_comments(jira_id) + comment = jira_comments[comment_idx] + if comment.has_vote(): + row_tuple_1 = (comment.get_vote(), comment.get_author(), comment.get_pull(), + daydiff(Report.now, comment.get_created())) + + return row_tuple_1 + + # variables and method names ending with _1 correspond to the comments part + def print_report(self, print_comments=False): + print "%s (Count = %s) " % (self.header, len(self.github_report.pull_requests)) + + fields_tuple = ('JIRA ID', 'Pull Request', 'Jira Summary', 'JIRA Age', + 'Pull Age', 'JIRA Update Age', 'Pull Update Age (Days)', + 'JIRA Status', 'GitHub user') + row_tuple = self._row_tuple(0) + formatter = Formatter(fields_tuple, row_tuple) + self.print_(formatter, fields_tuple) + + row_tuple_1 = () + formatter_1 = Formatter() + + if print_comments or self.print_comments: + fields_tuple_1 = self._build_tuple(('Comment Vote', 'Comment Author', 'Pull URL', 'Comment Age'), '-\t\t') + row_tuple_1 = self._build_tuple(self._row_tuple_1(*self._idx_1st_comment_with_vote()), '-\t\t') + formatter_1 = Formatter(fields_tuple_1, row_tuple_1) + self.print_(formatter_1, fields_tuple_1) + print '' + + for p in range(0, len(self.github_report.pull_requests)): + row_tuple = self._row_tuple(p) + self.print_(formatter, row_tuple) + + if print_comments or self.print_comments: + has_vote = False + comments = self._jira_comments(self._jira_id(p)) + for c in range(len(comments)): # Check cleaner way + comment = comments[c] + if comment.has_vote(): + row_tuple_1 = self._build_tuple(self._row_tuple_1(p, c), '-\t\t') + if row_tuple_1 is not None: + self.print_(formatter_1, row_tuple_1) + has_vote = True + if has_vote: + print '' + + +class CompleteReport(Report): + def __init__(self, header=''): + Report.__init__(self, header) + self.jira_reports = [] + self.github_reports = [] + self.jira_github_combined_reports = [] + + def print_all(self): + if self.header is not '': + print self.header + + self._print_github_reports() + self._print_jira_github_combined_reports() + self._print_jira_reports() + + def _print_jira_reports(self): + for jira in self.jira_reports: + jira.print_report() + + def _print_github_reports(self): + for github in self.github_reports: + github.print_report() + + def _print_jira_github_combined_reports(self): + for jira_github_combined in self.jira_github_combined_reports: + jira_github_combined.print_report() diff --git a/dev-tools/report/report_builder.py b/dev-tools/report/report_builder.py new file mode 100644 index 00000000000..4b8a468ac06 --- /dev/null +++ b/dev-tools/report/report_builder.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from report import CompleteReport, GitHubReport, JiraReport, JiraGitHubCombinedReport + + +class ReportBuilder: + def __init__(self, jira_repo=None, github_repo=None): + self.jira_repo = jira_repo + self.github_repo = github_repo + + def build(self): + pass + + +class CompleteReportBuilder(ReportBuilder): + def __init__(self, jira_repo=None, github_repo=None): + ReportBuilder.__init__(self, jira_repo, github_repo) + self.report = CompleteReport() + self.build() + + def build(self): + # all open github pull requests + github_open = GitHubReport(self.github_repo.open_pulls("apache", "storm")) + github_bad_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH BAD OR CLOSED JIRA ID") + github_without_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITHOUT A JIRA ID") + github_unresolved_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH UNRESOLVED JIRA ID") + github_unresolved_jira_voted = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH VOTES FOR UNRESOLVED JIRAS") + github_open_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH OPEN JIRA ID") + github_unresolved_not_open_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH UNRESOLVED BUT NOT OPEN JIRA ID") + + # all unresolved JIRA issues + jira_unresolved = JiraReport(self.jira_repo.unresolved_jiras("STORM")) + jira_open = JiraReport(dict((x, y) for x, y in self.jira_repo.unresolved_jiras("STORM").items() if y.get_status() == 'Open')) + jira_in_progress = JiraReport(dict((x, y) for x, y in self.jira_repo.in_progress_jiras("STORM").items() if y.get_status() == 'In Progress'), + "\nIN PROGRESS JIRA ISSUES") + + for pull in github_open.pull_requests: + if pull.has_jira_id(): + pull_jira_id = pull.jira_id() + if pull_jira_id not in jira_unresolved.issues: + github_bad_jira.pull_requests.append(pull) + else: + github_unresolved_jira.pull_requests.append(pull) + if jira_unresolved.issues[pull_jira_id].has_voted_comment(): + github_unresolved_jira_voted.pull_requests.append(pull) + if pull_jira_id in jira_open.issues: + github_open_jira.pull_requests.append(pull) + else: + github_unresolved_not_open_jira.pull_requests.append(pull) + else: + github_without_jira.pull_requests.append(pull) + + jira_github_open = JiraGitHubCombinedReport(jira_open, github_open_jira, + "\nOPEN JIRA ISSUES THAT HAVE GITHUB PULL REQUESTS") + jira_github_unresolved_not_open = JiraGitHubCombinedReport(jira_unresolved, github_unresolved_not_open_jira, + "\nIN PROGRESS OR REOPENED JIRA ISSUES THAT HAVE GITHUB PULL REQUESTS") + jira_github_unresolved_voted = JiraGitHubCombinedReport(jira_unresolved, github_unresolved_jira_voted, + "\nGITHUB PULL REQUESTS WITH VOTES FOR UNRESOLVED JIRAS", True) + # jira_github_unresolved = JiraGitHubCombinedReport(jira_unresolved, github_unresolved_jira, + # "\nUnresolved JIRA issues with GitHub pull requests") + + jira_open_no_pull = JiraReport(jira_open.view(github_open_jira.jira_ids()), + "\nOPEN JIRA ISSUES THAT DON'T HAVE GITHUB PULL REQUESTS") + + # build complete report + self.report.jira_reports.append(jira_in_progress) + self.report.jira_reports.append(jira_open_no_pull) + + self.report.github_reports.append(github_bad_jira) + self.report.github_reports.append(github_without_jira) + + self.report.jira_github_combined_reports.append(jira_github_open) + self.report.jira_github_combined_reports.append(jira_github_unresolved_voted) + self.report.jira_github_combined_reports.append(jira_github_unresolved_not_open) + # self.report.jira_github_combined_reports.append(jira_github_unresolved) diff --git a/dev-tools/storm-merge.py b/dev-tools/storm-merge.py index 06ae25f8605..33690c216d6 100755 --- a/dev-tools/storm-merge.py +++ b/dev-tools/storm-merge.py @@ -24,7 +24,7 @@ def main(): for pullNumber in args: pull = github.pull("apache", "storm", pullNumber) - print "git pull "+pull.fromRepo()+" "+pull.fromBranch() + print "git pull --no-ff "+pull.from_repo()+" "+pull.from_branch() if __name__ == "__main__": main() diff --git a/dev-tools/test-ns.py b/dev-tools/test-ns.py index 2fd1421e071..c0749e83fe1 100755 --- a/dev-tools/test-ns.py +++ b/dev-tools/test-ns.py @@ -22,26 +22,7 @@ os.chdir("storm-core") ns = sys.argv[1] -pipe = Popen(["mvn", "clojure:repl"], stdin=PIPE) - - -pipe.stdin.write(""" -(do - (use 'clojure.test) - (require '%s :reload-all) - (let [results (run-tests '%s)] - (println results) - (if (or - (> (:fail results) 0) - (> (:error results) 0)) - (System/exit 1) - (System/exit 0)))) -""" % (ns, ns)) - - - -pipe.stdin.write("\n") -pipe.stdin.close() +pipe = Popen(["mvn", "test", "-DfailIfNoTests=false", "-Dtest=%s"%ns]) pipe.wait() os.chdir("..") diff --git a/dev-tools/travis/print-errors-from-clojure-test-reports.py b/dev-tools/travis/print-errors-from-clojure-test-reports.py deleted file mode 100644 index 1cb88fc403d..00000000000 --- a/dev-tools/travis/print-errors-from-clojure-test-reports.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import sys -import glob -from xml.etree.ElementTree import ElementTree - - -def print_detail_information(testcase, fail_or_error): - print "-" * 50 - print "classname: %s / testname: %s" % (testcase.get("classname"), testcase.get("name")) - print fail_or_error.text - print "-" * 50 - - -def print_error_reports_from_report_file(file_path): - tree = ElementTree() - tree.parse(file_path) - - testcases = tree.findall(".//testcase") - for testcase in testcases: - error = testcase.find("error") - if error is not None: - print_detail_information(testcase, error) - - fail = testcase.find("fail") - if fail is not None: - print_detail_information(testcase, fail) - - -def main(report_dir_path): - for test_report in glob.iglob(report_dir_path + '/*.xml'): - file_path = os.path.abspath(test_report) - try: - print_error_reports_from_report_file(file_path) - except Exception, e: - print "Error while reading report file, %s" % file_path - print "Exception: %s" % e - - -if __name__ == "__main__": - if sys.argv < 2: - print "Usage: %s [report dir path]" % sys.argv[0] - sys.exit(1) - - main(sys.argv[1]) \ No newline at end of file diff --git a/dev-tools/travis/print-errors-from-test-reports.py b/dev-tools/travis/print-errors-from-test-reports.py new file mode 100644 index 00000000000..72af6d5e19d --- /dev/null +++ b/dev-tools/travis/print-errors-from-test-reports.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import glob +import traceback +from xml.etree.ElementTree import ElementTree + +def print_detail_information(testcase, fail_or_error): + print "-" * 50 + print "classname: %s / testname: %s" % (testcase.get("classname"), testcase.get("name")) + print fail_or_error.text + stdout = testcase.find("system-out") + if stdout != None: + print "-" * 20, "system-out", "-"*20 + print stdout.text + stderr = testcase.find("system-err") + if stderr != None: + print "-" * 20, "system-err", "-"*20 + print stderr.text + print "-" * 50 + + +def print_error_reports_from_report_file(file_path): + tree = ElementTree() + try: + tree.parse(file_path) + except: + print "-" * 50 + print "Error parsing %s"%file_path + f = open(file_path, "r"); + print f.read(); + print "-" * 50 + return + + testcases = tree.findall(".//testcase") + for testcase in testcases: + error = testcase.find("error") + if error is not None: + print_detail_information(testcase, error) + + fail = testcase.find("fail") + if fail is not None: + print_detail_information(testcase, fail) + + failure = testcase.find("failure") + if failure is not None: + print_detail_information(testcase, failure) + + +def main(report_dir_path): + for test_report in glob.iglob(report_dir_path + '/*.xml'): + file_path = os.path.abspath(test_report) + try: + print "Checking %s" % test_report + print_error_reports_from_report_file(file_path) + except Exception, e: + print "Error while reading report file, %s" % file_path + print "Exception: %s" % e + traceback.print_exc() + + +if __name__ == "__main__": + if sys.argv < 2: + print "Usage: %s [report dir path]" % sys.argv[0] + sys.exit(1) + + main(sys.argv[1]) diff --git a/dev-tools/travis/ratprint.py b/dev-tools/travis/ratprint.py new file mode 100755 index 00000000000..5031045ab49 --- /dev/null +++ b/dev-tools/travis/ratprint.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +import re + +p = re.compile('Unapproved licenses:\s*([^\s\*]*).*\*\*\*') + +with open (sys.argv[1]) as ratfile: + rat = ratfile.read().replace('\n','') + +matches = p.search(rat) +failed = matches.group(1) + +if re.search('\S', failed): + print failed diff --git a/dev-tools/travis/save-logs.py b/dev-tools/travis/save-logs.py new file mode 100755 index 00000000000..5f4ad28bade --- /dev/null +++ b/dev-tools/travis/save-logs.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import subprocess +from datetime import datetime, timedelta + +def main(file, cmd): + print cmd, "writing to", file + out = open(file, "w") + count = 0 + process = subprocess.Popen(cmd, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE) + + start = datetime.now() + nextPrint = datetime.now() + timedelta(seconds=1) + # wait for the process to terminate + pout = process.stdout + line = pout.readline() + while line: + count = count + 1 + if datetime.now() > nextPrint: + diff = datetime.now() - start + sys.stdout.write("\r%d seconds %d log lines"%(diff.seconds, count)) + sys.stdout.flush() + nextPrint = datetime.now() + timedelta(seconds=10) + out.write(line) + line = pout.readline() + out.close() + errcode = process.wait() + diff = datetime.now() - start + sys.stdout.write("\r%d seconds %d log lines"%(diff.seconds, count)) + print + print cmd, "done", errcode + return errcode + +if __name__ == "__main__": + if sys.argv < 1: + print "Usage: %s [file info]" % sys.argv[0] + sys.exit(1) + + sys.exit(main(sys.argv[1], sys.argv[2:])) diff --git a/dev-tools/travis/travis-build.sh b/dev-tools/travis/travis-build.sh deleted file mode 100755 index a06151c327e..00000000000 --- a/dev-tools/travis/travis-build.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -PYTHON_VERSION_TO_FILE=`python -V > /tmp/python_version 2>&1` -PYTHON_VERSION=`cat /tmp/python_version` -RUBY_VERSION=`ruby -v` -NODEJS_VERSION=`node -v` - -echo "Python version : $PYTHON_VERSION" -echo "Ruby version : $RUBY_VERSION" -echo "NodeJs version : $NODEJS_VERSION" - - -STORM_SRC_ROOT_DIR=$1 - -TRAVIS_SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) - -cd ${STORM_SRC_ROOT_DIR} - -# Travis CI doesn't allow stdout bigger than 4M, so we have to reduce log while running tests -export LOG_LEVEL=WARN -# We should concern that Travis CI could be very slow cause it uses VM -export STORM_TEST_TIMEOUT_MS=100000 - -# We now lean on Travis CI's implicit behavior, ```mvn clean install -DskipTests``` before running script -mvn test -fae - -BUILD_RET_VAL=$? - -if [ ${BUILD_RET_VAL} -ne 0 ] -then - echo "There may be clojure test errors from storm-core, printing error reports..." - python ${TRAVIS_SCRIPT_DIR}/print-errors-from-clojure-test-reports.py ${STORM_SRC_ROOT_DIR}/storm-core/target/test-reports -else - echo "Double checking clojure test-report, Errors or Failures:" - egrep -il '&1` +echo "Maven version : " `mvn -v` + +STORM_SRC_ROOT_DIR=$1 + +TRAVIS_SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +cd ${STORM_SRC_ROOT_DIR} +python ${TRAVIS_SCRIPT_DIR}/save-logs.py "install.txt" mvn clean install -DskipTests -Pnative --batch-mode +BUILD_RET_VAL=$? + +if [[ "$BUILD_RET_VAL" != "0" ]]; +then + cat "install.txt" + echo "Looking for unapproved licenses" + for rat in `find . -name rat.txt`; + do + python ${TRAVIS_SCRIPT_DIR}/ratprint.py "${rat}" + done +fi + +exit ${BUILD_RET_VAL} diff --git a/dev-tools/travis/travis-script.sh b/dev-tools/travis/travis-script.sh new file mode 100755 index 00000000000..fb130426ef2 --- /dev/null +++ b/dev-tools/travis/travis-script.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +echo "Python version : " `python -V 2>&1` +echo "Ruby version : " `ruby -v` +echo "NodeJs version : " `node -v` +echo "Maven version : " `mvn -v` + +STORM_SRC_ROOT_DIR=$1 + +TRAVIS_SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +cd ${STORM_SRC_ROOT_DIR} + +if [ "$2" == "INTEGRATION-TEST" ] +then + /bin/bash ./integration-test/run-it.sh +else + # We should be concerned that Travis CI could be very slow because it uses VM + export STORM_TEST_TIMEOUT_MS=150000 + # Travis only has 3GB of memory, lets use 1GB for build, and 1.5GB for forked JVMs + export MAVEN_OPTS="-Xmx1024m" + + mvn --batch-mode test -fae -Pnative,all-tests -Prat -pl "$2" + BUILD_RET_VAL=$? + + for dir in `find . -type d -and -wholename \*/target/\*-reports`; + do + echo "Looking for errors in ${dir}" + python ${TRAVIS_SCRIPT_DIR}/print-errors-from-test-reports.py "${dir}" + done + + exit ${BUILD_RET_VAL} +fi + diff --git a/docs/Acking-framework-implementation.md b/docs/Acking-framework-implementation.md new file mode 100644 index 00000000000..5ca5d93df0d --- /dev/null +++ b/docs/Acking-framework-implementation.md @@ -0,0 +1,36 @@ +--- +layout: documentation +--- +[Storm's acker](https://github.com/apache/incubator-storm/blob/46c3ba7/storm-core/src/clj/backtype/storm/daemon/acker.clj#L28) tracks completion of each tupletree with a checksum hash: each time a tuple is sent, its value is XORed into the checksum, and each time a tuple is acked its value is XORed in again. If all tuples have been successfully acked, the checksum will be zero (the odds that the checksum will be zero otherwise are vanishingly small). + +You can read a bit more about the [reliability mechanism](Guaranteeing-message-processing.html#what-is-storms-reliability-api) elsewhere on the wiki -- this explains the internal details. + +### acker `execute()` + +The acker is actually a regular bolt, with its [execute method](https://github.com/apache/incubator-storm/blob/46c3ba7/storm-core/src/clj/backtype/storm/daemon/acker.clj#L36) defined withing `mk-acker-bolt`. When a new tupletree is born, the spout sends the XORed edge-ids of each tuple recipient, which the acker records in its `pending` ledger. Every time an executor acks a tuple, the acker receives a partial checksum that is the XOR of the tuple's own edge-id (clearing it from the ledger) and the edge-id of each downstream tuple the executor emitted (thus entering them into the ledger). + +This is accomplished as follows. + +On a tick tuple, just advance pending tupletree checksums towards death and return. Otherwise, update or create the record for this tupletree: + +* on init: initialize with the given checksum value, and record the spout's id for later. +* on ack: xor the partial checksum into the existing checksum value +* on fail: just mark it as failed + +Next, [put the record](https://github.com/apache/incubator-storm/blob/46c3ba7/storm-core/src/clj/backtype/storm/daemon/acker.clj#L50)), into the RotatingMap (thus resetting is countdown to expiry) and take action: + +* if the total checksum is zero, the tupletree is complete: remove it from the pending collection and notify the spout of success +* if the tupletree has failed, it is also complete: remove it from the pending collection and notify the spout of failure + +Finally, pass on an ack of our own. + +### Pending tuples and the `RotatingMap` + +The acker stores pending tuples in a [`RotatingMap`](https://github.com/apache/incubator-storm/blob/master/storm-core/src/jvm/backtype/storm/utils/RotatingMap.java#L19), a simple device used in several places within Storm to efficiently time-expire a process. + +The RotatingMap behaves as a HashMap, and offers the same O(1) access guarantees. + +Internally, it holds several HashMaps ('buckets') of its own, each holding a cohort of records that will expire at the same time. Let's call the longest-lived bucket death row, and the most recent the nursery. Whenever a value is `.put()` to the RotatingMap, it is relocated to the nursery -- and removed from any other bucket it might have been in (effectively resetting its death clock). + +Whenever its owner calls `.rotate()`, the RotatingMap advances each cohort one step further towards expiration. (Typically, Storm objects call rotate on every receipt of a system tick stream tuple.) If there are any key-value pairs in the former death row bucket, the RotatingMap invokes a callback (given in the constructor) for each key-value pair, letting its owner take appropriate action (eg, failing a tuple. + diff --git a/docs/documentation/Clojure-DSL.md b/docs/Clojure-DSL.md similarity index 92% rename from docs/documentation/Clojure-DSL.md rename to docs/Clojure-DSL.md index fcbbce4df9f..e71243c5be9 100644 --- a/docs/documentation/Clojure-DSL.md +++ b/docs/Clojure-DSL.md @@ -3,7 +3,7 @@ title: Clojure DSL layout: documentation documentation: true --- -Storm comes with a Clojure DSL for defining spouts, bolts, and topologies. The Clojure DSL has access to everything the Java API exposes, so if you're a Clojure user you can code Storm topologies without touching Java at all. The Clojure DSL is defined in the source in the [backtype.storm.clojure](https://github.com/apache/storm/blob/0.5.3/src/clj/backtype/storm/clojure.clj) namespace. +Storm comes with a Clojure DSL for defining spouts, bolts, and topologies. The Clojure DSL has access to everything the Java API exposes, so if you're a Clojure user you can code Storm topologies without touching Java at all. The Clojure DSL is defined in the source in the [org.apache.storm.clojure]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/clojure.clj) namespace. This page outlines all the pieces of the Clojure DSL, including: @@ -17,7 +17,7 @@ This page outlines all the pieces of the Clojure DSL, including: To define a topology, use the `topology` function. `topology` takes in two arguments: a map of "spout specs" and a map of "bolt specs". Each spout and bolt spec wires the code for the component into the topology by specifying things like inputs and parallelism. -Let's take a look at an example topology definition [from the storm-starter project](https://github.com/apache/storm/blob/master/examples/storm-starter/src/clj/storm/starter/clj/word_count.clj): +Let's take a look at an example topology definition [from the storm-starter project]({{page.git-blob-base}}/examples/storm-starter/src/clj/org/apache/storm/starter/clj/word_count.clj): ```clojure (topology @@ -38,11 +38,11 @@ The maps of spout and bolt specs are maps from the component id to the correspon #### spout-spec -`spout-spec` takes as arguments the spout implementation (an object that implements [IRichSpout](/javadoc/apidocs/backtype/storm/topology/IRichSpout.html)) and optional keyword arguments. The only option that exists currently is the `:p` option, which specifies the parallelism for the spout. If you omit `:p`, the spout will execute as a single task. +`spout-spec` takes as arguments the spout implementation (an object that implements [IRichSpout](javadocs/org/apache/storm/topology/IRichSpout.html)) and optional keyword arguments. The only option that exists currently is the `:p` option, which specifies the parallelism for the spout. If you omit `:p`, the spout will execute as a single task. #### bolt-spec -`bolt-spec` takes as arguments the input declaration for the bolt, the bolt implementation (an object that implements [IRichBolt](/javadoc/apidocs/backtype/storm/topology/IRichBolt.html)), and optional keyword arguments. +`bolt-spec` takes as arguments the input declaration for the bolt, the bolt implementation (an object that implements [IRichBolt](javadocs/org/apache/storm/topology/IRichBolt.html)), and optional keyword arguments. The input declaration is a map from stream ids to stream groupings. A stream id can have one of two forms: @@ -203,7 +203,7 @@ The signature for `defspout` looks like the following: If you leave out the option map, it defaults to {:prepare true}. The output declaration for `defspout` has the same syntax as `defbolt`. -Here's an example `defspout` implementation from [storm-starter](https://github.com/apache/storm/blob/master/examples/storm-starter/src/clj/storm/starter/clj/word_count.clj): +Here's an example `defspout` implementation from [storm-starter]({{page.git-blob-base}}/examples/storm-starter/src/clj/org/apache/storm/starter/clj/word_count.clj): ```clojure (defspout sentence-spout ["sentence"] @@ -254,7 +254,7 @@ The following example illustrates how to use this spout in a `spout-spec`: That's all there is to the Clojure DSL. To submit topologies in remote mode or local mode, just use the `StormSubmitter` or `LocalCluster` classes just like you would from Java. -To create topology configs, it's easiest to use the `backtype.storm.config` namespace which defines constants for all of the possible configs. The constants are the same as the static constants in the `Config` class, except with dashes instead of underscores. For example, here's a topology config that sets the number of workers to 15 and configures the topology in debug mode: +To create topology configs, it's easiest to use the `org.apache.storm.config` namespace which defines constants for all of the possible configs. The constants are the same as the static constants in the `Config` class, except with dashes instead of underscores. For example, here's a topology config that sets the number of workers to 15 and configures the topology in debug mode: ```clojure {TOPOLOGY-DEBUG true diff --git a/docs/Command-line-client.md b/docs/Command-line-client.md new file mode 100644 index 00000000000..363d916aacb --- /dev/null +++ b/docs/Command-line-client.md @@ -0,0 +1,281 @@ +--- +title: Command Line Client +layout: documentation +documentation: true +--- +This page describes all the commands that are possible with the "storm" command line client. To learn how to set up your "storm" client to talk to a remote cluster, follow the instructions in [Setting up development environment](Setting-up-development-environment.html). + +These commands are: + +1. jar +1. sql +1. kill +1. activate +1. deactivate +1. rebalance +1. repl +1. classpath +1. localconfvalue +1. remoteconfvalue +1. nimbus +1. supervisor +1. ui +1. drpc +1. blobstore +1. dev-zookeeper +1. get-errors +1. heartbeats +1. kill_workers +1. list +1. logviewer +1. monitor +1. node-health-check +1. pacemaker +1. set_log_level +1. shell +1. upload-credentials +1. version +1. help + +### jar + +Syntax: `storm jar topology-jar-path class ...` + +Runs the main method of `class` with the specified arguments. The storm jars and configs in `~/.storm` are put on the classpath. The process is configured so that [StormSubmitter](javadocs/org/apache/storm/StormSubmitter.html) will upload the jar at `topology-jar-path` when the topology is submitted. + +When you want to ship other jars which is not included to application jar, you can pass them to `--jars` option with comma-separated string. +For example, --jars "your-local-jar.jar,your-local-jar2.jar" will load your-local-jar.jar and your-local-jar2.jar. +And when you want to ship maven artifacts and its transitive dependencies, you can pass them to `--artifacts` with comma-separated string. You can also exclude some dependencies like what you're doing in maven pom. Please add exclusion artifacts with '^' separated string after the artifact. For example, `--artifacts "redis.clients:jedis:2.9.0,org.apache.kafka:kafka_2.10:0.8.2.2^org.slf4j:slf4j-log4j12"` will load jedis and kafka artifact and all of transitive dependencies but exclude slf4j-log4j12 from kafka. + +When you need to pull the artifacts from other than Maven Central, you can pass remote repositories to --artifactRepositories option with comma-separated string. Repository format is "^". '^' is taken as separator because URL allows various characters. For example, --artifactRepositories "jboss-repository^http://repository.jboss.com/maven2,HDPRepo^http://repo.hortonworks.com/content/groups/public/" will add JBoss and HDP repositories for dependency resolver. + +Complete example of both options is here: `./bin/storm jar example/storm-starter/storm-starter-topologies-*.jar org.apache.storm.starter.RollingTopWords blobstore-remote2 remote --jars "./external/storm-redis/storm-redis-1.1.0.jar,./external/storm-kafka/storm-kafka-1.1.0.jar" --artifacts "redis.clients:jedis:2.9.0,org.apache.kafka:kafka_2.10:0.8.2.2^org.slf4j:slf4j-log4j12" --artifactRepositories "jboss-repository^http://repository.jboss.com/maven2,HDPRepo^http://repo.hortonworks.com/content/groups/public/"` + +When you pass jars and/or artifacts options, StormSubmitter will upload them when the topology is submitted, and they will be included to classpath of both the process which runs the class, and also workers for that topology. + +### sql + +Syntax: `storm sql sql-file topology-name` + +Compiles the SQL statements into a Trident topology and submits it to Storm. + +`--jars` and `--artifacts`, and `--artifactRepositories` options available for jar are also applied to sql command. Please refer "help jar" to see how to use --jars and --artifacts, and --artifactRepositories options. You normally want to pass these options since you need to set data source to your sql which is an external storage in many cases. + +### kill + +Syntax: `storm kill topology-name [-w wait-time-secs]` + +Kills the topology with the name `topology-name`. Storm will first deactivate the topology's spouts for the duration of the topology's message timeout to allow all messages currently being processed to finish processing. Storm will then shutdown the workers and clean up their state. You can override the length of time Storm waits between deactivation and shutdown with the -w flag. + +### activate + +Syntax: `storm activate topology-name` + +Activates the specified topology's spouts. + +### deactivate + +Syntax: `storm deactivate topology-name` + +Deactivates the specified topology's spouts. + +### rebalance + +Syntax: `storm rebalance topology-name [-w wait-time-secs] [-n new-num-workers] [-e component=parallelism]*` + +Sometimes you may wish to spread out where the workers for a topology are running. For example, let's say you have a 10 node cluster running 4 workers per node, and then let's say you add another 10 nodes to the cluster. You may wish to have Storm spread out the workers for the running topology so that each node runs 2 workers. One way to do this is to kill the topology and resubmit it, but Storm provides a "rebalance" command that provides an easier way to do this. + +Rebalance will first deactivate the topology for the duration of the message timeout (overridable with the -w flag) and then redistribute the workers evenly around the cluster. The topology will then return to its previous state of activation (so a deactivated topology will still be deactivated and an activated topology will go back to being activated). + +The rebalance command can also be used to change the parallelism of a running topology. Use the -n and -e switches to change the number of workers or number of executors of a component respectively. + +### repl + +Syntax: `storm repl` + +Opens up a Clojure REPL with the storm jars and configuration on the classpath. Useful for debugging. + +### classpath + +Syntax: `storm classpath` + +Prints the classpath used by the storm client when running commands. + +### localconfvalue + +Syntax: `storm localconfvalue conf-name` + +Prints out the value for `conf-name` in the local Storm configs. The local Storm configs are the ones in `~/.storm/storm.yaml` merged in with the configs in `defaults.yaml`. + +### remoteconfvalue + +Syntax: `storm remoteconfvalue conf-name` + +Prints out the value for `conf-name` in the cluster's Storm configs. The cluster's Storm configs are the ones in `$STORM-PATH/conf/storm.yaml` merged in with the configs in `defaults.yaml`. This command must be run on a cluster machine. + +### nimbus + +Syntax: `storm nimbus` + +Launches the nimbus daemon. This command should be run under supervision with a tool like [daemontools](http://cr.yp.to/daemontools.html) or [monit](http://mmonit.com/monit/). See [Setting up a Storm cluster](Setting-up-a-Storm-cluster.html) for more information. + +### supervisor + +Syntax: `storm supervisor` + +Launches the supervisor daemon. This command should be run under supervision with a tool like [daemontools](http://cr.yp.to/daemontools.html) or [monit](http://mmonit.com/monit/). See [Setting up a Storm cluster](Setting-up-a-Storm-cluster.html) for more information. + +### ui + +Syntax: `storm ui` + +Launches the UI daemon. The UI provides a web interface for a Storm cluster and shows detailed stats about running topologies. This command should be run under supervision with a tool like [daemontools](http://cr.yp.to/daemontools.html) or [monit](http://mmonit.com/monit/). See [Setting up a Storm cluster](Setting-up-a-Storm-cluster.html) for more information. + +### drpc + +Syntax: `storm drpc` + +Launches a DRPC daemon. This command should be run under supervision with a tool like [daemontools](http://cr.yp.to/daemontools.html) or [monit](http://mmonit.com/monit/). See [Distributed RPC](Distributed-RPC.html) for more information. + +### blobstore + +Syntax: `storm blobstore cmd` + +list [KEY...] - lists blobs currently in the blob store + +cat [-f FILE] KEY - read a blob and then either write it to a file, or STDOUT (requires read access). + +create [-f FILE] [-a ACL ...] [--replication-factor NUMBER] KEY - create a new blob. Contents comes from a FILE or STDIN. ACL is in the form [uo]:[username]:[r-][w-][a-] can be comma separated list. + +update [-f FILE] KEY - update the contents of a blob. Contents comes from a FILE or STDIN (requires write access). + +delete KEY - delete an entry from the blob store (requires write access). + +set-acl [-s ACL] KEY - ACL is in the form [uo]:[username]:[r-][w-][a-] can be comma separated list (requires admin access). + +replication --read KEY - Used to read the replication factor of the blob. + +replication --update --replication-factor NUMBER KEY where NUMBER > 0. It is used to update the replication factor of a blob. + +For example, the following would create a mytopo:data.tgz key using the data stored in data.tgz. User alice would have full access, bob would have read/write access and everyone else would have read access. + +storm blobstore create mytopo:data.tgz -f data.tgz -a u:alice:rwa,u:bob:rw,o::r + +See [Blobstore(Distcahce)](distcache-blobstore.html) for more information. + +### dev-zookeeper + +Syntax: `storm dev-zookeeper` + +Launches a fresh Zookeeper server using "dev.zookeeper.path" as its local dir and "storm.zookeeper.port" as its port. This is only intended for development/testing, the Zookeeper instance launched is not configured to be used in production. + +### get-errors + +Syntax: `storm get-errors topology-name` + +Get the latest error from the running topology. The returned result contains the key value pairs for component-name and component-error for the components in error. The result is returned in json format. + +### heartbeats + +Syntax: `storm heartbeats [cmd]` + +list PATH - lists heartbeats nodes under PATH currently in the ClusterState. +get PATH - Get the heartbeat data at PATH + +### kill_workers + +Syntax: `storm kill_workers` + +Kill the workers running on this supervisor. This command should be run on a supervisor node. If the cluster is running in secure mode, then user needs to have admin rights on the node to be able to successfully kill all workers. + +### list + +Syntax: `storm list` + +List the running topologies and their statuses. + +### logviewer + +Syntax: `storm logviewer` + +Launches the log viewer daemon. It provides a web interface for viewing storm log files. This command should be run under supervision with a tool like daemontools or monit. + +See Setting up a Storm cluster for more information.(http://storm.apache.org/documentation/Setting-up-a-Storm-cluster) + +### monitor + +Syntax: `storm monitor topology-name [-i interval-secs] [-m component-id] [-s stream-id] [-w [emitted | transferred]]` + +Monitor given topology's throughput interactively. +One can specify poll-interval, component-id, stream-id, watch-item[emitted | transferred] + By default, + poll-interval is 4 seconds; + all component-ids will be list; + stream-id is 'default'; + watch-item is 'emitted'; + +### node-health-check + +Syntax: `storm node-health-check` + +Run health checks on the local supervisor. + +### pacemaker + +Syntax: `storm pacemaker` + +Launches the Pacemaker daemon. This command should be run under +supervision with a tool like daemontools or monit. + +See Setting up a Storm cluster for more information.(http://storm.apache.org/documentation/Setting-up-a-Storm-cluster) + +### set_log_level + +Syntax: `storm set_log_level -l [logger name]=[log level][:optional timeout] -r [logger name] topology-name` + +Dynamically change topology log levels + +where log level is one of: ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF +and timeout is integer seconds. + +e.g. + ./bin/storm set_log_level -l ROOT=DEBUG:30 topology-name + + Set the root logger's level to DEBUG for 30 seconds + + ./bin/storm set_log_level -l com.myapp=WARN topology-name + + Set the com.myapp logger's level to WARN for 30 seconds + + ./bin/storm set_log_level -l com.myapp=WARN -l com.myOtherLogger=ERROR:123 topology-name + + Set the com.myapp logger's level to WARN indifinitely, and com.myOtherLogger to ERROR for 123 seconds + + ./bin/storm set_log_level -r com.myOtherLogger topology-name + + Clears settings, resetting back to the original level + +### shell + +Syntax: `storm shell resourcesdir command args` + +Makes constructing jar and uploading to nimbus for using non JVM languages + +eg: `storm shell resources/ python topology.py arg1 arg2` + +### upload-credentials + +Syntax: `storm upload_credentials topology-name [credkey credvalue]*` + +Uploads a new set of credentials to a running topology + +### version + +Syntax: `storm version` + +Prints the version number of this Storm release. + +### help +Syntax: `storm help [command]` + +Print one help message or list of available commands diff --git a/docs/Common-patterns.md b/docs/Common-patterns.md new file mode 100644 index 00000000000..e0e8c1f665e --- /dev/null +++ b/docs/Common-patterns.md @@ -0,0 +1,84 @@ +--- +title: Common Topology Patterns +layout: documentation +documentation: true +--- + +This page lists a variety of common patterns in Storm topologies. + +1. Batching +2. BasicBolt +3. In-memory caching + fields grouping combo +4. Streaming top N +5. TimeCacheMap for efficiently keeping a cache of things that have been recently updated +6. CoordinatedBolt and KeyedFairBolt for Distributed RPC + + +### Batching + +Oftentimes for efficiency reasons or otherwise, you want to process a group of tuples in batch rather than individually. For example, you may want to batch updates to a database or do a streaming aggregation of some sort. + +If you want reliability in your data processing, the right way to do this is to hold on to tuples in an instance variable while the bolt waits to do the batching. Once you do the batch operation, you then ack all the tuples you were holding onto. + +If the bolt emits tuples, then you may want to use multi-anchoring to ensure reliability. It all depends on the specific application. See [Guaranteeing message processing](Guaranteeing-message-processing.html) for more details on how reliability works. + +### BasicBolt +Many bolts follow a similar pattern of reading an input tuple, emitting zero or more tuples based on that input tuple, and then acking that input tuple immediately at the end of the execute method. Bolts that match this pattern are things like functions and filters. This is such a common pattern that Storm exposes an interface called [IBasicBolt](javadocs/org/apache/storm/topology/IBasicBolt.html) that automates this pattern for you. See [Guaranteeing message processing](Guaranteeing-message-processing.html) for more information. + +### In-memory caching + fields grouping combo + +It's common to keep caches in-memory in Storm bolts. Caching becomes particularly powerful when you combine it with a fields grouping. For example, suppose you have a bolt that expands short URLs (like bit.ly, t.co, etc.) into long URLs. You can increase performance by keeping an LRU cache of short URL to long URL expansions to avoid doing the same HTTP requests over and over. Suppose component "urls" emits short URLS, and component "expand" expands short URLs into long URLs and keeps a cache internally. Consider the difference between the two following snippets of code: + +```java +builder.setBolt("expand", new ExpandUrl(), parallelism) + .shuffleGrouping(1); +``` + +```java +builder.setBolt("expand", new ExpandUrl(), parallelism) + .fieldsGrouping("urls", new Fields("url")); +``` + +The second approach will have vastly more effective caches, since the same URL will always go to the same task. This avoids having duplication across any of the caches in the tasks and makes it much more likely that a short URL will hit the cache. + +### Streaming top N + +A common continuous computation done on Storm is a "streaming top N" of some sort. Suppose you have a bolt that emits tuples of the form ["value", "count"] and you want a bolt that emits the top N tuples based on count. The simplest way to do this is to have a bolt that does a global grouping on the stream and maintains a list in memory of the top N items. + +This approach obviously doesn't scale to large streams since the entire stream has to go through one task. A better way to do the computation is to do many top N's in parallel across partitions of the stream, and then merge those top N's together to get the global top N. The pattern looks like this: + +```java +builder.setBolt("rank", new RankObjects(), parallelism) + .fieldsGrouping("objects", new Fields("value")); +builder.setBolt("merge", new MergeObjects()) + .globalGrouping("rank"); +``` + +This pattern works because of the fields grouping done by the first bolt which gives the partitioning you need for this to be semantically correct. You can see an example of this pattern in storm-starter [here]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/RollingTopWords.java). + +If however you have a known skew in the data being processed it can be advantageous to use partialKeyGrouping instead of fieldsGrouping. This will distribute the load for each key between two downstream bolts instead of a single one. + +```java +builder.setBolt("count", new CountObjects(), parallelism) + .partialKeyGrouping("objects", new Fields("value")); +builder.setBolt("rank" new AggregateCountsAndRank(), parallelism) + .fieldsGrouping("count", new Fields("key")) +builder.setBolt("merge", new MergeRanksObjects()) + .globalGrouping("rank"); +``` + +The topology needs an extra layer of processing to aggregate the partial counts from the upstream bolts but this only processes aggregated values now so the bolt it is not subject to the load caused by the skewed data. You can see an example of this pattern in storm-starter [here]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/SkewedRollingTopWords.java). + +### TimeCacheMap for efficiently keeping a cache of things that have been recently updated + +You sometimes want to keep a cache in memory of items that have been recently "active" and have items that have been inactive for some time be automatically expires. [TimeCacheMap](javadocs/org/apache/storm/utils/TimeCacheMap.html) is an efficient data structure for doing this and provides hooks so you can insert callbacks whenever an item is expired. + +### CoordinatedBolt and KeyedFairBolt for Distributed RPC + +When building distributed RPC applications on top of Storm, there are two common patterns that are usually needed. These are encapsulated by [CoordinatedBolt](javadocs/org/apache/storm/task/CoordinatedBolt.html) and [KeyedFairBolt](javadocs/org/apache/storm/task/KeyedFairBolt.html) which are part of the "standard library" that ships with the Storm codebase. + +`CoordinatedBolt` wraps the bolt containing your logic and figures out when your bolt has received all the tuples for any given request. It makes heavy use of direct streams to do this. + +`KeyedFairBolt` also wraps the bolt containing your logic and makes sure your topology processes multiple DRPC invocations at the same time, instead of doing them serially one at a time. + +See [Distributed RPC](Distributed-RPC.html) for more details. diff --git a/docs/Concepts.md b/docs/Concepts.md new file mode 100644 index 00000000000..d46033cf9b5 --- /dev/null +++ b/docs/Concepts.md @@ -0,0 +1,115 @@ +--- +title: Concepts +layout: documentation +documentation: true +--- + +This page lists the main concepts of Storm and links to resources where you can find more information. The concepts discussed are: + +1. Topologies +2. Streams +3. Spouts +4. Bolts +5. Stream groupings +6. Reliability +7. Tasks +8. Workers + +### Topologies + +The logic for a realtime application is packaged into a Storm topology. A Storm topology is analogous to a MapReduce job. One key difference is that a MapReduce job eventually finishes, whereas a topology runs forever (or until you kill it, of course). A topology is a graph of spouts and bolts that are connected with stream groupings. These concepts are described below. + +**Resources:** + +* [TopologyBuilder](javadocs/org/apache/storm/topology/TopologyBuilder.html): use this class to construct topologies in Java +* [Running topologies on a production cluster](Running-topologies-on-a-production-cluster.html) +* [Local mode](Local-mode.html): Read this to learn how to develop and test topologies in local mode. + +### Streams + +The stream is the core abstraction in Storm. A stream is an unbounded sequence of tuples that is processed and created in parallel in a distributed fashion. Streams are defined with a schema that names the fields in the stream's tuples. By default, tuples can contain integers, longs, shorts, bytes, strings, doubles, floats, booleans, and byte arrays. You can also define your own serializers so that custom types can be used natively within tuples. + +Every stream is given an id when declared. Since single-stream spouts and bolts are so common, [OutputFieldsDeclarer](javadocs/org/apache/storm/topology/OutputFieldsDeclarer.html) has convenience methods for declaring a single stream without specifying an id. In this case, the stream is given the default id of "default". + + +**Resources:** + +* [Tuple](javadocs/org/apache/storm/tuple/Tuple.html): streams are composed of tuples +* [OutputFieldsDeclarer](javadocs/org/apache/storm/topology/OutputFieldsDeclarer.html): used to declare streams and their schemas +* [Serialization](Serialization.html): Information about Storm's dynamic typing of tuples and declaring custom serializations + +### Spouts + +A spout is a source of streams in a topology. Generally spouts will read tuples from an external source and emit them into the topology (e.g. a Kestrel queue or the Twitter API). Spouts can either be __reliable__ or __unreliable__. A reliable spout is capable of replaying a tuple if it failed to be processed by Storm, whereas an unreliable spout forgets about the tuple as soon as it is emitted. + +Spouts can emit more than one stream. To do so, declare multiple streams using the `declareStream` method of [OutputFieldsDeclarer](javadocs/org/apache/storm/topology/OutputFieldsDeclarer.html) and specify the stream to emit to when using the `emit` method on [SpoutOutputCollector](javadocs/org/apache/storm/spout/SpoutOutputCollector.html). + +The main method on spouts is `nextTuple`. `nextTuple` either emits a new tuple into the topology or simply returns if there are no new tuples to emit. It is imperative that `nextTuple` does not block for any spout implementation, because Storm calls all the spout methods on the same thread. + +The other main methods on spouts are `ack` and `fail`. These are called when Storm detects that a tuple emitted from the spout either successfully completed through the topology or failed to be completed. `ack` and `fail` are only called for reliable spouts. See [the Javadoc](javadocs/org/apache/storm/spout/ISpout.html) for more information. + +**Resources:** + +* [IRichSpout](javadocs/org/apache/storm/topology/IRichSpout.html): this is the interface that spouts must implement. +* [Guaranteeing message processing](Guaranteeing-message-processing.html) + +### Bolts + +All processing in topologies is done in bolts. Bolts can do anything from filtering, functions, aggregations, joins, talking to databases, and more. + +Bolts can do simple stream transformations. Doing complex stream transformations often requires multiple steps and thus multiple bolts. For example, transforming a stream of tweets into a stream of trending images requires at least two steps: a bolt to do a rolling count of retweets for each image, and one or more bolts to stream out the top X images (you can do this particular stream transformation in a more scalable way with three bolts than with two). + +Bolts can emit more than one stream. To do so, declare multiple streams using the `declareStream` method of [OutputFieldsDeclarer](javadocs/org/apache/storm/topology/OutputFieldsDeclarer.html) and specify the stream to emit to when using the `emit` method on [OutputCollector](javadocs/org/apache/storm/task/OutputCollector.html). + +When you declare a bolt's input streams, you always subscribe to specific streams of another component. If you want to subscribe to all the streams of another component, you have to subscribe to each one individually. [InputDeclarer](javadocs/org/apache/storm/topology/InputDeclarer.html) has syntactic sugar for subscribing to streams declared on the default stream id. Saying `declarer.shuffleGrouping("1")` subscribes to the default stream on component "1" and is equivalent to `declarer.shuffleGrouping("1", DEFAULT_STREAM_ID)`. + +The main method in bolts is the `execute` method which takes in as input a new tuple. Bolts emit new tuples using the [OutputCollector](javadocs/org/apache/storm/task/OutputCollector.html) object. Bolts must call the `ack` method on the `OutputCollector` for every tuple they process so that Storm knows when tuples are completed (and can eventually determine that its safe to ack the original spout tuples). For the common case of processing an input tuple, emitting 0 or more tuples based on that tuple, and then acking the input tuple, Storm provides an [IBasicBolt](javadocs/org/apache/storm/topology/IBasicBolt.html) interface which does the acking automatically. + +Its perfectly fine to launch new threads in bolts that do processing asynchronously. [OutputCollector](javadocs/org/apache/storm/task/OutputCollector.html) is thread-safe and can be called at any time. + +**Resources:** + +* [IRichBolt](javadocs/org/apache/storm/topology/IRichBolt.html): this is general interface for bolts. +* [IBasicBolt](javadocs/org/apache/storm/topology/IBasicBolt.html): this is a convenience interface for defining bolts that do filtering or simple functions. +* [OutputCollector](javadocs/org/apache/storm/task/OutputCollector.html): bolts emit tuples to their output streams using an instance of this class +* [Guaranteeing message processing](Guaranteeing-message-processing.html) + +### Stream groupings + +Part of defining a topology is specifying for each bolt which streams it should receive as input. A stream grouping defines how that stream should be partitioned among the bolt's tasks. + +There are eight built-in stream groupings in Storm, and you can implement a custom stream grouping by implementing the [CustomStreamGrouping](javadocs/org/apache/storm/grouping/CustomStreamGrouping.html) interface: + +1. **Shuffle grouping**: Tuples are randomly distributed across the bolt's tasks in a way such that each bolt is guaranteed to get an equal number of tuples. +2. **Fields grouping**: The stream is partitioned by the fields specified in the grouping. For example, if the stream is grouped by the "user-id" field, tuples with the same "user-id" will always go to the same task, but tuples with different "user-id"'s may go to different tasks. +3. **Partial Key grouping**: The stream is partitioned by the fields specified in the grouping, like the Fields grouping, but are load balanced between two downstream bolts, which provides better utilization of resources when the incoming data is skewed. [This paper](https://melmeric.files.wordpress.com/2014/11/the-power-of-both-choices-practical-load-balancing-for-distributed-stream-processing-engines.pdf) provides a good explanation of how it works and the advantages it provides. +4. **All grouping**: The stream is replicated across all the bolt's tasks. Use this grouping with care. +5. **Global grouping**: The entire stream goes to a single one of the bolt's tasks. Specifically, it goes to the task with the lowest id. +6. **None grouping**: This grouping specifies that you don't care how the stream is grouped. Currently, none groupings are equivalent to shuffle groupings. Eventually though, Storm will push down bolts with none groupings to execute in the same thread as the bolt or spout they subscribe from (when possible). +7. **Direct grouping**: This is a special kind of grouping. A stream grouped this way means that the __producer__ of the tuple decides which task of the consumer will receive this tuple. Direct groupings can only be declared on streams that have been declared as direct streams. Tuples emitted to a direct stream must be emitted using one of the [emitDirect](javadocs/org/apache/storm/task/OutputCollector.html#emitDirect(int, int, java.util.List) methods. A bolt can get the task ids of its consumers by either using the provided [TopologyContext](javadocs/org/apache/storm/task/TopologyContext.html) or by keeping track of the output of the `emit` method in [OutputCollector](javadocs/org/apache/storm/task/OutputCollector.html) (which returns the task ids that the tuple was sent to). +8. **Local or shuffle grouping**: If the target bolt has one or more tasks in the same worker process, tuples will be shuffled to just those in-process tasks. Otherwise, this acts like a normal shuffle grouping. + +**Resources:** + +* [TopologyBuilder](javadocs/org/apache/storm/topology/TopologyBuilder.html): use this class to define topologies +* [InputDeclarer](javadocs/org/apache/storm/topology/InputDeclarer.html): this object is returned whenever `setBolt` is called on `TopologyBuilder` and is used for declaring a bolt's input streams and how those streams should be grouped + +### Reliability + +Storm guarantees that every spout tuple will be fully processed by the topology. It does this by tracking the tree of tuples triggered by every spout tuple and determining when that tree of tuples has been successfully completed. Every topology has a "message timeout" associated with it. If Storm fails to detect that a spout tuple has been completed within that timeout, then it fails the tuple and replays it later. + +To take advantage of Storm's reliability capabilities, you must tell Storm when new edges in a tuple tree are being created and tell Storm whenever you've finished processing an individual tuple. These are done using the [OutputCollector](javadocs/org/apache/storm/task/OutputCollector.html) object that bolts use to emit tuples. Anchoring is done in the `emit` method, and you declare that you're finished with a tuple using the `ack` method. + +This is all explained in much more detail in [Guaranteeing message processing](Guaranteeing-message-processing.html). + +### Tasks + +Each spout or bolt executes as many tasks across the cluster. Each task corresponds to one thread of execution, and stream groupings define how to send tuples from one set of tasks to another set of tasks. You set the parallelism for each spout or bolt in the `setSpout` and `setBolt` methods of [TopologyBuilder](javadocs/org/apache/storm/topology/TopologyBuilder.html). + +### Workers + +Topologies execute across one or more worker processes. Each worker process is a physical JVM and executes a subset of all the tasks for the topology. For example, if the combined parallelism of the topology is 300 and 50 workers are allocated, then each worker will execute 6 tasks (as threads within the worker). Storm tries to spread the tasks evenly across all the workers. + +**Resources:** + +* [Config.TOPOLOGY_WORKERS](javadocs/org/apache/storm/Config.html#TOPOLOGY_WORKERS): this config sets the number of workers to allocate for executing the topology diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 00000000000..979ac9a378a --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,31 @@ +--- +title: Configuration +layout: documentation +documentation: true +--- +Storm has a variety of configurations for tweaking the behavior of nimbus, supervisors, and running topologies. Some configurations are system configurations and cannot be modified on a topology by topology basis, whereas other configurations can be modified per topology. + +Every configuration has a default value defined in [defaults.yaml]({{page.git-blob-base}}/conf/defaults.yaml) in the Storm codebase. You can override these configurations by defining a storm.yaml in the classpath of Nimbus and the supervisors. Finally, you can define a topology-specific configuration that you submit along with your topology when using [StormSubmitter](javadocs/org/apache/storm/StormSubmitter.html). However, the topology-specific configuration can only override configs prefixed with "TOPOLOGY". + +Storm 0.7.0 and onwards lets you override configuration on a per-bolt/per-spout basis. The only configurations that can be overriden this way are: + +1. "topology.debug" +2. "topology.max.spout.pending" +3. "topology.max.task.parallelism" +4. "topology.kryo.register": This works a little bit differently than the other ones, since the serializations will be available to all components in the topology. More details on [Serialization](Serialization.html). + +The Java API lets you specify component specific configurations in two ways: + +1. *Internally:* Override `getComponentConfiguration` in any spout or bolt and return the component-specific configuration map. +2. *Externally:* `setSpout` and `setBolt` in `TopologyBuilder` return an object with methods `addConfiguration` and `addConfigurations` that can be used to override the configurations for the component. + +The preference order for configuration values is defaults.yaml < storm.yaml < topology specific configuration < internal component specific configuration < external component specific configuration. + + +**Resources:** + +* [Config](javadocs/org/apache/storm/Config.html): a listing of all configurations as well as a helper class for creating topology specific configurations +* [defaults.yaml]({{page.git-blob-base}}/conf/defaults.yaml): the default values for all configurations +* [Setting up a Storm cluster](Setting-up-a-Storm-cluster.html): explains how to create and configure a Storm cluster +* [Running topologies on a production cluster](Running-topologies-on-a-production-cluster.html): lists useful configurations when running topologies on a cluster +* [Local mode](Local-mode.html): lists useful configurations when using local mode diff --git a/docs/documentation/Contributing-to-Storm.md b/docs/Contributing-to-Storm.md similarity index 100% rename from docs/documentation/Contributing-to-Storm.md rename to docs/Contributing-to-Storm.md diff --git a/docs/documentation/Creating-a-new-Storm-project.md b/docs/Creating-a-new-Storm-project.md similarity index 78% rename from docs/documentation/Creating-a-new-Storm-project.md rename to docs/Creating-a-new-Storm-project.md index b6186b5f4ab..35ab1eba1eb 100644 --- a/docs/documentation/Creating-a-new-Storm-project.md +++ b/docs/Creating-a-new-Storm-project.md @@ -8,11 +8,11 @@ This page outlines how to set up a Storm project for development. The steps are: 1. Add Storm jars to classpath 2. If using multilang, add multilang dir to classpath -Follow along to see how to set up the [storm-starter](https://github.com/apache/storm/blob/master/examples/storm-starter) project in Eclipse. +Follow along to see how to set up the [storm-starter]({{page.git-blob-base}}/examples/storm-starter) project in Eclipse. ### Add Storm jars to classpath -You'll need the Storm jars on your classpath to develop Storm topologies. Using [Maven](Maven.html) is highly recommended. [Here's an example](https://github.com/apache/storm/blob/master/examples/storm-starter/pom.xml) of how to setup your pom.xml for a Storm project. If you don't want to use Maven, you can include the jars from the Storm release on your classpath. +You'll need the Storm jars on your classpath to develop Storm topologies. Using [Maven](Maven.html) is highly recommended. [Here's an example]({{page.git-blob-base}}/examples/storm-starter/pom.xml) of how to setup your pom.xml for a Storm project. If you don't want to use Maven, you can include the jars from the Storm release on your classpath. To set up the classpath in Eclipse, create a new Java project, include `src/jvm/` as a source path, and make sure all the jars in `lib/` and `lib/dev/` are in the `Referenced Libraries` section of the project. diff --git a/docs/documentation/DSLs-and-multilang-adapters.md b/docs/DSLs-and-multilang-adapters.md similarity index 100% rename from docs/documentation/DSLs-and-multilang-adapters.md rename to docs/DSLs-and-multilang-adapters.md diff --git a/docs/Daemon-Fault-Tolerance.md b/docs/Daemon-Fault-Tolerance.md new file mode 100644 index 00000000000..8dce601a8b4 --- /dev/null +++ b/docs/Daemon-Fault-Tolerance.md @@ -0,0 +1,30 @@ +--- +title: Daemon Fault Tolerance +layout: documentation +documentation: true +--- +Storm has several different daemon processes. Nimbus that schedules workers, supervisors that launch and kill workers, the log viewer that gives access to logs, and the UI that shows the status of a cluster. + +## What happens when a worker dies? + +When a worker dies, the supervisor will restart it. If it continuously fails on startup and is unable to heartbeat to Nimbus, Nimbus will reschedule the worker. + +## What happens when a node dies? + +The tasks assigned to that machine will time-out and Nimbus will reassign those tasks to other machines. + +## What happens when Nimbus or Supervisor daemons die? + +The Nimbus and Supervisor daemons are designed to be fail-fast (process self-destructs whenever any unexpected situation is encountered) and stateless (all state is kept in Zookeeper or on disk). As described in [Setting up a Storm cluster](Setting-up-a-Storm-cluster.html), the Nimbus and Supervisor daemons must be run under supervision using a tool like daemontools or monit. So if the Nimbus or Supervisor daemons die, they restart like nothing happened. + +Most notably, no worker processes are affected by the death of Nimbus or the Supervisors. This is in contrast to Hadoop, where if the JobTracker dies, all the running jobs are lost. + +## Is Nimbus a single point of failure? + +If you lose the Nimbus node, the workers will still continue to function. Additionally, supervisors will continue to restart workers if they die. However, without Nimbus, workers won't be reassigned to other machines when necessary (like if you lose a worker machine). + +Storm Nimbus is highly available since 1.0.0. More information please refer to [Nimbus HA Design](nimbus-ha-design.html) document. + +## How does Storm guarantee data processing? + +Storm provides mechanisms to guarantee data processing even if nodes die or messages are lost. See [Guaranteeing message processing](Guaranteeing-message-processing.html) for the details. diff --git a/docs/documentation/Defining-a-non-jvm-language-dsl-for-storm.md b/docs/Defining-a-non-jvm-language-dsl-for-storm.md similarity index 84% rename from docs/documentation/Defining-a-non-jvm-language-dsl-for-storm.md rename to docs/Defining-a-non-jvm-language-dsl-for-storm.md index 77eb39255aa..7096a430e4b 100644 --- a/docs/documentation/Defining-a-non-jvm-language-dsl-for-storm.md +++ b/docs/Defining-a-non-jvm-language-dsl-for-storm.md @@ -3,7 +3,7 @@ title: Defining a Non-JVM DSL for Storm layout: documentation documentation: true --- -The right place to start to learn how to make a non-JVM DSL for Storm is [storm-core/src/storm.thrift](https://github.com/apache/storm/blob/master/storm-core/src/storm.thrift). Since Storm topologies are just Thrift structures, and Nimbus is a Thrift daemon, you can create and submit topologies in any language. +The right place to start to learn how to make a non-JVM DSL for Storm is [storm-core/src/storm.thrift]({{page.git-blob-base}}/storm-core/src/storm.thrift). Since Storm topologies are just Thrift structures, and Nimbus is a Thrift daemon, you can create and submit topologies in any language. When you create the Thrift structs for spouts and bolts, the code for the spout or bolt is specified in the ComponentObject struct: @@ -35,4 +35,4 @@ Then you can connect to Nimbus using the Thrift API and submit the topology, pas void submitTopology(1: string name, 2: string uploadedJarLocation, 3: string jsonConf, 4: StormTopology topology) throws (1: AlreadyAliveException e, 2: InvalidTopologyException ite); ``` -Finally, one of the key things to do in a non-JVM DSL is make it easy to define the entire topology in one file (the bolts, spouts, and the definition of the topology). \ No newline at end of file +Finally, one of the key things to do in a non-JVM DSL is make it easy to define the entire topology in one file (the bolts, spouts, and the definition of the topology). diff --git a/docs/documentation/Distributed-RPC.md b/docs/Distributed-RPC.md similarity index 96% rename from docs/documentation/Distributed-RPC.md rename to docs/Distributed-RPC.md index 484178b1ced..b20419a080d 100644 --- a/docs/documentation/Distributed-RPC.md +++ b/docs/Distributed-RPC.md @@ -24,7 +24,7 @@ A client sends the DRPC server the name of the function to execute and the argum ### LinearDRPCTopologyBuilder -Storm comes with a topology builder called [LinearDRPCTopologyBuilder](/javadoc/apidocs/backtype/storm/drpc/LinearDRPCTopologyBuilder.html) that automates almost all the steps involved for doing DRPC. These include: +Storm comes with a topology builder called [LinearDRPCTopologyBuilder](javadocs/org/apache/storm/drpc/LinearDRPCTopologyBuilder.html) that automates almost all the steps involved for doing DRPC. These include: 1. Setting up the spout 2. Returning the results to the DRPC server @@ -118,7 +118,7 @@ The reach of a URL is the number of unique people exposed to a URL on Twitter. T A single reach computation can involve thousands of database calls and tens of millions of follower records during the computation. It's a really, really intense computation. As you're about to see, implementing this function on top of Storm is dead simple. On a single machine, reach can take minutes to compute; on a Storm cluster, you can compute reach for even the hardest URLs in a couple seconds. -A sample reach topology is defined in storm-starter [here](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/storm/starter/ReachTopology.java). Here's how you define the reach topology: +A sample reach topology is defined in storm-starter [here]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/ReachTopology.java). Here's how you define the reach topology: ```java LinearDRPCTopologyBuilder builder = new LinearDRPCTopologyBuilder("reach"); @@ -196,4 +196,4 @@ The rest of the topology should be self-explanatory. As you can see, every singl ### Advanced * KeyedFairBolt for weaving the processing of multiple requests at the same time -* How to use `CoordinatedBolt` directly \ No newline at end of file +* How to use `CoordinatedBolt` directly diff --git a/docs/Eventlogging.md b/docs/Eventlogging.md new file mode 100644 index 00000000000..08952324285 --- /dev/null +++ b/docs/Eventlogging.md @@ -0,0 +1,96 @@ +--- +title: Topology event inspector +layout: documentation +documentation: true +--- + +# Introduction + +Topology event inspector provides the ability to view the tuples as it flows through different stages in a storm topology. +This could be useful for inspecting the tuples emitted at a spout or a bolt in the topology pipeline while the topology is running, without stopping or redeploying the topology. The normal flow of tuples from the spouts to the bolts is not affected by turning on event logging. + +## Enabling event logging + +Note: Event logging needs to be enabled first by setting the storm config "topology.eventlogger.executors" to a non zero value. Please see +the [Configuration](#config) section for more details. + +Events can be logged by clicking the "Debug" button under the topology actions in the topology view. This logs the +tuples from all the spouts and bolts in a topology at the specified sampling percentage. + +
+ + +

Figure 1: Enable event logging at topology level.

+
+ +You could also enable event logging at a specific spout or bolt level by going to the corresponding component page and +clicking "Debug" under component actions. + +
+ + +

Figure 2: Enable event logging at component level.

+
+ +## Viewing the event logs +The Storm "logviewer" should be running for viewing the logged tuples. If not already running log viewer can be started by running the "bin/storm logviewer" command from the storm installation directory. For viewing the tuples, go to the specific spout or bolt component page from storm UI and click on the "events" link under the component summary (as highlighted in Figure 2 above). + +This would open up a view like below where you can navigate between different pages and view the logged tuples. + +
+ + +

Figure 3: Viewing the logged events.

+
+ +Each line in the event log contains an entry corresponding to a tuple emitted from a specific spout/bolt in a comma separated format. + +`Timestamp, Component name, Component task-id, MessageId (in case of anchoring), List of emitted values` + +## Disabling the event logs + +Event logging can be disabled at a specific component or at the topology level by clicking the "Stop Debug" under the topology or component actions in the Storm UI. + +
+ + +

Figure 4: Disable event logging at topology level.

+
+ +## Configuration +Eventlogging works by sending the events (tuples) from each component to an internal eventlogger bolt. By default Storm does not start any event logger tasks, but this can be easily changed by setting the below parameter while running your topology (by setting it in storm.yaml or passing options via command line). + +| Parameter | Meaning | +| -------------------------------------------|-----------------------| +| "topology.eventlogger.executors": 0 | No event logger tasks are created (default). | +| "topology.eventlogger.executors": 1 | One event logger task for the topology. | +| "topology.eventlogger.executors": nil | One event logger task per worker. | + + +## Extending eventlogging +Storm provides an `IEventLogger` interface which is used by the event logger bolt to log the events. The default implementation for this is a FileBasedEventLogger which logs the events to an events.log file ( `logs/workers-artifacts///events.log`). Alternate implementations of the `IEventLogger` interface can be added to extend the event logging functionality (say build a search index or log the events in a database etc) + +```java +/** + * EventLogger interface for logging the event info to a sink like log file or db + * for inspecting the events via UI for debugging. + */ +public interface IEventLogger { + /** + * Invoked during eventlogger bolt prepare. + */ + void prepare(Map stormConf, TopologyContext context); + + /** + * Invoked when the {@link EventLoggerBolt} receives a tuple from the spouts or bolts that has event logging enabled. + * + * @param e the event + */ + void log(EventInfo e); + + /** + * Invoked when the event logger bolt is cleaned up + */ + void close(); +} +``` diff --git a/docs/documentation/FAQ.md b/docs/FAQ.md similarity index 90% rename from docs/documentation/FAQ.md rename to docs/FAQ.md index b292b2f9504..127c95c9681 100644 --- a/docs/documentation/FAQ.md +++ b/docs/FAQ.md @@ -28,7 +28,7 @@ documentation: true ### Halp! I cannot see: -* **my logs** Logs by default go to $STORM_HOME/logs. Check that you have write permissions to that directory. They are configured in the logback/cluster.xml (0.9) and log4j/*.properties in earlier versions. +* **my logs** Logs by default go to $STORM_HOME/logs. Check that you have write permissions to that directory. They are configured in log4j2/{cluster, worker}.xml. * **final JVM settings** Add the `-XX+PrintFlagsFinal` commandline option in the childopts (see the conf file) * **final Java system properties** Add `Properties props = System.getProperties(); props.list(System.out);` near where you build your topology. @@ -62,6 +62,10 @@ You can join streams with join, merge or multiReduce. At time of writing, you can't emit to multiple output streams from Trident -- see [STORM-68](https://issues.apache.org/jira/browse/STORM-68) +### Why am I getting a NotSerializableException/IllegalStateException when my topology is being started up? + +Within the Storm lifecycle, the topology is instantiated and then serialized to byte format to be stored in ZooKeeper, prior to the topology being executed. Within this step, if a spout or bolt within the topology has an initialized unserializable property, serialization will fail. If there is a need for a field that is unserializable, initialize it within the bolt or spout's prepare method, which is run after the topology is delivered to the worker. + ## Spouts ### What is a coordinator, and why are there several? @@ -74,11 +78,11 @@ You should only store static data, and as little of it as possible, into the met ### How often is the 'emitPartitionBatchNew' function called? -Since the MBC is the actual spout, all the tuples in a batch are just members of its tupletree. That means storm's "max spout pending" config effectively defines the number of concurrent batches trident runs. The MBC emits a new batch if it has fewer than max-spending tuples pending and if at least one [trident batch interval](https://github.com/apache/storm/blob/master/conf/defaults.yaml#L115)'s worth of seconds has passed since the last batch. +Since the MBC is the actual spout, all the tuples in a batch are just members of its tupletree. That means storm's "max spout pending" config effectively defines the number of concurrent batches trident runs. The MBC emits a new batch if it has fewer than max-spending tuples pending and if at least one [trident batch interval]({{page.git-blob-base}}/conf/defaults.yaml#L115)'s worth of seconds has passed since the last batch. ### If nothing was emitted does Trident slow down the calls? -Yes, there's a pluggable "spout wait strategy"; the default is to sleep for a [configurable amount of time](https://github.com/apache/storm/blob/master/conf/defaults.yaml#L110) +Yes, there's a pluggable "spout wait strategy"; the default is to sleep for a [configurable amount of time]({{page.git-blob-base}}/conf/defaults.yaml#L110) ### OK, then what is the trident batch interval for? @@ -109,7 +113,7 @@ You can't change the overall batch size once generated, but you can change the n ### How do I aggregate events by time? -If have records with an immutable timestamp, and you would like to count, average or otherwise aggregate them into discrete time buckets, Trident is an excellent and scalable solution. +If you have records with an immutable timestamp, and you would like to count, average or otherwise aggregate them into discrete time buckets, Trident is an excellent and scalable solution. Write an `Each` function that turns the timestamp into a time bucket: if the bucket size was "by hour", then the timestamp `2013-08-08 12:34:56` would be mapped to the `2013-08-08 12:00:00` time bucket, and so would everything else in the twelve o'clock hour. Then group on that timebucket and use a grouped persistentAggregate. The persistentAggregate uses a local cacheMap backed by a data store. Groups with many records require very few reads from the data store, and use efficient bulk reads and writes; as long as your data feed is relatively prompt Trident will make very efficient use of memory and network. Even if a server drops off line for a day, then delivers that full day's worth of data in a rush, the old results will be calmly retrieved and updated -- and without interfering with calculating the current results. @@ -120,4 +124,4 @@ You cannot know that all events are collected -- this is an epistemological chal * Set a time limit using domain knowledge * Introduce a _punctuation_: a record known to come after all records in the given time bucket. Trident uses this scheme to know when a batch is complete. If you for instance receive records from a set of sensors, each in order for that sensor, then once all sensors have sent you a 3:02:xx or later timestamp lets you know you can commit. * When possible, make your process incremental: each value that comes in makes the answer more an more true. A Trident ReducerAggregator is an operator that takes a prior result and a set of new records and returns a new result. This lets the result be cached and serialized to a datastore; if a server drops off line for a day and then comes back with a full day's worth of data in a rush, the old results will be calmly retrieved and updated. -* Lambda architecture: Record all events into an archival store (S3, HBase, HDFS) on receipt. in the fast layer, once the time window is clear, process the bucket to get an actionable answer, and ignore everything older than the time window. Periodically run a global aggregation to calculate a "correct" answer. \ No newline at end of file +* Lambda architecture: Record all events into an archival store (S3, HBase, HDFS) on receipt. in the fast layer, once the time window is clear, process the bucket to get an actionable answer, and ignore everything older than the time window. Periodically run a global aggregation to calculate a "correct" answer. diff --git a/docs/documentation/Fault-tolerance.md b/docs/Fault-tolerance.md similarity index 94% rename from docs/documentation/Fault-tolerance.md rename to docs/Fault-tolerance.md index d70fd1df6e4..9a7a349f5b2 100644 --- a/docs/documentation/Fault-tolerance.md +++ b/docs/Fault-tolerance.md @@ -1,7 +1,5 @@ --- -title: Fault Tolerance layout: documentation -documentation: true --- This page explains the design details of Storm that make it a fault-tolerant system. @@ -27,4 +25,4 @@ So the answer is that Nimbus is "sort of" a SPOF. In practice, it's not a big de ## How does Storm guarantee data processing? -Storm provides mechanisms to guarantee data processing even if nodes die or messages are lost. See [Guaranteeing message processing](Guaranteeing-message-processing.html) for the details. \ No newline at end of file +Storm provides mechanisms to guarantee data processing even if nodes die or messages are lost. See [Guaranteeing message processing](Guaranteeing-message-processing.html) for the details. diff --git a/docs/documentation/Guaranteeing-message-processing.md b/docs/Guaranteeing-message-processing.md similarity index 89% rename from docs/documentation/Guaranteeing-message-processing.md rename to docs/Guaranteeing-message-processing.md index 4ecc620b0aa..4c2314cd9f9 100644 --- a/docs/documentation/Guaranteeing-message-processing.md +++ b/docs/Guaranteeing-message-processing.md @@ -3,7 +3,8 @@ title: Guaranteeing Message Processing layout: documentation documentation: true --- -Storm guarantees that each message coming off a spout will be fully processed. This page describes how Storm accomplishes this guarantee and what you have to do as a user to benefit from Storm's reliability capabilities. +Storm offers several different levels of guaranteed message processing, including best effort, at least once, and exactly once through [Trident](Trident-tutorial.html). +This page describes how Storm can guarantee at least once processing. ### What does it mean for a message to be "fully processed"? @@ -25,11 +26,11 @@ This topology reads sentences off of a Kestrel queue, splits the sentences into ![Tuple tree](images/tuple_tree.png) -Storm considers a tuple coming off a spout "fully processed" when the tuple tree has been exhausted and every message in the tree has been processed. A tuple is considered failed when its tree of messages fails to be fully processed within a specified timeout. This timeout can be configured on a topology-specific basis using the [Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS](/javadoc/apidocs/backtype/storm/Config.html#TOPOLOGY_MESSAGE_TIMEOUT_SECS) configuration and defaults to 30 seconds. +Storm considers a tuple coming off a spout "fully processed" when the tuple tree has been exhausted and every message in the tree has been processed. A tuple is considered failed when its tree of messages fails to be fully processed within a specified timeout. This timeout can be configured on a topology-specific basis using the [Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS](javadocs/org/apache/storm/Config.html#TOPOLOGY_MESSAGE_TIMEOUT_SECS) configuration and defaults to 30 seconds. ### What happens if a message is fully processed or fails to be fully processed? -To understand this question, let's take a look at the lifecycle of a tuple coming off of a spout. For reference, here is the interface that spouts implement (see the [Javadoc](/javadoc/apidocs/backtype/storm/spout/ISpout.html) for more information): +To understand this question, let's take a look at the lifecycle of a tuple coming off of a spout. For reference, here is the interface that spouts implement (see the [Javadoc](javadocs/org/apache/storm/spout/ISpout.html) for more information): ```java public interface ISpout extends Serializable { @@ -53,7 +54,7 @@ Let's use `KestrelSpout` again to see what a `Spout` needs to do to guarantee me ### What is Storm's reliability API? -There's two things you have to do as a user to benefit from Storm's reliability capabilities. First, you need to tell Storm whenever you're creating a new link in the tree of tuples. Second, you need to tell Storm when you have finished processing an individual tuple. By doing both these things, Storm can detect when the tree of tuples is fully processed and can ack or fail the spout tuple appropriately. Storm's API provides a concise way of doing both of these tasks. +There are two things you have to do as a user to benefit from Storm's reliability capabilities. First, you need to tell Storm whenever you're creating a new link in the tree of tuples. Second, you need to tell Storm when you have finished processing an individual tuple. By doing both these things, Storm can detect when the tree of tuples is fully processed and can ack or fail the spout tuple appropriately. Storm's API provides a concise way of doing both of these tasks. Specifying a link in the tuple tree is called _anchoring_. Anchoring is done at the same time you emit a new tuple. Let's use the following bolt as an example. This bolt splits a tuple containing a sentence into a tuple for each word: @@ -131,12 +132,11 @@ In contrast, bolts that do aggregations or joins may delay acking a tuple until ### How do I make my applications work correctly given that tuples can be replayed? -As always in software design, the answer is "it depends." Storm 0.7.0 introduced the "transactional topologies" feature, which enables you to get fully fault-tolerant exactly-once messaging semantics for most computations. Read more about transactional topologies [here](Transactional-topologies.html). - +As always in software design, the answer is "it depends." If you really want exactly once semantics use the [Trident](Trident-tutorial.html) API. In some cases, like with a lot of analytics, dropping data is OK so disabling the fault tolerance by setting the number of acker bolts to 0 [Config.TOPOLOGY_ACKERS](javadocs/org/apache/storm/Config.html#TOPOLOGY_ACKERS). But in some cases you want to be sure that everything was processed at least once and nothing was dropped. This is especially useful if all operations are idenpotent or if deduping can happen aferwards. ### How does Storm implement reliability in an efficient way? -A Storm topology has a set of special "acker" tasks that track the DAG of tuples for every spout tuple. When an acker sees that a DAG is complete, it sends a message to the spout task that created the spout tuple to ack the message. You can set the number of acker tasks for a topology in the topology configuration using [Config.TOPOLOGY_ACKERS](/javadoc/apidocs/backtype/storm/Config.html#TOPOLOGY_ACKERS). Storm defaults TOPOLOGY_ACKERS to one task -- you will need to increase this number for topologies processing large amounts of messages. +A Storm topology has a set of special "acker" tasks that track the DAG of tuples for every spout tuple. When an acker sees that a DAG is complete, it sends a message to the spout task that created the spout tuple to ack the message. You can set the number of acker tasks for a topology in the topology configuration using [Config.TOPOLOGY_ACKERS](javadocs/org/apache/storm/Config.html#TOPOLOGY_ACKERS). Storm defaults TOPOLOGY_ACKERS to one task per worker. The best way to understand Storm's reliability implementation is to look at the lifecycle of tuples and tuple DAGs. When a tuple is created in a topology, whether in a spout or a bolt, it is given a random 64 bit id. These ids are used by ackers to track the tuple DAG for every spout tuple. @@ -178,4 +178,4 @@ There are three ways to remove reliability. The first is to set Config.TOPOLOGY_ The second way is to remove reliability on a message by message basis. You can turn off tracking for an individual spout tuple by omitting a message id in the `SpoutOutputCollector.emit` method. -Finally, if you don't care if a particular subset of the tuples downstream in the topology fail to be processed, you can emit them as unanchored tuples. Since they're not anchored to any spout tuples, they won't cause any spout tuples to fail if they aren't acked. \ No newline at end of file +Finally, if you don't care if a particular subset of the tuples downstream in the topology fail to be processed, you can emit them as unanchored tuples. Since they're not anchored to any spout tuples, they won't cause any spout tuples to fail if they aren't acked. diff --git a/docs/Hooks.md b/docs/Hooks.md new file mode 100644 index 00000000000..3c85407b7eb --- /dev/null +++ b/docs/Hooks.md @@ -0,0 +1,9 @@ +--- +title: Hooks +layout: documentation +documentation: true +--- +Storm provides hooks with which you can insert custom code to run on any number of events within Storm. You create a hook by extending the [BaseTaskHook](javadocs/org/apache/storm/hooks/BaseTaskHook.html) class and overriding the appropriate method for the event you want to catch. There are two ways to register your hook: + +1. In the open method of your spout or prepare method of your bolt using the [TopologyContext](javadocs/org/apache/storm/task/TopologyContext.html#addTaskHook) method. +2. Through the Storm configuration using the ["topology.auto.task.hooks"](javadocs/org/apache/storm/Config.html#TOPOLOGY_AUTO_TASK_HOOKS) config. These hooks are automatically registered in every spout or bolt, and are useful for doing things like integrating with a custom monitoring system. diff --git a/docs/Implementation-docs.md b/docs/Implementation-docs.md new file mode 100644 index 00000000000..9eb91f52315 --- /dev/null +++ b/docs/Implementation-docs.md @@ -0,0 +1,13 @@ +--- +title: Storm Internal Implementation +layout: documentation +documentation: true +--- +This section of the wiki is dedicated to explaining how Storm is implemented. You should have a good grasp of how to use Storm before reading these sections. + +- [Structure of the codebase](Structure-of-the-codebase.html) +- [Lifecycle of a topology](Lifecycle-of-a-topology.html) +- [Message passing implementation](Message-passing-implementation.html) +- [Metrics](Metrics.html) +- [Nimbus HA](nimbus-ha-design.html) +- [Storm SQL](storm-sql-internal.html) diff --git a/docs/documentation/Installing-native-dependencies.md b/docs/Installing-native-dependencies.md similarity index 97% rename from docs/documentation/Installing-native-dependencies.md rename to docs/Installing-native-dependencies.md index 3207b8e82aa..1937d4bffcf 100644 --- a/docs/documentation/Installing-native-dependencies.md +++ b/docs/Installing-native-dependencies.md @@ -35,4 +35,4 @@ To get the JZMQ build to work, you may need to do one or all of the following: 3. Upgrade autoconf on your machine 4. Follow the instructions in [this blog post](http://blog.pmorelli.com/getting-zeromq-and-jzmq-running-on-mac-os-x) -If you run into any errors when running `./configure`, [this thread](http://stackoverflow.com/questions/3522248/how-do-i-compile-jzmq-for-zeromq-on-osx) may provide a solution. \ No newline at end of file +If you run into any errors when running `./configure`, [this thread](http://stackoverflow.com/questions/3522248/how-do-i-compile-jzmq-for-zeromq-on-osx) may provide a solution. diff --git a/docs/Joins.md b/docs/Joins.md new file mode 100644 index 00000000000..9efb7c68f17 --- /dev/null +++ b/docs/Joins.md @@ -0,0 +1,139 @@ +--- +title: Joining Streams in Storm Core +layout: documentation +documentation: true +--- + +Storm core supports joining multiple data streams into one with the help of `JoinBolt`. +`JoinBolt` is a Windowed bolt, i.e. it waits for the configured window duration to match up the +tuples among the streams being joined. This helps align the streams within a Window boundary. + +Each of `JoinBolt`'s incoming data streams must be Fields Grouped on a single field. A stream +should only be joined with the other streams using the field on which it has been FieldsGrouped. +Knowing this will help understand the join syntax described below. + +## Performing Joins +Consider the following SQL join involving 4 tables: + +```sql +select userId, key4, key2, key3 +from table1 +inner join table2 on table2.userId = table1.key1 +inner join table3 on table3.key3 = table2.userId +left join table4 on table4.key4 = table3.key3 +``` + +Similar joins could be expressed on tuples generated by 4 spouts using `JoinBolt`: + +```java +JoinBolt jbolt = new JoinBolt("spout1", "key1") // from spout1 + .join ("spout2", "userId", "spout1") // inner join spout2 on spout2.userId = spout1.key1 + .join ("spout3", "key3", "spout2") // inner join spout3 on spout3.key3 = spout2.userId + .leftJoin ("spout4", "key4", "spout3") // left join spout4 on spout4.key4 = spout3.key3 + .select ("userId, key4, key2, spout3:key3") // chose output fields + .withTumblingWindow( new Duration(10, TimeUnit.MINUTES) ) ; + +topoBuilder.setBolt("joiner", jbolt, 1) + .fieldsGrouping("spout1", new Fields("key1") ) + .fieldsGrouping("spout2", new Fields("userId") ) + .fieldsGrouping("spout3", new Fields("key3") ) + .fieldsGrouping("spout4", new Fields("key4") ); +``` + +The bolt constructor takes two arguments. The 1st argument introduces the data from `spout1` +to be the first stream and specifies that it will always use field `key1` when joining this with the others streams. +The name of the component specified must refer to the spout or bolt that is directly connected to the Join bolt. +Here data received from `spout1` must be fields grouped on `key1`. Similarly, each of the `leftJoin()` and `join()` method +calls introduce a new stream along with the field to use for the join. As seen in above example, the same FieldsGrouping +requirement applies to these streams as well. The 3rd argument to the join methods refers to another stream with which +to join. + +The `select()` method is used to specify the output fields. The argument to `select` is a comma separated list of fields. +Individual field names can be prefixed with a stream name to disambiguate between the same field name occurring in +multiple streams as follows: `.select("spout3:key3, spout4:key3")`. Nested tuple types are supported if the +nesting has been done using `Map`s. For example `outer.inner.innermost` refers to a field that is nested three levels +deep where `outer` and `inner` are of type `Map`. + +Stream name prefix is not allowed for the fields in any of the join() arguments, but nested fields are supported. + +The call to `withTumblingWindow()` above, configures the join window to be a 10 minute tumbling window. Since `JoinBolt` +is a Windowed Bolt, we can also use the `withWindow` method to configure it as a sliding window (see tips section below). + +## Stream names and Join order +* Stream names must be introduced (in constructor or as 1st arg to various join methods) before being referred +to (in the 3rd argument of the join methods). Forward referencing of stream names, as shown below, is not allowed: + +```java +new JoinBolt( "spout1", "key1") + .join ( "spout2", "userId", "spout3") //not allowed. spout3 not yet introduced + .join ( "spout3", "key3", "spout1") +``` +* Internally, the joins will be performed in the order expressed by the user. + +## Joining based on Stream names + +For simplicity, Storm topologies often use the `default` stream. Topologies can also use named streams +instead of `default` streams. To support such topologies, `JoinBolt` can be configured to use stream +names, instead of source component (spout/bolt) names, via the constructor's first argument: + +```java +new JoinBolt(JoinBolt.Selector.STREAM, "stream1", "key1") + .join("stream2", "key2") + ... +``` +The first argument `JoinBolt.Selector.STREAM` informs the bolt that `stream1/2/3/4` refer to named streams +(as opposed to names of upstream spouts/bolts). + + +The below example joins two named streams from four spouts: + +```java +new JoinBolt(JoinBolt.Selector.STREAM, "stream1", "key1") + .join ("stream2", "userId", "stream1" ) + .select ("userId, key1, key2") + .withTumblingWindow( new Duration(10, TimeUnit.MINUTES) ) ; + +topoBuilder.setBolt("joiner", jbolt, 1) + .fieldsGrouping("bolt1", "stream1", new Fields("key1") ) + .fieldsGrouping("bolt2", "stream1", new Fields("key1") ) + .fieldsGrouping("bolt3", "stream2", new Fields("userId") ) + .fieldsGrouping("bolt4", "stream1", new Fields("key1") ); +``` + +In the above example, it is possible that `bolt1`, for example, is emitting other streams also. But the join bolt +is only subscribing to `stream1` & `stream2` from the different bolts. `stream1` from `bolt1`, `bolt2` and `bolt4` +is treated as a single stream and joined against `stream2` from `bolt3`. + +## Limitations: +1. Currently only INNER and LEFT joins are supported. + +2. Unlike SQL, which allows joining the same table on different keys to different tables, here the same one field must be used + on a stream. Fields Grouping ensures the right tuples are routed to the right instances of a Join Bolt. Consequently the + FieldsGrouping field must be same as the join field, for correct results. To perform joins on multiple fields, the fields + can be combined into one field and then sent to the Join bolt. + + +## Tips: + +1. Joins can be CPU and memory intensive. The larger the data accumulated in the current window (proportional to window + length), the longer it takes to do the join. Having a short sliding interval (few seconds for example) triggers frequent + joins. Consequently performance can suffer if using large window lengths or small sliding intervals or both. + +2. Duplication of joined records across windows is possible when using Sliding Windows. This is because the tuples continue to exist + across multiple windows when using Sliding Windows. + +3. If message timeouts are enabled, ensure the timeout setting (topology.message.timeout.secs) is large enough to comfortably + accommodate the window size, plus the additional processing by other spouts and bolts. + +4. Joining a window of two streams with M and N elements each, *in the worst case*, can produce MxN elements with every output tuple + anchored to one tuple from each input stream. This can mean a lot of output tuples from JoinBolt and even more ACKs for downstream bolts + to emit. This can place a substantial pressure on the messaging system and dramatically slowdown the topology if not careful. + To manage the load on the messaging subsystem, it is advisable to: + * Increase the worker's heap (topology.worker.max.heap.size.mb). + * **If** ACKing is not necessary for your topology, disable ACKers (topology.acker.executors=0). + * Disable event logger (topology.eventlogger.executors=0). + * Turn of topology debugging (topology.debug=false). + * Set topology.max.spout.pending to a value large enough to accommodate an estimated full window worth of tuples plus some more for headroom. + This helps mitigate the possibility of spouts emitting excessive tuples when messaging subsystem is experiencing excessive load. This situation + can occur when its value is set to null. + * Lastly, keep the window size to the minimum value necessary for solving the problem at hand. diff --git a/docs/documentation/Kestrel-and-Storm.md b/docs/Kestrel-and-Storm.md similarity index 88% rename from docs/documentation/Kestrel-and-Storm.md rename to docs/Kestrel-and-Storm.md index d079b8101a7..ff48995840f 100644 --- a/docs/documentation/Kestrel-and-Storm.md +++ b/docs/Kestrel-and-Storm.md @@ -3,16 +3,16 @@ title: Storm and Kestrel layout: documentation documentation: true --- -This page explains how to use to Storm to consume items from a Kestrel cluster. +This page explains how to use Storm to consume items from a Kestrel cluster. ## Preliminaries ### Storm -This tutorial uses examples from the [storm-kestrel](https://github.com/nathanmarz/storm-kestrel) project and the [storm-starter](http://github.com/apache/storm/blob/master/examples/storm-starter) project. It's recommended that you clone those projects and follow along with the examples. Read [Setting up development environment](https://github.com/apache/storm/wiki/Setting-up-development-environment) and [Creating a new Storm project](https://github.com/apache/storm/wiki/Creating-a-new-Storm-project) to get your machine set up. +This tutorial uses examples from the [storm-kestrel](https://github.com/nathanmarz/storm-kestrel) project and the [storm-starter](http://github.com/apache/storm/blob/{{page.version}}/examples/storm-starter) project. It's recommended that you clone those projects and follow along with the examples. Read [Setting up development environment](Setting-up-development-environment.html) and [Creating a new Storm project](Creating-a-new-Storm-project.html) to get your machine set up. ### Kestrel It assumes you are able to run locally a Kestrel server as described [here](https://github.com/nathanmarz/storm-kestrel). ## Kestrel Server and Queue -A single kestrel server has a set of queues. A Kestrel queue is a very simple message queue that runs on the JVM and uses the memcache protocol (with some extensions) to talk to clients. For details, look at the implementation of the [KestrelThriftClient](https://github.com/nathanmarz/storm-kestrel/blob/master/src/jvm/backtype/storm/spout/KestrelThriftClient.java) class provided in [storm-kestrel](https://github.com/nathanmarz/storm-kestrel) project. +A single kestrel server has a set of queues. A Kestrel queue is a very simple message queue that runs on the JVM and uses the memcache protocol (with some extensions) to talk to clients. For details, look at the implementation of the [KestrelThriftClient](https://github.com/nathanmarz/storm-kestrel/blob/master/src/jvm/org/apache/storm/spout/KestrelThriftClient.java) class provided in [storm-kestrel](https://github.com/nathanmarz/storm-kestrel) project. Each queue is strictly ordered following the FIFO (first in, first out) principle. To keep up with performance items are cached in system memory; though, only the first 128MB is kept in memory. When stopping the server, the queue state is stored in a journal file. @@ -120,9 +120,9 @@ In order to stop it type a closing bracket char ']' in console and hit 'Enter'. import java.io.InputStream; import java.util.Random; - import backtype.storm.spout.KestrelClient; - import backtype.storm.spout.KestrelClient.Item; - import backtype.storm.spout.KestrelClient.ParseError; + import org.apache.storm.spout.KestrelClient; + import org.apache.storm.spout.KestrelClient.Item; + import org.apache.storm.spout.KestrelClient.ParseError; public class AddSentenceItemsToKestrel { @@ -197,4 +197,4 @@ Than, wait about 5 seconds in order to avoid a ConnectException. Now execute the program to add items to the queue and launch the Storm topology. The order in which you launch the programs is of no importance. -If you run the topology with TOPOLOGY_DEBUG you should see tuples being emitted in the topology. \ No newline at end of file +If you run the topology with TOPOLOGY_DEBUG you should see tuples being emitted in the topology. diff --git a/docs/documentation/Lifecycle-of-a-topology.md b/docs/Lifecycle-of-a-topology.md similarity index 77% rename from docs/documentation/Lifecycle-of-a-topology.md rename to docs/Lifecycle-of-a-topology.md index 68e5757f753..dba1457f4e8 100644 --- a/docs/documentation/Lifecycle-of-a-topology.md +++ b/docs/Lifecycle-of-a-topology.md @@ -9,74 +9,74 @@ This page explains in detail the lifecycle of a topology from running the "storm First a couple of important notes about topologies: -1. The actual topology that runs is different than the topology the user specifies. The actual topology has implicit streams and an implicit "acker" bolt added to manage the acking framework (used to guarantee data processing). The implicit topology is created via the [system-topology!](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/common.clj#L188) function. +1. The actual topology that runs is different than the topology the user specifies. The actual topology has implicit streams and an implicit "acker" bolt added to manage the acking framework (used to guarantee data processing). The implicit topology is created via the [system-topology!](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/common.clj#L188) function. 2. `system-topology!` is used in two places: - - when Nimbus is creating tasks for the topology [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L316) - - in the worker so it knows where it needs to route messages to [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/worker.clj#L90) + - when Nimbus is creating tasks for the topology [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L316) + - in the worker so it knows where it needs to route messages to [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L90) ## Starting a topology - "storm jar" command executes your class with the specified arguments. The only special thing that "storm jar" does is set the "storm.jar" environment variable for use by `StormSubmitter` later. [code](https://github.com/apache/storm/blob/0.7.1/bin/storm#L101) - When your code uses `StormSubmitter.submitTopology`, `StormSubmitter` takes the following actions: - - First, `StormSubmitter` uploads the jar if it hasn't been uploaded before. [code](https://github.com/apache/storm/blob/0.7.1/src/jvm/backtype/storm/StormSubmitter.java#L83) + - First, `StormSubmitter` uploads the jar if it hasn't been uploaded before. [code](https://github.com/apache/storm/blob/0.7.1/src/jvm/org/apache/storm/StormSubmitter.java#L83) - Jar uploading is done via Nimbus's Thrift interface [code](https://github.com/apache/storm/blob/0.7.1/src/storm.thrift#L200) - `beginFileUpload` returns a path in Nimbus's inbox - 15 kilobytes are uploaded at a time through `uploadChunk` - `finishFileUpload` is called when it's finished uploading - - Here is Nimbus's implementation of those Thrift methods: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L694) - - Second, `StormSubmitter` calls `submitTopology` on the Nimbus thrift interface [code](https://github.com/apache/storm/blob/0.7.1/src/jvm/backtype/storm/StormSubmitter.java#L60) + - Here is Nimbus's implementation of those Thrift methods: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L694) + - Second, `StormSubmitter` calls `submitTopology` on the Nimbus thrift interface [code](https://github.com/apache/storm/blob/0.7.1/src/jvm/org/apache/storm/StormSubmitter.java#L60) - The topology config is serialized using JSON (JSON is used so that writing DSL's in any language is as easy as possible) - Notice that the Thrift `submitTopology` call takes in the Nimbus inbox path where the jar was uploaded -- Nimbus receives the topology submission. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L639) -- Nimbus normalizes the topology configuration. The main purpose of normalization is to ensure that every single task will have the same serialization registrations, which is critical for getting serialization working correctly. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L557) -- Nimbus sets up the static state for the topology [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L661) +- Nimbus receives the topology submission. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L639) +- Nimbus normalizes the topology configuration. The main purpose of normalization is to ensure that every single task will have the same serialization registrations, which is critical for getting serialization working correctly. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L557) +- Nimbus sets up the static state for the topology [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L661) - Jars and configs are kept on local filesystem because they're too big for Zookeeper. The jar and configs are copied into the path {nimbus local dir}/stormdist/{topology id} - `setup-storm-static` writes task -> component mapping into ZK - `setup-heartbeats` creates a ZK "directory" in which tasks can heartbeat -- Nimbus calls `mk-assignment` to assign tasks to machines [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L458) - - Assignment record definition is here: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/common.clj#L25) +- Nimbus calls `mk-assignment` to assign tasks to machines [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L458) + - Assignment record definition is here: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/common.clj#L25) - Assignment contains: - `master-code-dir`: used by supervisors to download the correct jars/configs for the topology from Nimbus - `task->node+port`: Map from a task id to the worker that task should be running on. (A worker is identified by a node/port pair) - `node->host`: A map from node id to hostname. This is used so workers know which machines to connect to to communicate with other workers. Node ids are used to identify supervisors so that multiple supervisors can be run on one machine. One place this is done is with Mesos integration. - `task->start-time-secs`: Contains a map from task id to the timestamp at which Nimbus launched that task. This is used by Nimbus when monitoring topologies, as tasks are given a longer timeout to heartbeat when they're first launched (the launch timeout is configured by "nimbus.task.launch.secs" config) -- Once topologies are assigned, they're initially in a deactivated mode. `start-storm` writes data into Zookeeper so that the cluster knows the topology is active and can start emitting tuples from spouts. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L504) +- Once topologies are assigned, they're initially in a deactivated mode. `start-storm` writes data into Zookeeper so that the cluster knows the topology is active and can start emitting tuples from spouts. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L504) - TODO cluster state diagram (show all nodes and what's kept everywhere) - Supervisor runs two functions in the background: - - `synchronize-supervisor`: This is called whenever assignments in Zookeeper change and also every 10 seconds. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/supervisor.clj#L241) - - Downloads code from Nimbus for topologies assigned to this machine for which it doesn't have the code yet. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/supervisor.clj#L258) - - Writes into local filesystem what this node is supposed to be running. It writes a map from port -> LocalAssignment. LocalAssignment contains a topology id as well as the list of task ids for that worker. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/supervisor.clj#L13) - - `sync-processes`: Reads from the LFS what `synchronize-supervisor` wrote and compares that to what's actually running on the machine. It then starts/stops worker processes as necessary to synchronize. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/supervisor.clj#L177) + - `synchronize-supervisor`: This is called whenever assignments in Zookeeper change and also every 10 seconds. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/supervisor.clj#L241) + - Downloads code from Nimbus for topologies assigned to this machine for which it doesn't have the code yet. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/supervisor.clj#L258) + - Writes into local filesystem what this node is supposed to be running. It writes a map from port -> LocalAssignment. LocalAssignment contains a topology id as well as the list of task ids for that worker. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/supervisor.clj#L13) + - `sync-processes`: Reads from the LFS what `synchronize-supervisor` wrote and compares that to what's actually running on the machine. It then starts/stops worker processes as necessary to synchronize. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/supervisor.clj#L177) -- Worker processes start up through the `mk-worker` function [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/worker.clj#L67) - - Worker connects to other workers and starts a thread to monitor for changes. So if a worker gets reassigned, the worker will automatically reconnect to the other worker's new location. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/worker.clj#L123) - - Monitors whether a topology is active or not and stores that state in the `storm-active-atom` variable. This variable is used by tasks to determine whether or not to call `nextTuple` on the spouts. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/worker.clj#L155) - - The worker launches the actual tasks as threads within it [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/worker.clj#L178) -- Tasks are set up through the `mk-task` function [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/task.clj#L160) - - Tasks set up routing function which takes in a stream and an output tuple and returns a list of task ids to send the tuple to [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/task.clj#L207) (there's also a 3-arity version used for direct streams) - - Tasks set up the spout-specific or bolt-specific code with [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/task.clj#L241) +- Worker processes start up through the `mk-worker` function [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L67) + - Worker connects to other workers and starts a thread to monitor for changes. So if a worker gets reassigned, the worker will automatically reconnect to the other worker's new location. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L123) + - Monitors whether a topology is active or not and stores that state in the `storm-active-atom` variable. This variable is used by tasks to determine whether or not to call `nextTuple` on the spouts. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L155) + - The worker launches the actual tasks as threads within it [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L178) +- Tasks are set up through the `mk-task` function [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L160) + - Tasks set up routing function which takes in a stream and an output tuple and returns a list of task ids to send the tuple to [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L207) (there's also a 3-arity version used for direct streams) + - Tasks set up the spout-specific or bolt-specific code with [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L241) ## Topology Monitoring - Nimbus monitors the topology during its lifetime - - Schedules recurring task on the timer thread to check the topologies [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L623) - - Nimbus's behavior is represented as a finite state machine [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L98) - - The "monitor" event is called on a topology every "nimbus.monitor.freq.secs", which calls `reassign-topology` through `reassign-transition` [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L497) + - Schedules recurring task on the timer thread to check the topologies [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L623) + - Nimbus's behavior is represented as a finite state machine [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L98) + - The "monitor" event is called on a topology every "nimbus.monitor.freq.secs", which calls `reassign-topology` through `reassign-transition` [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L497) - `reassign-topology` calls `mk-assignments`, the same function used to assign the topology the first time. `mk-assignments` is also capable of incrementally updating a topology - `mk-assignments` checks heartbeats and reassigns workers as necessary - Any reassignments change the state in ZK, which will trigger supervisors to synchronize and start/stop workers ## Killing a topology -- "storm kill" command runs this code which just calls the Nimbus Thrift interface to kill the topology: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/command/kill_topology.clj) -- Nimbus receives the kill command [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L671) -- Nimbus applies the "kill" transition to the topology [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L676) -- The kill transition function changes the status of the topology to "killed" and schedules the "remove" event to run "wait time seconds" in the future. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L63) +- "storm kill" command runs this code which just calls the Nimbus Thrift interface to kill the topology: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/command/kill_topology.clj) +- Nimbus receives the kill command [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L671) +- Nimbus applies the "kill" transition to the topology [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L676) +- The kill transition function changes the status of the topology to "killed" and schedules the "remove" event to run "wait time seconds" in the future. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L63) - The wait time defaults to the topology message timeout but can be overridden with the -w flag in the "storm kill" command - This causes the topology to be deactivated for the wait time before its actually shut down. This gives the topology a chance to finish processing what it's currently processing before shutting down the workers - - Changing the status during the kill transition ensures that the kill protocol is fault-tolerant to Nimbus crashing. On startup, if the status of the topology is "killed", Nimbus schedules the remove event to run "wait time seconds" in the future [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L111) -- Removing a topology cleans out the assignment and static information from ZK [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L116) -- A separate cleanup thread runs the `do-cleanup` function which will clean up the heartbeat dir and the jars/configs stored locally. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/backtype/storm/daemon/nimbus.clj#L577) \ No newline at end of file + - Changing the status during the kill transition ensures that the kill protocol is fault-tolerant to Nimbus crashing. On startup, if the status of the topology is "killed", Nimbus schedules the remove event to run "wait time seconds" in the future [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L111) +- Removing a topology cleans out the assignment and static information from ZK [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L116) +- A separate cleanup thread runs the `do-cleanup` function which will clean up the heartbeat dir and the jars/configs stored locally. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/nimbus.clj#L577) diff --git a/docs/Local-mode.md b/docs/Local-mode.md new file mode 100644 index 00000000000..e3d96665066 --- /dev/null +++ b/docs/Local-mode.md @@ -0,0 +1,29 @@ +--- +title: Local Mode +layout: documentation +documentation: true +--- +Local mode simulates a Storm cluster in process and is useful for developing and testing topologies. Running topologies in local mode is similar to running topologies [on a cluster](Running-topologies-on-a-production-cluster.html). + +To create an in-process cluster, simply use the `LocalCluster` class. For example: + +```java +import org.apache.storm.LocalCluster; + +LocalCluster cluster = new LocalCluster(); +``` + +You can then submit topologies using the `submitTopology` method on the `LocalCluster` object. Just like the corresponding method on [StormSubmitter](javadocs/org/apache/storm/StormSubmitter.html), `submitTopology` takes a name, a topology configuration, and the topology object. You can then kill a topology using the `killTopology` method which takes the topology name as an argument. + +To shutdown a local cluster, simple call: + +```java +cluster.shutdown(); +``` + +### Common configurations for local mode + +You can see a full list of configurations [here](javadocs/org/apache/storm/Config.html). + +1. **Config.TOPOLOGY_MAX_TASK_PARALLELISM**: This config puts a ceiling on the number of threads spawned for a single component. Oftentimes production topologies have a lot of parallelism (hundreds of threads) which places unreasonable load when trying to test the topology in local mode. This config lets you easy control that parallelism. +2. **Config.TOPOLOGY_DEBUG**: When this is set to true, Storm will log a message every time a tuple is emitted from any spout or bolt. This is extremely useful for debugging. diff --git a/docs/Logs.md b/docs/Logs.md new file mode 100644 index 00000000000..28e66934a90 --- /dev/null +++ b/docs/Logs.md @@ -0,0 +1,30 @@ +--- +title: Storm Logs +layout: documentation +documentation: true +--- +Logs in Storm are essential for tracking the status, operations, error messages and debug information for all the +daemons (e.g., nimbus, supervisor, logviewer, drpc, ui, pacemaker) and topologies' workers. + +### Location of the Logs +All the daemon logs are placed under ${storm.log.dir} directory, which an administrator can set in the System properties or +in the cluster configuration. By default, ${storm.log.dir} points to ${storm.home}/logs. + +All the worker logs are placed under the workers-artifacts directory in a hierarchical manner, e.g., +${workers-artifacts}/${topologyId}/${port}/worker.log. Users can set the workers-artifacts directory +by configuring the variable "storm.workers.artifacts.dir". By default, workers-artifacts directory +locates at ${storm.log.dir}/logs/workers-artifacts. + +### Using the Storm UI for Log View/Download and Log Search +Daemon and worker logs are allowed to view and download through Storm UI by authorized users. + +To improve the debugging of Storm, we provide the Log Search feature. +Log Search supports searching in a certain log file or in all of a topology's log files: + +String search in a log file: In the log page for a worker, a user can search a certain string, e.g., "Exception", in a certain worker log. This search can happen for both normal text log or rolled zip log files. In the results, the offset and matched lines will be displayed. + +![Search in a log](images/search-for-a-single-worker-log.png "Search in a log") + +Search in a topology: a user can also search a string for a certain topology by clicking the icon of magnifying lens at the top right corner of the UI page. This means the UI will try to search on all the supervisor nodes in a distributed way to find the matched string in all logs for this topology. The search can happen for either normal text log files or rolled zip log files by checking/unchecking the "Search archived logs:" box. Then the matched results can be shown on the UI with url links, directing the user to the certain logs on each supervisor node. This powerful feature is very helpful for users to find certain problematic supervisor nodes running this topology. + +![Search in a topology](images/search-a-topology.png "Search in a topology") diff --git a/docs/Maven.md b/docs/Maven.md new file mode 100644 index 00000000000..0c09c2c89b3 --- /dev/null +++ b/docs/Maven.md @@ -0,0 +1,22 @@ +--- +title: Maven +layout: documentation +documentation: true +--- +To develop topologies, you'll need the Storm jars on your classpath. You should either include the unpacked jars in the classpath for your project or use Maven to include Storm as a development dependency. Storm is hosted on Maven Central. To include Storm in your project as a development dependency, add the following to your pom.xml: + + +```xml + + org.apache.storm + storm-core + {{page.version}} + provided + +``` + +[Here's an example]({{page.git-blob-base}}/examples/storm-starter/pom.xml) of a pom.xml for a Storm project. + +### Developing Storm + +Please refer to [DEVELOPER.md]({{page.git-blob-base}}/DEVELOPER.md) for more details. diff --git a/docs/Message-passing-implementation.md b/docs/Message-passing-implementation.md new file mode 100644 index 00000000000..fd4cf2cbeea --- /dev/null +++ b/docs/Message-passing-implementation.md @@ -0,0 +1,30 @@ +--- +title: Message Passing Implementation +layout: documentation +documentation: true +--- +(Note: this walkthrough is out of date as of 0.8.0. 0.8.0 revamped the message passing infrastructure to be based on the Disruptor) + +This page walks through how emitting and transferring tuples works in Storm. + +- Worker is responsible for message transfer + - `refresh-connections` is called every "task.refresh.poll.secs" or whenever assignment in ZK changes. It manages connections to other workers and maintains a mapping from task -> worker [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L123) + - Provides a "transfer function" that is used by tasks to send tuples to other tasks. The transfer function takes in a task id and a tuple, and it serializes the tuple and puts it onto a "transfer queue". There is a single transfer queue for each worker. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L56) + - The serializer is thread-safe [code](https://github.com/apache/storm/blob/0.7.1/src/jvm/org/apache/storm/serialization/KryoTupleSerializer.java#L26) + - The worker has a single thread which drains the transfer queue and sends the messages to other workers [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L185) + - Message sending happens through this protocol: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/messaging/protocol.clj) + - The implementation for distributed mode uses ZeroMQ [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/messaging/zmq.clj) + - The implementation for local mode uses in memory Java queues (so that it's easy to use Storm locally without needing to get ZeroMQ installed) [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/messaging/local.clj) +- Receiving messages in tasks works differently in local mode and distributed mode + - In local mode, the tuple is sent directly to an in-memory queue for the receiving task [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/messaging/local.clj#L21) + - In distributed mode, each worker listens on a single TCP port for incoming messages and then routes those messages in-memory to tasks. The TCP port is called a "virtual port", because it receives [task id, message] and then routes it to the actual task. [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/worker.clj#L204) + - The virtual port implementation is here: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/zilch/virtual_port.clj) + - Tasks listen on an in-memory ZeroMQ port for messages from the virtual port [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L201) + - Bolts listen here: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L489) + - Spouts listen here: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L382) +- Tasks are responsible for message routing. A tuple is emitted either to a direct stream (where the task id is specified) or a regular stream. In direct streams, the message is only sent if that bolt subscribes to that direct stream. In regular streams, the stream grouping functions are used to determine the task ids to send the tuple to. + - Tasks have a routing map from {stream id} -> {component id} -> {stream grouping function} [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L198) + - The "tasks-fn" returns the task ids to send the tuples to for either regular stream emit or direct stream emit [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L207) + - After getting the output task ids, bolts and spouts use the transfer-fn provided by the worker to actually transfer the tuples + - Bolt transfer code here: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L429) + - Spout transfer code here: [code](https://github.com/apache/storm/blob/0.7.1/src/clj/org/apache/storm/daemon/task.clj#L329) diff --git a/docs/Metrics.md b/docs/Metrics.md new file mode 100644 index 00000000000..1349d0c8c97 --- /dev/null +++ b/docs/Metrics.md @@ -0,0 +1,129 @@ +--- +title: Storm Metrics +layout: documentation +documentation: true +--- +Storm exposes a metrics interface to report summary statistics across the full topology. +It's used internally to track the numbers you see in the Nimbus UI console: counts of executes and acks; average process latency per bolt; worker heap usage; and so forth. + +### Metric Types + +Metrics have to implement [`IMetric`]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/IMetric.java) which contains just one method, `getValueAndReset` -- do any remaining work to find the summary value, and reset back to an initial state. For example, the MeanReducer divides the running total by its running count to find the mean, then initializes both values back to zero. + +Storm gives you these metric types: + +* [AssignableMetric]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/AssignableMetric.java) -- set the metric to the explicit value you supply. Useful if it's an external value or in the case that you are already calculating the summary statistic yourself. +* [CombinedMetric]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/CombinedMetric.java) -- generic interface for metrics that can be updated associatively. +* [CountMetric]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/CountMetric.java) -- a running total of the supplied values. Call `incr()` to increment by one, `incrBy(n)` to add/subtract the given number. + - [MultiCountMetric]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/MultiCountMetric.java) -- a hashmap of count metrics. +* [ReducedMetric]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/ReducedMetric.java) + - [MeanReducer]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/MeanReducer.java) -- track a running average of values given to its `reduce()` method. (It accepts `Double`, `Integer` or `Long` values, and maintains the internal average as a `Double`.) Despite his reputation, the MeanReducer is actually a pretty nice guy in person. + - [MultiReducedMetric]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/MultiReducedMetric.java) -- a hashmap of reduced metrics. + + +### Metrics Consumer + +You can listen and handle the topology metrics via registering Metrics Consumer to your topology. + +To register metrics consumer to your topology, add to your topology's configuration like: + +```java +conf.registerMetricsConsumer(org.apache.storm.metric.LoggingMetricsConsumer.class, 1); +``` + +You can refer [Config#registerMetricsConsumer](javadocs/org/apache/storm/Config.html#registerMetricsConsumer-java.lang.Class-) and overloaded methods from javadoc. + +Otherwise edit the storm.yaml config file: + +```yaml +topology.metrics.consumer.register: + - class: "org.apache.storm.metric.LoggingMetricsConsumer" + parallelism.hint: 1 + - class: "org.apache.storm.metric.HttpForwardingMetricsConsumer" + parallelism.hint: 1 + argument: "http://example.com:8080/metrics/my-topology/" +``` + +Storm appends MetricsConsumerBolt to your topology per each registered metrics consumer internally, and each MetricsConsumerBolt subscribes to receive metrics from all tasks. The parallelism for that Bolt is set to `parallelism.hint` and `component id` for that Bolt is set to `__metrics_`. If you register same class name more than once, postfix `#` is appended to component id. + +Storm provides some built-in metrics consumers for you to try out to see which metrics are provided in your topology. + +* [`LoggingMetricsConsumer`]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/LoggingMetricsConsumer.java) -- listens for all metrics and dumps them to log file with TSV (Tab Separated Values). +* [`HttpForwardingMetricsConsumer`]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/HttpForwardingMetricsConsumer.java) -- listens for all metrics and POSTs them serialized to a configured URL via HTTP. Storm also provides [`HttpForwardingMetricsServer`]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/HttpForwardingMetricsServer.java) as abstract class so you can extend this class and run as a HTTP server, and handle metrics sent by HttpForwardingMetricsConsumer. + +Also, Storm exposes the interface [`IMetricsConsumer`]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/metric/api/IMetricsConsumer.java) for implementing Metrics Consumer so you can create custom metrics consumers and attach to their topologies, or use other great implementation of Metrics Consumers provided by Storm community. Some of examples are [versign/storm-graphite](https://github.com/verisign/storm-graphite), and [storm-metrics-statsd](https://github.com/endgameinc/storm-metrics-statsd). + +When you implement your own metrics consumer, `argument` is passed to Object when [IMetricsConsumer#prepare](javadocs/org/apache/storm/metric/api/IMetricsConsumer.html#prepare-java.util.Map-java.lang.Object-org.apache.storm.task.TopologyContext-org.apache.storm.task.IErrorReporter-) is called, so you need to infer the Java type of configured value on yaml, and do explicit type casting. + +Please keep in mind that MetricsConsumerBolt is just a kind of Bolt, so whole throughput of the topology will go down when registered metrics consumers cannot keep up handling incoming metrics, so you may want to take care of those Bolts like normal Bolts. One of idea to avoid this is making your implementation of Metrics Consumer as `non-blocking` fashion. + + +### Build your own metric (task level) + +You can measure your own metric by registering `IMetric` to Metric Registry. + +Suppose we would like to measure execution count of Bolt#execute. Let's start with defining metric instance. CountMetric seems to fit our use case. + +```java +private transient CountMetric countMetric; +``` + +Notice we define it as transient. IMertic is not Serializable so we defined as transient to avoid any serialization issues. + +Next, let's initialize and register the metric instance. + +```java +@Override +public void prepare(Map conf, TopologyContext context, OutputCollector collector) { + // other intialization here. + countMetric = new CountMetric(); + context.registerMetric("execute_count", countMetric, 60); +} +``` + +The meaning of first and second parameters are straightforward, metric name and instance of IMetric. Third parameter of [TopologyContext#registerMetric](javadocs/org/apache/storm/task/TopologyContext.html#registerMetric-java.lang.String-T-int-) is the period (seconds) to publish and reset the metric. + +Last, let's increment the value when Bolt.execute() is executed. + +```java +public void execute(Tuple input) { + countMetric.incr(); + // handle tuple here. +} +``` + +Note that sample rate for topology metrics is not applied to custom metrics since we're calling incr() ourselves. + +Done! `countMetric.getValueAndReset()` is called every 60 seconds as we registered as period, and pair of ("execute_count", value) will be pushed to MetricsConsumer. + + +### Build your own metrics (worker level) + +You can register your own worker level metrics by adding them to `Config.WORKER_METRICS` for all workers in cluster, or `Config.TOPOLOGY_WORKER_METRICS` for all workers in specific topology. + +For example, we can add `worker.metrics` to storm.yaml in cluster, + +```yaml +worker.metrics: + metricA: "aaa.bbb.ccc.ddd.MetricA" + metricB: "aaa.bbb.ccc.ddd.MetricB" + ... +``` + +or put `Map` (metric name, metric class name) with key `Config.TOPOLOGY_WORKER_METRICS` to config map. + +There're some restrictions for worker level metric instances: + +A) Metrics for worker level should be kind of gauge since it is initialized and registered from SystemBolt and not exposed to user tasks. + +B) Metrics will be initialized with default constructor, and no injection for configuration or object will be performed. + +C) Bucket size (seconds) for metrics is fixed to `Config.TOPOLOGY_BUILTIN_METRICS_BUCKET_SIZE_SECS`. + + +### Builtin Metrics + +The [builtin metrics]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/builtin_metrics.clj) instrument Storm itself. + +[builtin_metrics.clj]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/builtin_metrics.clj) sets up data structures for the built-in metrics, and facade methods that the other framework components can use to update them. The metrics themselves are calculated in the calling code -- see for example [`ack-spout-msg`]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/executor.clj#358) in `clj/b/s/daemon/daemon/executor.clj` + diff --git a/docs/documentation/Multilang-protocol.md b/docs/Multilang-protocol.md similarity index 81% rename from docs/documentation/Multilang-protocol.md rename to docs/Multilang-protocol.md index 017ad328c06..45ad2bc30ca 100644 --- a/docs/documentation/Multilang-protocol.md +++ b/docs/Multilang-protocol.md @@ -14,6 +14,34 @@ ShellSpout, and ShellProcess classes. These classes implement the IBolt and ISpout interfaces and the protocol for executing a script or program via the shell using Java's ProcessBuilder class. +### Packaging of shell scripts + +By default the ShellProcess assumes that your code is packaged inside of your topology jar under the resources subdirectory of your jar and by default will change the current working directory of +the executable process to be that resources directory extracted from the jar. +A jar file does not store permissions of the files in it. This includes the execute bit that would allow a shell script to be laoded and run by the operating systme. +As such in most examples the scripts are of the form `python mybolt.py` because the python executable is already on the supervisor and mybolt is packaged in the resources directory of the jar. + +If you want to package something more complicated, like a new version of python itself, you need to instead use the blob store for this and a `.tgz` archive that does support permissions. + +See the docs on the [Blob Store](distcache-blobstore.html) for more details on how to ship a jar. + +To make a ShellBolt/ShellSpout work with executables + scripts shipped in the blob store dist cache add + +``` +changeChildCWD(false); +``` + +in the constructor of your ShellBolt/ShellSpout. The shell command will then be relative to the cwd of the worker. Where the sym-links to the resources are. + +So if I shipped python with a symlink named `newPython` and a python ShellSpout I shipped into `shell_spout.py` I would have a something like + +``` +public MyShellSpout() { + super("./newPython/bin/python", "./shell_spout.py"); + changeChildCWD(false); +} +``` + ## Output fields Output fields are part of the Thrift definition of the topology. This means that when you multilang in Java, you need to create a bolt that extends ShellBolt, implements IRichBolt, and declare the fields in `declareOutputFields` (similarly for ShellSpout). @@ -66,7 +94,7 @@ The initial handshake is the same for both types of shell components: "4": "example-bolt2" }, "taskid": 3, - // Everything below this line is only available in Storm 0.11.0+ + // Everything below this line is only available in Storm 0.10.0+ "componentid": "example-bolt" "stream->target->grouping": { "default": { @@ -82,6 +110,11 @@ The initial handshake is the same for both types of shell components: } } } + "source->stream->fields": { + "example-spout": { + "default": ["word"] + } + } } } ``` @@ -90,7 +123,7 @@ Your script should create an empty file named with its PID in this directory. e. the PID is 1234, so an empty file named 1234 is created in the directory. This file lets the supervisor know the PID so it can shutdown the process later on. -As of Storm 0.11.0, the context sent by Storm to shell components has been +As of Storm 0.10.0, the context sent by Storm to shell components has been enhanced substantially to include all aspects of the topology context available to JVM components. One key addition is the ability to determine a shell component's source and targets (i.e., inputs and outputs) in the topology via @@ -107,7 +140,7 @@ What happens next depends on the type of component: Shell spouts are synchronous. The rest happens in a while(true) loop: -* STDIN: Either a next, ack, or fail command. +* STDIN: Either a next, ack, activate, deactivate or fail command. "next" is the equivalent of ISpout's `nextTuple`. It looks like: @@ -121,6 +154,16 @@ Shell spouts are synchronous. The rest happens in a while(true) loop: {"command": "ack", "id": "1231231"} ``` +"activate" is the equivalent of ISpout's `activate`: +``` +{"command": "activate"} +``` + +"deactivate" is the equivalent of ISpout's `deactivate`: +``` +{"command": "deactivate"} +``` + "fail" looks like: ``` diff --git a/docs/Pacemaker.md b/docs/Pacemaker.md new file mode 100644 index 00000000000..63d7482c84f --- /dev/null +++ b/docs/Pacemaker.md @@ -0,0 +1,117 @@ +--- +title: Pacemaker +layout: documentation +documentation: true +--- + + +### Introduction +Pacemaker is a storm daemon designed to process heartbeats from workers. As Storm is scaled up, ZooKeeper begins to become a bottleneck due to high volumes of writes from workers doing heartbeats. Lots of writes to disk and too much traffic across the network is generated as ZooKeeper tries to maintain consistency. + +Because heartbeats are of an ephemeral nature, they do not need to be persisted to disk or synced across nodes; an in-memory store will do. This is the role of Pacemaker. Pacemaker functions as a simple in-memory key/value store with ZooKeeper-like, directory-style keys and byte array values. + +The corresponding Pacemaker client is a plugin for the `ClusterState` interface, `org.apache.storm.pacemaker.pacemaker_state_factory`. Heartbeat calls are funneled by the `ClusterState` produced by `pacemaker_state_factory` into the Pacemaker daemon, while other set/get operations are forwarded to ZooKeeper. + +------ + +### Configuration + + - `pacemaker.host` : (deprecated) The host that the Pacemaker daemon is running on + - `pacemaker.servers` : The hosts that the Pacemaker daemons are running on - This supercedes `pacemaker.host` + - `pacemaker.port` : The port that Pacemaker will listen on + - `pacemaker.max.threads` : Maximum number of threads Pacemaker daemon will use to handle requests. + - `pacemaker.childopts` : Any JVM parameters that need to go to the Pacemaker. (used by storm-deploy project) + - `pacemaker.auth.method` : The authentication method that is used (more info below) + +#### Example + +To get Pacemaker up and running, set the following option in the cluster config on all nodes: +``` +storm.cluster.state.store: "org.apache.storm.pacemaker.pacemaker_state_factory" +``` + +The Pacemaker servers also need to be set on all nodes: +``` +pacemaker.servers: + - somehost.mycompany.com + - someotherhost.mycompany.com +``` +The pacemaker.host config still works for a single pacemaker, although it has been deprecated. +``` +pacemaker.host: single_pacemaker.mycompany.com +``` + +And then start all of your daemons + +(including Pacemaker): +``` +$ storm pacemaker +``` + +The Storm cluster should now be pushing all worker heartbeats through Pacemaker. + +### Security + +Currently digest (password-based) and Kerberos security are supported. Security is currently only around reads, not writes. Writes may be performed by anyone, whereas reads may only be performed by authorized and authenticated users. This is an area for future development, as it leaves the cluster open to DoS attacks, but it prevents any sensitive information from reaching unauthorized eyes, which was the main goal. + +#### Digest +To configure digest authentication, set `pacemaker.auth.method: DIGEST` in the cluster config on the nodes hosting Nimbus and Pacemaker. +The nodes must also have `java.security.auth.login.config` set to point to a JAAS config file containing the following structure: +``` +PacemakerDigest { + username="some username" + password="some password"; +}; +``` + +Any node with these settings configured will be able to read from Pacemaker. +Worker nodes need not have these configs set, and may keep `pacemaker.auth.method: NONE` set, since they do not need to read from the Pacemaker daemon. + +#### Kerberos +To configure Kerberos authentication, set `pacemaker.auth.method: KERBEROS` in the cluster config on the nodes hosting Nimbus and Pacemaker. +The nodes must also have `java.security.auth.login.config` set to point to a JAAS config. + +The JAAS config on Nimbus must look something like this: +``` +PacemakerClient { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="/etc/keytabs/nimbus.keytab" + storeKey=true + useTicketCache=false + serviceName="pacemaker" + principal="nimbus@MY.COMPANY.COM"; +}; + +``` + +The JAAS config on Pacemaker must look something like this: +``` +PacemakerServer { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="/etc/keytabs/pacemaker.keytab" + storeKey=true + useTicketCache=false + principal="pacemaker@MY.COMPANY.COM"; +}; +``` + + - The client's user principal in the `PacemakerClient` section on the Nimbus host must match the `nimbus.daemon.user` storm cluster config value. + - The client's `serviceName` value must match the server's user principal in the `PacemakerServer` section on the Pacemaker host. + + +### Fault Tolerance + +Pacemaker runs as a single daemon instance, making it a potential Single Point of Failure. + +If Pacemaker becomes unreachable by Nimbus, through crash or network partition, the workers will continue to run, and Nimbus will repeatedly attempt to reconnect. Nimbus functionality will be disrupted, but the topologies themselves will continue to run. +In case of partition of the cluster where Nimbus and Pacemaker are on the same side of the partition, the workers that are on the other side of the partition will not be able to heartbeat, and Nimbus will reschedule the tasks elsewhere. This is probably what we want to happen anyway. + + +### ZooKeeper Comparison +Compared to ZooKeeper, Pacemaker uses less CPU, less memory, and of course no disk for the same load, thanks to lack of overhead from maintaining consistency between nodes. +On Gigabit networking, there is a theoretical limit of about 6000 nodes. However, the real limit is likely around 2000-3000 nodes. These limits have not yet been tested. +On a 270 supervisor cluster, fully scheduled with topologies, Pacemaker resource utilization was 70% of one core and nearly 1GiB of RAM on a machine with 4 `Intel(R) Xeon(R) CPU E5530 @ 2.40GHz` and 24GiB of RAM. + +Pacemaker now supports HA. Multiple Pacemaker instances can be used at once in a storm cluster to allow massive scalability. Just include the names of the Pacemaker hosts in the pacemaker.servers config and workers and Nimbus will start communicating with them. They're fault tolerant as well. The system keeps on working as long as there is at least one pacemaker left running - provided it can handle the load. diff --git a/docs/documentation/Powered-By.md b/docs/Powered-By.md similarity index 98% rename from docs/documentation/Powered-By.md rename to docs/Powered-By.md index 185e76b659b..7fcc0345b67 100644 --- a/docs/documentation/Powered-By.md +++ b/docs/Powered-By.md @@ -1,11 +1,9 @@ --- -title: Companies Using Apache Storm layout: documentation -documentation: true --- -Want to be added to this page? Send an email [here](mailto:dev@storm.apache.org). +Want to be added to this page? Send an email [here](mailto:nathan.marz@gmail.com). - +
- - - -\s*$/g,ra={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"
@@ -147,16 +145,6 @@ Health Market Science (HMS) provides data management as a service for the health
-Verisign - -

-Verisign, a global leader in domain names and Internet security, enables Internet navigation for many of the world's most recognized domain names and provides protection for enterprises around the world. Ensuring the security, stability, and resiliency of key Internet infrastructure and services, including the .COM and .NET top level domains and two of the Internet's DNS root servers, is at the heart of Verisign’s mission. Storm is a component of our data analytics stack that powers a variety of real-time applications. One example is security monitoring where we are leveraging Storm to analyze the network telemetry data of our globally distributed infrastructure in order to detect and mitigate cyber attacks. -

-
diff --git a/docs/documentation/Project-ideas.md b/docs/Project-ideas.md similarity index 93% rename from docs/documentation/Project-ideas.md rename to docs/Project-ideas.md index c8d449f2cb0..aa022ea4581 100644 --- a/docs/documentation/Project-ideas.md +++ b/docs/Project-ideas.md @@ -3,4 +3,4 @@ layout: documentation --- * **DSLs for non-JVM languages:** These DSL's should be all-inclusive and not require any Java for the creation of topologies, spouts, or bolts. Since topologies are [Thrift](http://thrift.apache.org/) structs, Nimbus is a Thrift service, and bolts can be written in any language, this is possible. * **Online machine learning algorithms:** Something like [Mahout](http://mahout.apache.org/) but for online algorithms - * **Suite of performance benchmarks:** These benchmarks should test Storm's performance on CPU and IO intensive workloads. There should be benchmarks for different classes of applications, such as stream processing (where throughput is the priority) and distributed RPC (where latency is the priority). \ No newline at end of file + * **Suite of performance benchmarks:** These benchmarks should test Storm's performance on CPU and IO intensive workloads. There should be benchmarks for different classes of applications, such as stream processing (where throughput is the priority) and distributed RPC (where latency is the priority). diff --git a/docs/README.md b/docs/README.md index c8ec761afcf..e6d939b05f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,13 @@ # Apache Storm Website and Documentation -This is the source for the Storm website and documentation. It is statically generated using [jekyll](http://jekyllrb.com). +This is the source for the Release specific part of the Apache Storm website and documentation. It is statically generated using [jekyll](http://jekyllrb.com). ## Generate Javadoc You have to generate javadoc on project root before generating document site. ``` -mvn clean install -Pdist # you may skip tests with `-DskipTests=true` to save time +mvn javadoc:javadoc -Dnotimestamp=true +mvn javadoc:aggregate -DreportOutputDirectory=./docs/ -DdestDir=javadocs -Dnotimestamp=true ``` You need to create distribution package with gpg certificate. Please refer [here](https://github.com/apache/storm/blob/master/DEVELOPER.md#packaging). @@ -30,18 +31,83 @@ Point your browser to http://localhost:4000 By default, jekyll will generate the site in a `_site` directory. +This will only show the portion of the documentation that is specific to this release. -## Publishing the Website -In order to publish the website, you must have committer access to Storm's subversion repository. +## Adding a new release to the website +In order to add a new relase, you must have committer access to Storm's subversion repository at https://svn.apache.org/repos/asf/storm/site. -The Storm website is published using Apache svnpubsub. Any changes committed to subversion will be automatically published to storm.apache.org. +Release documentation is placed under the releases directory named after the release version. Most metadata about the release will be generated automatically from the name using a jekyll plugin. Or by plaing them in the _data/releases.yml file. -To publish changes, tell jekyll to generate the site in the `publish` directory of subversion, then commit the changes: +To create a new release run the following from the main git directory +``` +mvn javadoc:javadoc -Dnotimestamp=true +mvn javadoc:aggregate -DreportOutputDirectory=./docs/ -DdestDir=javadocs -Dnotimestamp=true +mkdir ${path_to_svn}/releases/${release_name} +#Copy everything over, and compare checksums, except for things that are part of the site, +# and are not release specific like the _* directories that are jekyll specific +# assests/ css/ and README.md +rsync -ac --delete --exclude _\* --exclude assets --exclude css --exclude README.md ./docs/ ${path_to_svn}/releases/${release_name} +cd ${path_to_svn} +svn add releases/${release_name} +svn commit +``` + +to publish a new release run ``` -cd docs -jekyll build -d /path/to/svn/repo/publish -cd /path/to/svn/repo/publish +cd ${path_to_svn} +jekyll build -d publish/ +svn add publish/ #Add any new files svn commit ``` + +## How release specific docs work + +Release specific documentation is controlled by a jekyll plugin [releases.rb](./_plugins/releases.rb) + +If the plugin is running from the git repo the config `storm_release_only` is set and teh plugin will treat all of the markdown files as release sepcific file. + +If it is running from the subversion repositiory it will look in the releases driectory for release sepcific docs. + +http://svn.apache.org/viewvc/storm/site/releases/ + +Each sub directory named after the release in question. The "current" release is pointed to by a symlink in that directory called `current`. + +The plugin sets three configs for each release page. + + * version - the version number of the release/directory + * git-tree-base - a link to a directory in github that this version is on + * git-blob-base - a link to to where on github that this version is on, but should be used when pointing to files. + +If `storm_release_only` is set for the project the version is determined from the maven pom.xml and the branch is the current branch in git. If it is not set the version is determined by the name of the sub-directory and branch is assumed to be a `"v#{version}"` which corresponds with our naming conventions. For SNAPSHOT releases you will need to override this in `_data/releases.yml` + +The plugin also augments the `site.data.releases` dataset. +Each release in the list includes the following, and each can be set in `_data/releases.yml` to override what is automatically generated by the plugin. + + * git-tag-or-branch - tag or branch name on github/apache/storm + * git-tree-base - a link to a directory in github that this version is on + * git-blob-base - a link to to where on github that this version is on, but should be used when pointing to files. + * base-name - name of the release files to download, without the .tar.gz + * has-download - if this is an official release and a download link should be created. + +So if you wanted to create a link to a file on github inside the release specific docs you would create a link like + +``` +[LICENSE]([DEVELOPER.md]({{page.git-blob-base}}/LICENSE) +``` + +If you wanted to create a maven string to tell people what dependency to use you would do something like + +``` + + ... + {{version}} + +``` + +If you want to refer to a javadoc for the current release use a relative path. It will be in the javadocs subdirectory. + +``` +[TopologyBuilder](javadocs/org/apache/storm/topology/TopologyBuilder.html) +``` diff --git a/docs/documentation/Rationale.md b/docs/Rationale.md similarity index 100% rename from docs/documentation/Rationale.md rename to docs/Rationale.md diff --git a/docs/Resource_Aware_Scheduler_overview.md b/docs/Resource_Aware_Scheduler_overview.md new file mode 100644 index 00000000000..e3e2b562494 --- /dev/null +++ b/docs/Resource_Aware_Scheduler_overview.md @@ -0,0 +1,435 @@ +--- +title: Resource Aware Scheduler +layout: documentation +documentation: true +--- + +# Introduction + +The purpose of this document is to provide a description of the Resource Aware Scheduler for the Storm distributed real-time computation system. This document will provide you with both a high level description of the resource aware scheduler in Storm. Some of the benefits are using a resource aware scheduler on top of Storm is outlined in the following presentation at Hadoop Summit 2016: + +http://www.slideshare.net/HadoopSummit/resource-aware-scheduling-in-apache-storm + +# Table of Contents +1. [Using Resource Aware Scheduler](#Using-Resource-Aware-Scheduler) +2. [API Overview](#API-Overview) + 1. [Setting Memory Requirement](#Setting-Memory-Requirement) + 2. [Setting CPU Requirement](#Setting-CPU-Requirement) + 3. [Limiting the Heap Size per Worker (JVM) Process](#Limiting-the-Heap-Size-per-Worker-(JVM)Process) + 4. [Setting Available Resources on Node](#Setting-Available-Resources-on-Node) + 5. [Other Configurations](#Other-Configurations) +3. [Topology Priorities and Per User Resource](#Topology-Priorities-and-Per-User-Resource) + 1. [Setup](#Setup) + 2. [Specifying Topology Priority](#Specifying-Topology-Priority) + 3. [Specifying Scheduling Strategy](#Specifying-Scheduling-Strategy) + 4. [Specifying Topology Prioritization Strategy](#Specifying-Topology-Prioritization-Strategy) + 5. [Specifying Eviction Strategy](#Specifying-Eviction-Strategy) +4. [Profiling Resource Usage](#Profiling-Resource-Usage) +5. [Enhancements on original DefaultResourceAwareStrategy](#Enhancements-on-original-DefaultResourceAwareStrategy) + +
+## Using Resource Aware Scheduler + +The user can switch to using the Resource Aware Scheduler by setting the following in *conf/storm.yaml* + + storm.scheduler: “org.apache.storm.scheduler.resource.ResourceAwareScheduler” + +
+## API Overview + +For use with Trident, please see the [Trident RAS API](Trident-RAS-API.html) + +For a Storm Topology, the user can now specify the amount of resources a topology component (i.e. Spout or Bolt) is required to run a single instance of the component. The user can specify the resource requirement for a topology component by using the following API calls. + +
+### Setting Memory Requirement + +API to set component memory requirement: + + public T setMemoryLoad(Number onHeap, Number offHeap) + +Parameters: +* Number onHeap – The amount of on heap memory an instance of this component will consume in megabytes +* Number offHeap – The amount of off heap memory an instance of this component will consume in megabytes + +The user also has to option to just specify the on heap memory requirement if the component does not have an off heap memory need. + + public T setMemoryLoad(Number onHeap) + +Parameters: +* Number onHeap – The amount of on heap memory an instance of this component will consume + +If no value is provided for offHeap, 0.0 will be used. If no value is provided for onHeap, or if the API is never called for a component, the default value will be used. + +Example of Usage: + + SpoutDeclarer s1 = builder.setSpout("word", new TestWordSpout(), 10); + s1.setMemoryLoad(1024.0, 512.0); + builder.setBolt("exclaim1", new ExclamationBolt(), 3) + .shuffleGrouping("word").setMemoryLoad(512.0); + +The entire memory requested for this topology is 16.5 GB. That is from 10 spouts with 1GB on heap memory and 0.5 GB off heap memory each and 3 bolts with 0.5 GB on heap memory each. + +
+### Setting CPU Requirement + +API to set component CPU requirement: + + public T setCPULoad(Double amount) + +Parameters: +* Number amount – The amount of on CPU an instance of this component will consume. + +Currently, the amount of CPU resources a component requires or is available on a node is represented by a point system. CPU usage is a difficult concept to define. Different CPU architectures perform differently depending on the task at hand. They are so complex that expressing all of that in a single precise portable number is impossible. Instead we take a convention over configuration approach and are primarily concerned with rough level of CPU usage while still providing the possibility to specify amounts more fine grained. + +By convention a CPU core typically will get 100 points. If you feel that your processors are more or less powerful you can adjust this accordingly. Heavy tasks that are CPU bound will get 100 points, as they can consume an entire core. Medium tasks should get 50, light tasks 25, and tiny tasks 10. In some cases you have a task that spawns other threads to help with processing. These tasks may need to go above 100 points to express the amount of CPU they are using. If these conventions are followed the common case for a single threaded task the reported Capacity * 100 should be the number of CPU points that the task needs. + +Example of Usage: + + SpoutDeclarer s1 = builder.setSpout("word", new TestWordSpout(), 10); + s1.setCPULoad(15.0); + builder.setBolt("exclaim1", new ExclamationBolt(), 3) + .shuffleGrouping("word").setCPULoad(10.0); + builder.setBolt("exclaim2", new HeavyBolt(), 1) + .shuffleGrouping("exclaim1").setCPULoad(450.0); + +
+### Limiting the Heap Size per Worker (JVM) Process + + public void setTopologyWorkerMaxHeapSize(Number size) + +Parameters: +* Number size – The memory limit a worker process will be allocated in megabytes + +The user can limit the amount of memory resources the resource aware scheduler allocates to a single worker on a per topology basis by using the above API. This API is in place so that the users can spread executors to multiple workers. However, spreading executors to multiple workers may increase the communication latency since executors will not be able to use Disruptor Queue for intra-process communication. + +Example of Usage: + + Config conf = new Config(); + conf.setTopologyWorkerMaxHeapSize(512.0); + +
+### Setting Available Resources on Node + +A storm administrator can specify node resource availability by modifying the *conf/storm.yaml* file located in the storm home directory of that node. + +A storm administrator can specify how much available memory a node has in megabytes adding the following to *storm.yaml* + + supervisor.memory.capacity.mb: [amount] + +A storm administrator can also specify how much available CPU resources a node has available adding the following to *storm.yaml* + + supervisor.cpu.capacity: [amount] + + +Note: that the amount the user can specify for the available CPU is represented using a point system like discussed earlier. + +Example of Usage: + + supervisor.memory.capacity.mb: 20480.0 + supervisor.cpu.capacity: 100.0 + +
+### Other Configurations + +The user can set some default configurations for the Resource Aware Scheduler in *conf/storm.yaml*: + + //default value if on heap memory requirement is not specified for a component + topology.component.resources.onheap.memory.mb: 128.0 + + //default value if off heap memory requirement is not specified for a component + topology.component.resources.offheap.memory.mb: 0.0 + + //default value if CPU requirement is not specified for a component + topology.component.cpu.pcore.percent: 10.0 + + //default value for the max heap size for a worker + topology.worker.max.heap.size.mb: 768.0 + +
+## Topology Priorities and Per User Resource + +The Resource Aware Scheduler or RAS also has multitenant capabilities since many Storm users typically share a Storm cluster. Resource Aware Scheduler can allocate resources on a per user basis. Each user can be guaranteed a certain amount of resources to run his or her topologies and the Resource Aware Scheduler will meet those guarantees when possible. When the Storm cluster has extra free resources, Resource Aware Scheduler will to be able allocate additional resources to user in a fair manner. The importance of topologies can also vary. Topologies can be used for actual production or just experimentation, thus Resource Aware Scheduler will take into account the importance of a topology when determining the order in which to schedule topologies or when to evict topologies + +
+### Setup + +The resource guarantees of a user can be specified *conf/user-resource-pools.yaml*. Specify the resource guarantees of a user in the following format: + + resource.aware.scheduler.user.pools: + [UserId] + cpu: [Amount of Guarantee CPU Resources] + memory: [Amount of Guarantee Memory Resources] + +An example of what *user-resource-pools.yaml* can look like: + + resource.aware.scheduler.user.pools: + jerry: + cpu: 1000 + memory: 8192.0 + derek: + cpu: 10000.0 + memory: 32768 + bobby: + cpu: 5000.0 + memory: 16384.0 + +Please note that the specified amount of Guaranteed CPU and Memory can be either a integer or double + +
+### Specifying Topology Priority +The range of topology priorities can range form 0-29. The topologies priorities will be partitioned into several priority levels that may contain a range of priorities. +For example we can create a priority level mapping: + + PRODUCTION => 0 – 9 + STAGING => 10 – 19 + DEV => 20 – 29 + +Thus, each priority level contains 10 sub priorities. Users can set the priority level of a topology by using the following API + + conf.setTopologyPriority(int priority) + +Parameters: +* priority – an integer representing the priority of the topology + +Please note that the 0-29 range is not a hard limit. Thus, a user can set a priority number that is higher than 29. However, the property of higher the priority number, lower the importance still holds + +
+### Specifying Scheduling Strategy + +A user can specify on a per topology basis what scheduling strategy to use. Users can implement the IStrategy interface and define new strategies to schedule specific topologies. This pluggable interface was created since we realize different topologies might have different scheduling needs. A user can set the topology strategy within the topology definition by using the API: + + public void setTopologyStrategy(Class clazz) + +Parameters: +* clazz – The strategy class that implements the IStrategy interface + +Example Usage: + + conf.setTopologyStrategy(org.apache.storm.scheduler.resource.strategies.scheduling.DefaultResourceAwareStrategy.class); + +A default scheduling is provided. The DefaultResourceAwareStrategy is implemented based off the scheduling algorithm in the original paper describing resource aware scheduling in Storm: + +Peng, Boyang, Mohammad Hosseini, Zhihao Hong, Reza Farivar, and Roy Campbell. "R-storm: Resource-aware scheduling in storm." In Proceedings of the 16th Annual Middleware Conference, pp. 149-161. ACM, 2015. + +http://dl.acm.org/citation.cfm?id=2814808 + +**Please Note: Enhancements have to made on top of the original scheduling strategy as described in the paper. Please see section "Enhancements on original DefaultResourceAwareStrategy"** + +
+### Specifying Topology Prioritization Strategy + +The order of scheduling is a pluggable interface in which a user could define a strategy that prioritizes topologies. For a user to define his or her own prioritization strategy, he or she needs to implement the ISchedulingPriorityStrategy interface. A user can set the scheduling priority strategy by setting the *Config.RESOURCE_AWARE_SCHEDULER_PRIORITY_STRATEGY* to point to the class that implements the strategy. For instance: + + resource.aware.scheduler.priority.strategy: "org.apache.storm.scheduler.resource.strategies.priority.DefaultSchedulingPriorityStrategy" + +A default strategy will be provided. The following explains how the default scheduling priority strategy works. + +**DefaultSchedulingPriorityStrategy** + +The order of scheduling should be based on the distance between a user’s current resource allocation and his or her guaranteed allocation. We should prioritize the users who are the furthest away from their resource guarantee. The difficulty of this problem is that a user may have multiple resource guarantees, and another user can have another set of resource guarantees, so how can we compare them in a fair manner? Let's use the average percentage of resource guarantees satisfied as a method of comparison. + +For example: + +|User|Resource Guarantee|Resource Allocated| +|----|------------------|------------------| +|A|<10 CPU, 50GB>|<2 CPU, 40 GB>| +|B|< 20 CPU, 25GB>|<15 CPU, 10 GB>| + +User A’s average percentage satisfied of resource guarantee: + +(2/10+40/50)/2 = 0.5 + +User B’s average percentage satisfied of resource guarantee: + +(15/20+10/25)/2 = 0.575 + +Thus, in this example User A has a smaller average percentage of his or her resource guarantee satisfied than User B. Thus, User A should get priority to be allocated more resource, i.e., schedule a topology submitted by User A. + +When scheduling, RAS sorts users by the average percentage satisfied of resource guarantee and schedule topologies from users based on that ordering starting from the users with the lowest average percentage satisfied of resource guarantee. When a user’s resource guarantee is completely satisfied, the user’s average percentage satisfied of resource guarantee will be greater than or equal to 1. + +
+### Specifying Eviction Strategy +The eviction strategy is used when there are not enough free resources in the cluster to schedule new topologies. If the cluster is full, we need a mechanism to evict topologies so that user resource guarantees can be met and additional resource can be shared fairly among users. The strategy for evicting topologies is also a pluggable interface in which the user can implement his or her own topology eviction strategy. For a user to implement his or her own eviction strategy, he or she needs to implement the IEvictionStrategy Interface and set *Config.RESOURCE_AWARE_SCHEDULER_EVICTION_STRATEGY* to point to the implemented strategy class. For instance: + + resource.aware.scheduler.eviction.strategy: "org.apache.storm.scheduler.resource.strategies.eviction.DefaultEvictionStrategy" + +A default eviction strategy is provided. The following explains how the default topology eviction strategy works + +**DefaultEvictionStrategy** + +To determine if topology eviction should occur we should take into account the priority of the topology that we are trying to schedule and whether the resource guarantees for the owner of the topology have been met. + +We should never evict a topology from a user that does not have his or her resource guarantees satisfied. The following flow chart should describe the logic for the eviction process. + +![Viewing metrics with VisualVM](images/resource_aware_scheduler_default_eviction_strategy.png) + +
+## Profiling Resource Usage + +Figuring out resource usage for your topology: + +To get an idea of how much memory/CPU your topology is actually using you can add the following to your topology launch code. + + //Log all storm metrics + conf.registerMetricsConsumer(backtype.storm.metric.LoggingMetricsConsumer.class); + + //Add in per worker CPU measurement + Map workerMetrics = new HashMap(); + workerMetrics.put("CPU", "org.apache.storm.metrics.sigar.CPUMetric"); + conf.put(Config.TOPOLOGY_WORKER_METRICS, workerMetrics); + +The CPU metrics will require you to add + + + org.apache.storm + storm-metrics + 1.0.0 + + +as a topology dependency (1.0.0 or higher). + +You can then go to your topology on the UI, turn on the system metrics, and find the log that the LoggingMetricsConsumer is writing to. It will output results in the log like. + + 1454526100 node1.nodes.com:6707 -1:__system CPU {user-ms=74480, sys-ms=10780} + 1454526100 node1.nodes.com:6707 -1:__system memory/nonHeap {unusedBytes=2077536, virtualFreeBytes=-64621729, initBytes=2555904, committedBytes=66699264, maxBytes=-1, usedBytes=64621728} + 1454526100 node1.nodes.com:6707 -1:__system memory/heap {unusedBytes=573861408, virtualFreeBytes=694644256, initBytes=805306368, committedBytes=657719296, maxBytes=778502144, usedBytes=83857888} + +The metrics with -1:__system are generally metrics for the entire worker. In the example above that worker is running on node1.nodes.com:6707. These metrics are collected every 60 seconds. For the CPU you can see that over the 60 seconds this worker used 74480 + 10780 = 85260 ms of CPU time. This is equivalent to 85260/60000 or about 1.5 cores. + +The Memory usage is similar but look at the usedBytes. offHeap is 64621728 or about 62MB, and onHeap is 83857888 or about 80MB, but you should know what you set your heap to in each of your workers already. How do you divide this up per bolt/spout? That is a bit harder and may require some trial and error from your end. + +
+## * Enhancements on original DefaultResourceAwareStrategy * + +The default resource aware scheduling strategy as described in the paper above has two main scheduling phases: + +1. Task Selection - Calculate the order task/executors in a topology should be scheduled +2. Node Selection - Given a task/executor, find a node to schedule the task/executor on. + +Enhancements have been made for both scheduling phases + +### Task Selection Enhancements + +Instead of using a breadth first traversal of the topology graph to create a ordering of components and its executors, a new heuristic is used that orders components by the number of in and out edges (potential connections) of the component. This is discovered to be a more effective way to colocate executors that communicate with each other and reduce the network latency. + + +### Node Selection Enhancements + +Node selection comes down first selecting which rack (server rack) and then which node on that rack to choose. The gist of strategy in choosing a rack and node is finding the rack that has the "most" resource available and in that rack find the node with the "most" free resources. The assumption we are making for this strategy is that the node or rack with the most free resources will have the highest probability that allows us to schedule colocate the most number of executors on the node or rack to reduce network communication latency + +Racks and nodes will be sorted from best choice to worst choice. When finding an executor, the strategy will iterate through all racks and nodes, starting from best to worst, before giving up. Racks and nodes will be sorted in the following matter: + +1. How many executors are already scheduled on the rack or node + -- This is done so we move executors to schedule closer to executors that are already scheduled and running. If a topology partially crashed and a subset of the topology's executors need to be rescheduled, we want to reschedule these executors as close (network wise) as possible to the executors that healthy and running. + +2. Subordinate resource availability or the amount "effective" resources on the rack or node + -- Please refer the section on Subordinate Resource Availability + +3. Average of the all the resource availability + -- This is simply taking the average of the percent available (available resources on node or rack divied by theavailable resources on rack or cluster, repectively). This situation will only be used when "effective resources" for two objects (rack or node) are the same. Then we consider the average of all the percentages of resources as a metric for sorting. For example: + + Avail Resources: + node 1: CPU = 50 Memory = 1024 Slots = 20 + node 2: CPU = 50 Memory = 8192 Slots = 40 + node 3: CPU = 1000 Memory = 0 Slots = 0 + + Effective resources for nodes: + node 1 = 50 / (50+50+1000) = 0.045 (CPU bound) + node 2 = 50 / (50+50+1000) = 0.045 (CPU bound) + node 3 = 0 (memory and slots are 0) + +ode 1 and node 2 have the same effective resources but clearly node 2 has more resources (memory and slots) than node 1 and we would want to pick node 2 first since there is a higher probability we will be able to schedule more executors on it. This is what the phase 2 averaging does + +Thus the sorting follows the following progression. Compare based on 1) and if equal then compare based on 2) and if equal compare based on 3) and if equal we break ties by arbitrarly assigning ordering based on comparing the ids of the node or rack. + +**Subordinate Resource Availability** + +Originally the getBestClustering algorithm for RAS finds the "Best" rack based on which rack has the "most available" resources by finding the rack with the biggest sum of available memory + available across all nodes in the rack. This method is not very accurate since memory and cpu usage aree values on a different scale and the values are not normailized. This method is also not effective since it does not consider the number of slots available and it fails to identifying racks that are not schedulable due to the exhaustion of one of the resources either memory, cpu, or slots. Also the previous method does not consider failures of workers. When executors of a topology gets unassigned and needs to be scheduled again, the current logic in getBestClustering may be inadequate since it will likely return a cluster that is different from where the majority of executors from the topology is originally scheduling in. + +The new strategy/algorithm to find the "best" rack or node, I dub subordinate resource availability ordering (inspired by Dominant Resource Fairness), sorts racks and nodes by the subordinate (not dominant) resource availability. + +For example given 4 racks with the following resource availabilities + + //generate some that has alot of memory but little of cpu + rack-3 Avail [ CPU 100.0 MEM 200000.0 Slots 40 ] Total [ CPU 100.0 MEM 200000.0 Slots 40 ] + //generate some supervisors that are depleted of one resource + rack-2 Avail [ CPU 0.0 MEM 80000.0 Slots 40 ] Total [ CPU 0.0 MEM 80000.0 Slots 40 ] + //generate some that has a lot of cpu but little of memory + rack-4 Avail [ CPU 6100.0 MEM 10000.0 Slots 40 ] Total [ CPU 6100.0 MEM 10000.0 Slots 40 ] + //generate another rack of supervisors with less resources than rack-0 + rack-1 Avail [ CPU 2000.0 MEM 40000.0 Slots 40 ] Total [ CPU 2000.0 MEM 40000.0 Slots 40 ] + //best rack to choose + rack-0 Avail [ CPU 4000.0 MEM 80000.0 Slots 40( ] Total [ CPU 4000.0 MEM 80000.0 Slots 40 ] + Cluster Overall Avail [ CPU 12200.0 MEM 410000.0 Slots 200 ] Total [ CPU 12200.0 MEM 410000.0 Slots 200 ] + +It is clear that rack-0 is the best cluster since its the most balanced and can potentially schedule the most executors, while rack-2 is the worst rack since rack-2 is depleted of cpu resource thus rendering it unschedulable even though there are other resources available. + +We first calculate the resource availability percentage of all the racks for each resource by computing: + + (resource available on rack) / (resource available in cluster) + +We do this calculation to normalize the values otherwise the resource values would not be comparable. + +So for our example: + + rack-3 Avail [ CPU 0.819672131147541% MEM 48.78048780487805% Slots 20.0% ] effective resources: 0.00819672131147541 + rack-2 Avail [ 0.0% MEM 19.51219512195122% Slots 20.0% ] effective resources: 0.0 + rack-4 Avail [ CPU 50.0% MEM 2.4390243902439024% Slots 20.0% ] effective resources: 0.024390243902439025 + rack-1 Avail [ CPU 16.39344262295082% MEM 9.75609756097561% Slots 20.0% ] effective resources: 0.0975609756097561 + rack-0 Avail [ CPU 32.78688524590164% MEM 19.51219512195122% Slots 20.0% ] effective resources: 0.1951219512195122 + +The effective resource of a rack, which is also the subordinate resource, is computed by: + + MIN(resource availability percentage of {CPU, Memory, # of free Slots}). + +Then we order the racks by the effective resource. + +Thus for our example: + + Sorted rack: [rack-0, rack-1, rack-4, rack-3, rack-2] + +This metric is used in sorting for both nodes and racks. When sorting racks, we consider resources available on the rack and in the whole cluster (containing all racks). When sorting nodes, we consider resources available on the node and the resources available in the rack (sum of all resources available for all nodes in rack) + +Original Jira for this enhancement: [STORM-1766](https://issues.apache.org/jira/browse/STORM-1766) + +### Improvements in Scheduling +This section provides some experimental results on the performance benefits with the enhancements on top of the original scheduling strategy. The experiments are based off of running simulations using: + +https://github.com/jerrypeng/storm-scheduler-test-framework + +Random topologies and clusters are used in the simulation as well as a comprehensive dataset consisting of all real topologies running in all the storm clusters at Yahoo. + +The below graphs provides a comparison of how well the various strategies schedule topologies to minimize network latency. A network metric is calculated for each scheduling of a topology by each scheduling strategy. The network metric is calculated based on how many connections each executor in a topology has to make to another executor residing in the same worker (JVM process), in different worker but same host, different host, different rack. The assumption we are making is the following + +1. Intra-worker communication is the fastest +2. Inter-worker communication is fast +3. Inter-node communication is slower +4. Inter-rack communication is the slowest + +For this network metric, the larger the number is number is the more potential network latency the topology will have for this scheduling. Two types of experiments are performed. One set experiments are performed with randomly generated topologies and randomly generate clusters. The other set of experiments are performed with a dataset containing all of the running topologies at yahoo and semi-randomly generated clusters based on the size of the topology. Both set of experiments are run millions of iterations until results converge. + +For the experiments involving randomly generated topologies, an optimal strategy is implemented that exhausively finds the most optimal solution if a solution exists. The topologies and clusters used in this experiment are relatively small so that the optimal strategy traverse to solution space to find a optimal solution in a reasonable amount of time. This strategy is not run with the Yahoo topologies since the topologies are large and would take unreasonable amount of time to run, since the solutions space is W^N (ordering doesn't matter within a worker) where W is the number of workers and N is the number of executors. The NextGenStrategy represents the scheduling strategy with these enhancements. The DefaultResourceAwareStrategy represents the original scheduling strategy. The RoundRobinStrategy represents a naive strategy that simply schedules executors in a round robin fashion while respecting the resource constraints. The graph below presents averages of the network metric. A CDF graph is also presented further down. + +| Random Topologies | Yahoo topologies | +|-------------------|------------------| +|![](images/ras_new_strategy_network_metric_random.png)| ![](images/ras_new_strategy_network_metric_yahoo_topologies.png)| + +The next graph displays how close the schedulings from the respectively scheduling strategies are to the schedulings of the optimal strategy. As explained earlier, this is only done for the random generated topologies and clusters. + +| Random Topologies | +|-------------------| +|![](images/ras_new_strategy_network_metric_improvement_random.png)| + +The below graph is a CDF of the network metric: + +| Random Topologies | Yahoo topologies | +|-------------------|------------------| +|![](images/ras_new_strategy_network_cdf_random.png)| ![](images/ras_new_strategy_network_metric_cdf_yahoo_topologies.png)| + +Below is a comparison of the how long the strategies take to run: + +| Random Topologies | Yahoo topologies | +|-------------------|------------------| +|![](images/ras_new_strategy_runtime_random.png)| ![](images/ras_new_strategy_runtime_yahoo.png)| + diff --git a/docs/documentation/Running-topologies-on-a-production-cluster.md b/docs/Running-topologies-on-a-production-cluster.md similarity index 77% rename from docs/documentation/Running-topologies-on-a-production-cluster.md rename to docs/Running-topologies-on-a-production-cluster.md index ebba452b61a..802fd2e5b90 100644 --- a/docs/documentation/Running-topologies-on-a-production-cluster.md +++ b/docs/Running-topologies-on-a-production-cluster.md @@ -5,9 +5,9 @@ documentation: true --- Running topologies on a production cluster is similar to running in [Local mode](Local-mode.html). Here are the steps: -1) Define the topology (Use [TopologyBuilder](/javadoc/apidocs/backtype/storm/topology/TopologyBuilder.html) if defining using Java) +1) Define the topology (Use [TopologyBuilder](javadocs/org/apache/storm/topology/TopologyBuilder.html) if defining using Java) -2) Use [StormSubmitter](/javadoc/apidocs/backtype/storm/StormSubmitter.html) to submit the topology to the cluster. `StormSubmitter` takes as input the name of the topology, a configuration for the topology, and the topology itself. For example: +2) Use [StormSubmitter](javadocs/org/apache/storm/StormSubmitter.html) to submit the topology to the cluster. `StormSubmitter` takes as input the name of the topology, a configuration for the topology, and the topology itself. For example: ```java Config conf = new Config(); @@ -47,10 +47,10 @@ You can find out how to configure your `storm` client to talk to a Storm cluster ### Common configurations -There are a variety of configurations you can set per topology. A list of all the configurations you can set can be found [here](/javadoc/apidocs/backtype/storm/Config.html). The ones prefixed with "TOPOLOGY" can be overridden on a topology-specific basis (the other ones are cluster configurations and cannot be overridden). Here are some common ones that are set for a topology: +There are a variety of configurations you can set per topology. A list of all the configurations you can set can be found [here](javadocs/org/apache/storm/Config.html). The ones prefixed with "TOPOLOGY" can be overridden on a topology-specific basis (the other ones are cluster configurations and cannot be overridden). Here are some common ones that are set for a topology: 1. **Config.TOPOLOGY_WORKERS**: This sets the number of worker processes to use to execute the topology. For example, if you set this to 25, there will be 25 Java processes across the cluster executing all the tasks. If you had a combined 150 parallelism across all components in the topology, each worker process will have 6 tasks running within it as threads. -2. **Config.TOPOLOGY_ACKERS**: This sets the number of tasks that will track tuple trees and detect when a spout tuple has been fully processed. Ackers are an integral part of Storm's reliability model and you can read more about them on [Guaranteeing message processing](Guaranteeing-message-processing.html). +2. **Config.TOPOLOGY_ACKER_EXECUTORS**: This sets the number of executors that will track tuple trees and detect when a spout tuple has been fully processed. Ackers are an integral part of Storm's reliability model and you can read more about them on [Guaranteeing message processing](Guaranteeing-message-processing.html). By not setting this variable or setting it as null, Storm will set the number of acker executors to be equal to the number of workers configured for this topology. If this variable is set to 0, then Storm will immediately ack tuples as soon as they come off the spout, effectively disabling reliability. 3. **Config.TOPOLOGY_MAX_SPOUT_PENDING**: This sets the maximum number of spout tuples that can be pending on a single spout task at once (pending means the tuple has not been acked or failed yet). It is highly recommended you set this config to prevent queue explosion. 4. **Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS**: This is the maximum amount of time a spout tuple has to be fully completed before it is considered failed. This value defaults to 30 seconds, which is sufficient for most topologies. See [Guaranteeing message processing](Guaranteeing-message-processing.html) for more information on how Storm's reliability model works. 5. **Config.TOPOLOGY_SERIALIZATIONS**: You can register more serializers to Storm using this config so that you can use custom types within tuples. @@ -74,4 +74,4 @@ To update a running topology, the only option currently is to kill the current t The best place to monitor a topology is using the Storm UI. The Storm UI provides information about errors happening in tasks and fine-grained stats on the throughput and latency performance of each component of each running topology. -You can also look at the worker logs on the cluster machines. \ No newline at end of file +You can also look at the worker logs on the cluster machines. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 00000000000..a9d0d7feaa6 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,480 @@ +--- +title: Running Apache Storm Securely +layout: documentation +documentation: true +--- + +# Running Apache Storm Securely + +Apache Storm offers a range of configuration options when trying to secure +your cluster. By default all authentication and authorization is disabled but +can be turned on as needed. + +## Firewall/OS level Security + +You can still have a secure storm cluster without turning on formal +Authentication and Authorization. But to do so usually requires +configuring your Operating System to restrict the operations that can be done. +This is generally a good idea even if you plan on running your cluster with Auth. + +The exact detail of how to setup these precautions varies a lot and is beyond +the scope of this document. + +It is generally a good idea to enable a firewall and restrict incoming network +connections to only those originating from the cluster itself and from trusted +hosts and services, a complete list of ports storm uses are below. + +If the data your cluster is processing is sensitive it might be best to setup +IPsec to encrypt all traffic being sent between the hosts in the cluster. + +### Ports + +| Default Port | Storm Config | Client Hosts/Processes | Server | +|--------------|--------------|------------------------|--------| +| 2181 | `storm.zookeeper.port` | Nimbus, Supervisors, and Worker processes | Zookeeper | +| 6627 | `nimbus.thrift.port` | Storm clients, Supervisors, and UI | Nimbus | +| 8080 | `ui.port` | Client Web Browsers | UI | +| 8000 | `logviewer.port` | Client Web Browsers | Logviewer | +| 3772 | `drpc.port` | External DRPC Clients | DRPC | +| 3773 | `drpc.invocations.port` | Worker Processes | DRPC | +| 3774 | `drpc.http.port` | External HTTP DRPC Clients | DRPC | +| 670{0,1,2,3} | `supervisor.slots.ports` | Worker Processes | Worker Processes | + + +### UI/Logviewer + +The UI and logviewer processes provide a way to not only see what a cluster is +doing, but also manipulate running topologies. In general these processes should +not be exposed except to users of the cluster. + +Some form of Authentication is typically required, with using java servlet filters + +```yaml +ui.filter: "filter.class" +ui.filter.params: "param1":"value1" +``` +or by restricting the UI/log viewers ports to only accept connections from local +hosts, and then front them with another web server, like Apache httpd, that can +authenticate/authorize incoming connections and +proxy the connection to the storm process. To make this work the ui process must have +logviewer.port set to the port of the proxy in its storm.yaml, while the logviewers +must have it set to the actual port that they are going to bind to. + +The servlet filters are preferred because it allows individual topologies to +specificy who is and who is not allowed to access the pages associated with +them. + +Storm UI can be configured to use AuthenticationFilter from hadoop-auth. +```yaml +ui.filter: "org.apache.hadoop.security.authentication.server.AuthenticationFilter" +ui.filter.params: + "type": "kerberos" + "kerberos.principal": "HTTP/nimbus.witzend.com" + "kerberos.keytab": "/vagrant/keytabs/http.keytab" + "kerberos.name.rules": "RULE:[2:$1@$0]([jt]t@.*EXAMPLE.COM)s/.*/$MAPRED_USER/ RULE:[2:$1@$0]([nd]n@.*EXAMPLE.COM)s/.*/$HDFS_USER/DEFAULT" +``` +make sure to create a principal 'HTTP/{hostname}' (here hostname should be the one where UI daemon runs + +Once configured users needs to do kinit before accessing UI. +Ex: +curl -i --negotiate -u:anyUser -b ~/cookiejar.txt -c ~/cookiejar.txt http://storm-ui-hostname:8080/api/v1/cluster/summary + +1. Firefox: Goto about:config and search for network.negotiate-auth.trusted-uris double-click to add value "http://storm-ui-hostname:8080" +2. Google-chrome: start from command line with: google-chrome --auth-server-whitelist="*storm-ui-hostname" --auth-negotiate-delegate-whitelist="*storm-ui-hostname" +3. IE: Configure trusted websites to include "storm-ui-hostname" and allow negotiation for that website + +**Note**: For viewing any logs via `logviewer` in secure mode, all the hosts that runs `logviewer` should also be added to the above white list. For big clusters you could white list the host's domain (for e.g. set `network.negotiate-auth.trusted-uris` to `.yourdomain.com`). + +**Caution**: In AD MIT Keberos setup the key size is bigger than the default UI jetty server request header size. Make sure you set ui.header.buffer.bytes to 65536 in storm.yaml. More details are on [STORM-633](https://issues.apache.org/jira/browse/STORM-633) + + +## UI / DRPC SSL + +Both UI and DRPC allows users to configure ssl . + +### UI + +For UI users needs to set following config in storm.yaml. Generating keystores with proper keys and certs should be taken care by the user before this step. + +1. ui.https.port +2. ui.https.keystore.type (example "jks") +3. ui.https.keystore.path (example "/etc/ssl/storm_keystore.jks") +4. ui.https.keystore.password (keystore password) +5. ui.https.key.password (private key password) + +optional config +6. ui.https.truststore.path (example "/etc/ssl/storm_truststore.jks") +7. ui.https.truststore.password (truststore password) +8. ui.https.truststore.type (example "jks") + +If users want to setup 2-way auth +9. ui.https.want.client.auth (If this set to true server requests for client certifcate authentication, but keeps the connection if no authentication provided) +10. ui.https.need.client.auth (If this set to true server requires client to provide authentication) + + + + +### DRPC +similarly to UI , users need to configure following for DRPC + +1. drpc.https.port +2. drpc.https.keystore.type (example "jks") +3. drpc.https.keystore.path (example "/etc/ssl/storm_keystore.jks") +4. drpc.https.keystore.password (keystore password) +5. drpc.https.key.password (private key password) + +optional config +6. drpc.https.truststore.path (example "/etc/ssl/storm_truststore.jks") +7. drpc.https.truststore.password (truststore password) +8. drpc.https.truststore.type (example "jks") + +If users want to setup 2-way auth +9. drpc.https.want.client.auth (If this set to true server requests for client certifcate authentication, but keeps the connection if no authentication provided) +10. drpc.https.need.client.auth (If this set to true server requires client to provide authentication) + + + + + +## Authentication (Kerberos) + +Storm offers pluggable authentication support through thrift and SASL. This +example only goes off of Kerberos as it is a common setup for most big data +projects. + +Setting up a KDC and configuring kerberos on each node is beyond the scope of +this document and it is assumed that you have done that already. + +### Create Headless Principals and keytabs + +Each Zookeeper Server, Nimbus, and DRPC server will need a service principal, which, by convention, includes the FQDN of the host it will run on. Be aware that the zookeeper user *MUST* be zookeeper. +The supervisors and UI also need a principal to run as, but because they are outgoing connections they do not need to be service principals. +The following is an example of how to setup kerberos principals, but the +details may vary depending on your KDC and OS. + + +```bash +# Zookeeper (Will need one of these for each box in teh Zk ensamble) +sudo kadmin.local -q 'addprinc zookeeper/zk1.example.com@STORM.EXAMPLE.COM' +sudo kadmin.local -q "ktadd -k /tmp/zk.keytab zookeeper/zk1.example.com@STORM.EXAMPLE.COM" +# Nimbus and DRPC +sudo kadmin.local -q 'addprinc storm/storm.example.com@STORM.EXAMPLE.COM' +sudo kadmin.local -q "ktadd -k /tmp/storm.keytab storm/storm.example.com@STORM.EXAMPLE.COM" +# All UI logviewer and Supervisors +sudo kadmin.local -q 'addprinc storm@STORM.EXAMPLE.COM' +sudo kadmin.local -q "ktadd -k /tmp/storm.keytab storm@STORM.EXAMPLE.COM" +``` + +be sure to distribute the keytab(s) to the appropriate boxes and set the FS permissions so that only the headless user running ZK, or storm has access to them. + +#### Storm Kerberos Configuration + +Both storm and Zookeeper use jaas configuration files to log the user in. +Each jaas file may have multiple sections for different interfaces being used. + +To enable Kerberos authentication in storm you need to set the following storm.yaml configs +```yaml +storm.thrift.transport: "org.apache.storm.security.auth.kerberos.KerberosSaslTransportPlugin" +java.security.auth.login.config: "/path/to/jaas.conf" +``` + +Nimbus and the supervisor processes will also connect to ZooKeeper(ZK) and we want to configure them to use Kerberos for authentication with ZK. To do this append +``` +-Djava.security.auth.login.config=/path/to/jaas.conf +``` + +to the childopts of nimbus, ui, and supervisor. Here is an example given the default childopts settings at the time of writing: + +```yaml +nimbus.childopts: "-Xmx1024m -Djava.security.auth.login.config=/path/to/jaas.conf" +ui.childopts: "-Xmx768m -Djava.security.auth.login.config=/path/to/jaas.conf" +supervisor.childopts: "-Xmx256m -Djava.security.auth.login.config=/path/to/jaas.conf" +``` + +The jaas.conf file should look something like the following for the storm nodes. +The StormServer section is used by nimbus and the DRPC Nodes. It does not need to be included on supervisor nodes. +The StormClient section is used by all storm clients that want to talk to nimbus, including the ui, logviewer, and supervisor. We will use this section on the gateways as well but the structure of that will be a bit different. +The Client section is used by processes wanting to talk to zookeeper and really only needs to be included with nimbus and the supervisors. +The Server section is used by the zookeeper servers. +Having unused sections in the jaas is not a problem. + +``` +StormServer { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="$keytab" + storeKey=true + useTicketCache=false + principal="$principal"; +}; +StormClient { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="$keytab" + storeKey=true + useTicketCache=false + serviceName="$nimbus_user" + principal="$principal"; +}; +Client { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="$keytab" + storeKey=true + useTicketCache=false + serviceName="zookeeper" + principal="$principal"; +}; +Server { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="$keytab" + storeKey=true + useTicketCache=false + principal="$principal"; +}; +``` + +The following is an example based off of the keytabs generated +``` +StormServer { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="/keytabs/storm.keytab" + storeKey=true + useTicketCache=false + principal="storm/storm.example.com@STORM.EXAMPLE.COM"; +}; +StormClient { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="/keytabs/storm.keytab" + storeKey=true + useTicketCache=false + serviceName="storm" + principal="storm@STORM.EXAMPLE.COM"; +}; +Client { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="/keytabs/storm.keytab" + storeKey=true + useTicketCache=false + serviceName="zookeeper" + principal="storm@STORM.EXAMPLE.COM"; +}; +Server { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + keyTab="/keytabs/zk.keytab" + storeKey=true + useTicketCache=false + serviceName="zookeeper" + principal="zookeeper/zk1.example.com@STORM.EXAMPLE.COM"; +}; +``` + +Nimbus also will translate the principal into a local user name, so that other services can use this name. To configure this for Kerberos authentication set + +``` +storm.principal.tolocal: "org.apache.storm.security.auth.KerberosPrincipalToLocal" +``` + +This only needs to be done on nimbus, but it will not hurt on any node. +We also need to inform the topology who the supervisor daemon and the nimbus daemon are running as from a ZooKeeper perspective. + +``` +storm.zookeeper.superACL: "sasl:${nimbus-user}" +``` + +Here *nimbus-user* is the Kerberos user that nimbus uses to authenticate with ZooKeeper. If ZooKeeeper is stripping host and realm then this needs to have host and realm stripped too. + +#### ZooKeeper Ensemble + +Complete details of how to setup a secure ZK are beyond the scope of this document. But in general you want to enable SASL authentication on each server, and optionally strip off host and realm + +``` +authProvider.1 = org.apache.zookeeper.server.auth.SASLAuthenticationProvider +kerberos.removeHostFromPrincipal = true +kerberos.removeRealmFromPrincipal = true +``` + +And you want to include the jaas.conf on the command line when launching the server so it can use it can find the keytab. +``` +-Djava.security.auth.login.config=/jaas/zk_jaas.conf +``` + +#### Gateways + +Ideally the end user will only need to run kinit before interacting with storm. To make this happen seamlessly we need the default jaas.conf on the gateways to be something like + +``` +StormClient { + com.sun.security.auth.module.Krb5LoginModule required + doNotPrompt=false + useTicketCache=true + serviceName="$nimbus_user"; +}; +``` + +The end user can override this if they have a headless user that has a keytab. + +### Authorization Setup + +*Authentication* does the job of verifying who the user is, but we also need *authorization* to do the job of enforcing what each user can do. + +The preferred authorization plug-in for nimbus is The *SimpleACLAuthorizer*. To use the *SimpleACLAuthorizer*, set the following: + +```yaml +nimbus.authorizer: "org.apache.storm.security.auth.authorizer.SimpleACLAuthorizer" +``` + +DRPC has a separate authorizer configuration for it. Do not use SimpleACLAuthorizer for DRPC. + +The *SimpleACLAuthorizer* plug-in needs to know who the supervisor users are, and it needs to know about all of the administrator users, including the user running the ui daemon. + +These are set through *nimbus.supervisor.users* and *nimbus.admins* respectively. Each can either be a full Kerberos principal name, or the name of the user with host and realm stripped off. + +The Log servers have their own authorization configurations. These are set through *logs.users* and *logs.groups*. These should be set to the admin users or groups for all of the nodes in the cluster. + +When a topology is submitted, the submitting user can specify users in this list as well. The users and groups specified-in addition to the users in the cluster-wide setting-will be granted access to the submitted topology's worker logs in the logviewers. + +### Supervisors headless User and group Setup + +To ensure isolation of users in multi-tenancy, there is need to run supervisors and headless user and group unique to execution on the supervisor nodes. To enable this follow below steps. +1. Add headlessuser to all supervisor hosts. +2. Create unique group and make it the primary group for the headless user on the supervisor nodes. +3. The set following properties on storm for these supervisor nodes. + +### Multi-tenant Scheduler + +To support multi-tenancy better we have written a new scheduler. To enable this scheduler set. +```yaml +storm.scheduler: "org.apache.storm.scheduler.multitenant.MultitenantScheduler" +``` +Be aware that many of the features of this scheduler rely on storm authentication. Without them the scheduler will not know what the user is and will not isolate topologies properly. + +The goal of the multi-tenant scheduler is to provide a way to isolate topologies from one another, but to also limit the resources that an individual user can have in the cluster. + +The scheduler currently has one config that can be set either through =storm.yaml= or through a separate config file called =multitenant-scheduler.yaml= that should be placed in the same directory as =storm.yaml=. It is preferable to use =multitenant-scheduler.yaml= because it can be updated without needing to restart nimbus. + +There is currently only one config in =multitenant-scheduler.yaml=, =multitenant.scheduler.user.pools= is a map from the user name, to the maximum number of nodes that user is guaranteed to be able to use for their topologies. + +For example: + +```yaml +multitenant.scheduler.user.pools: + "evans": 10 + "derek": 10 +``` + +### Run worker processes as user who submitted the topology +By default storm runs workers as the user that is running the supervisor. This is not ideal for security. To make storm run the topologies as the user that launched them set. + +```yaml +supervisor.run.worker.as.user: true +``` + +There are several files that go along with this that are needed to be configured properly to make storm secure. + +The worker-launcher executable is a special program that allows the supervisor to launch workers as different users. For this to work it needs to be owned by root, but with the group set to be a group that only teh supervisor headless user is a part of. +It also needs to have 6550 permissions. +There is also a worker-launcher.cfg file, usually located under /etc/ that should look something like the following + +``` +storm.worker-launcher.group=$(worker_launcher_group) +min.user.id=$(min_user_id) +``` +where worker_launcher_group is the same group the supervisor is a part of, and min.user.id is set to the first real user id on the system. +This config file also needs to be owned by root and not have world or group write permissions. + +### Impersonating a user +A storm client may submit requests on behalf of another user. For example, if a `userX` submits an oozie workflow and as part of workflow execution if user `oozie` wants to submit a topology on behalf of `userX` +it can do so by leveraging the impersonation feature.In order to submit topology as some other user , you can use `StormSubmitter.submitTopologyAs` API. Alternatively you can use `NimbusClient.getConfiguredClientAs` +to get a nimbus client as some other user and perform any nimbus action(i.e. kill/rebalance/activate/deactivate) using this client. + +Impersonation authorization is disabled by default which means any user can perform impersonation. To ensure only authorized users can perform impersonation you should start nimbus with `nimbus.impersonation.authorizer` set to `org.apache.storm.security.auth.authorizer.ImpersonationAuthorizer`. +The `ImpersonationAuthorizer` uses `nimbus.impersonation.acl` as the acl to authorize users. Following is a sample nimbus config for supporting impersonation: + +```yaml +nimbus.impersonation.authorizer: org.apache.storm.security.auth.authorizer.ImpersonationAuthorizer +nimbus.impersonation.acl: + impersonating_user1: + hosts: + [comma separated list of hosts from which impersonating_user1 is allowed to impersonate other users] + groups: + [comma separated list of groups whose users impersonating_user1 is allowed to impersonate] + impersonating_user2: + hosts: + [comma separated list of hosts from which impersonating_user2 is allowed to impersonate other users] + groups: + [comma separated list of groups whose users impersonating_user2 is allowed to impersonate] +``` + +To support the oozie use case following config can be supplied: +```yaml +nimbus.impersonation.acl: + oozie: + hosts: + [oozie-host1, oozie-host2, 127.0.0.1] + groups: + [some-group-that-userX-is-part-of] +``` + +### Automatic Credentials Push and Renewal +Individual topologies have the ability to push credentials (tickets and tokens) to workers so that they can access secure services. Exposing this to all of the users can be a pain for them. +To hide this from them in the common case plugins can be used to populate the credentials, unpack them on the other side into a java Subject, and also allow Nimbus to renew the credentials if needed. +These are controlled by the following configs. topology.auto-credentials is a list of java plugins, all of which must implement IAutoCredentials interface, that populate the credentials on gateway +and unpack them on the worker side. On a kerberos secure cluster they should be set by default to point to org.apache.storm.security.auth.kerberos.AutoTGT. +nimbus.credential.renewers.classes should also be set to this value so that nimbus can periodically renew the TGT on behalf of the user. + +nimbus.credential.renewers.freq.secs controls how often the renewer will poll to see if anything needs to be renewed, but the default should be fine. + +In addition Nimbus itself can be used to get credentials on behalf of the user submitting topologies. This can be configures using nimbus.autocredential.plugins.classes which is a list +of fully qualified class names ,all of which must implement INimbusCredentialPlugin. Nimbus will invoke the populateCredentials method of all the configured implementation as part of topology +submission. You should use this config with topology.auto-credentials and nimbus.credential.renewers.classes so the credentials can be populated on worker side and nimbus can automatically renew +them. Currently there are 2 examples of using this config, AutoHDFS and AutoHBase which auto populates hdfs and hbase delegation tokens for topology submitter so they don't have to distribute keytabs +on all possible worker hosts. + +### Limits +By default storm allows any sized topology to be submitted. But ZK and others have limitations on how big a topology can actually be. The following configs allow you to limit the maximum size a topology can be. + +| YAML Setting | Description | +|------------|----------------------| +| nimbus.slots.perTopology | The maximum number of slots/workers a topology can use. | +| nimbus.executors.perTopology | The maximum number of executors/threads a topology can use. | + +### Log Cleanup +The Logviewer daemon now is also responsible for cleaning up old log files for dead topologies. + +| YAML Setting | Description | +|--------------|-------------------------------------| +| logviewer.cleanup.age.mins | How old (by last modification time) must a worker's log be before that log is considered for clean-up. (Living workers' logs are never cleaned up by the logviewer: Their logs are rolled via logback.) | +| logviewer.cleanup.interval.secs | Interval of time in seconds that the logviewer cleans up worker logs. | + + +### Allowing specific users or groups to access storm + + With SimpleACLAuthorizer any user with valid kerberos ticket can deploy a topology or do further operations such as activate, deactivate , access cluster information. + One can restrict this access by specifying nimbus.users or nimbus.groups. If nimbus.users configured only the users in the list can deploy a topology or access cluster. + Similarly nimbus.groups restrict storm cluster access to users who belong to those groups. + + To configure specify the following config in storm.yaml + +```yaml +nimbus.users: + - "testuser" +``` + +or + +```yaml +nimbus.groups: + - "storm" +``` + + +### DRPC +Hopefully more on this soon + + diff --git a/docs/STORM-UI-REST-API.md b/docs/STORM-UI-REST-API.md new file mode 100644 index 00000000000..334402c5af5 --- /dev/null +++ b/docs/STORM-UI-REST-API.md @@ -0,0 +1,1497 @@ +--- +title: Storm UI REST API +layout: documentation +documentation: true +--- + + +The Storm UI daemon provides a REST API that allows you to interact with a Storm cluster, which includes retrieving +metrics data and configuration information as well as management operations such as starting or stopping topologies. + + +# Data format + +The REST API returns JSON responses and supports JSONP. +Clients can pass a callback query parameter to wrap JSON in the callback function. +**REST API allows CORS by default.** + +# Using the UI REST API + +_Note: It is recommended to ignore undocumented elements in the JSON response because future versions of Storm may not_ +_support those elements anymore._ + + +## REST API Base URL + +The REST API is part of the UI daemon of Storm (started by `storm ui`) and thus runs on the same host and port as the +Storm UI (the UI daemon is often run on the same host as the Nimbus daemon). The port is configured by `ui.port`, +which is set to `8080` by default (see [defaults.yaml](conf/defaults.yaml)). + +The API base URL would thus be: + + http://:/api/v1/... + +You can use a tool such as `curl` to talk to the REST API: + + # Request the cluster configuration. + # Note: We assume ui.port is configured to the default value of 8080. + $ curl http://:8080/api/v1/cluster/configuration + +##Impersonating a user in secure environment +In a secure environment an authenticated user can impersonate another user. To impersonate a user the caller must pass +`doAsUser` param or header with value set to the user that the request needs to be performed as. Please see SECURITY.MD +to learn more about how to setup impersonation ACLs and authorization. The rest API uses the same configs and acls that +are used by nimbus. + +Examples: + +```no-highlight + 1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1425844354\?doAsUser=testUSer1 + 2. curl 'http://localhost:8080/api/v1/topology/wordcount-1-1425844354/activate' -X POST -H 'doAsUser:testUSer1' +``` + +## GET Operations + +### /api/v1/cluster/configuration (GET) + +Returns the cluster configuration. + +Sample response (does not include all the data fields): + +```json + { + "dev.zookeeper.path": "/tmp/dev-storm-zookeeper", + "topology.tick.tuple.freq.secs": null, + "topology.builtin.metrics.bucket.size.secs": 60, + "topology.fall.back.on.java.serialization": true, + "topology.max.error.report.per.interval": 5, + "zmq.linger.millis": 5000, + "topology.skip.missing.kryo.registrations": false, + "storm.messaging.netty.client_worker_threads": 1, + "ui.childopts": "-Xmx768m", + "storm.zookeeper.session.timeout": 20000, + "nimbus.reassign": true, + "topology.trident.batch.emit.interval.millis": 500, + "storm.messaging.netty.flush.check.interval.ms": 10, + "nimbus.monitor.freq.secs": 10, + "logviewer.childopts": "-Xmx128m", + "java.library.path": "/usr/local/lib:/opt/local/lib:/usr/lib", + "topology.executor.send.buffer.size": 1024, + } +``` + +### /api/v1/cluster/summary (GET) + +Returns cluster summary information such as nimbus uptime or number of supervisors. + +Response fields: + +|Field |Value|Description +|--- |--- |--- +|stormVersion|String| Storm version| +|supervisors|Integer| Number of supervisors running| +|topologies| Integer| Number of topologies running| +|slotsTotal| Integer|Total number of available worker slots| +|slotsUsed| Integer| Number of worker slots used| +|slotsFree| Integer |Number of worker slots available| +|executorsTotal| Integer |Total number of executors| +|tasksTotal| Integer |Total tasks| +|schedulerDisplayResource| Boolean | Whether to display scheduler resource information| +|totalMem| Double | The total amount of memory in the cluster in MB| +|totalCpu| Double | The total amount of CPU in the cluster| +|availMem| Double | The amount of available memory in the cluster in MB| +|availCpu| Double | The amount of available cpu in the cluster| +|memAssignedPercentUtil| Double | The percent utilization of assigned memory resources in cluster| +|cpuAssignedPercentUtil| Double | The percent utilization of assigned CPU resources in cluster| + +Sample response: + +```json + { + "stormVersion": "0.9.2-incubating-SNAPSHOT", + "supervisors": 1, + "slotsTotal": 4, + "slotsUsed": 3, + "slotsFree": 1, + "executorsTotal": 28, + "tasksTotal": 28, + "schedulerDisplayResource": true, + "totalMem": 4096.0, + "totalCpu": 400.0, + "availMem": 1024.0, + "availCPU": 250.0, + "memAssignedPercentUtil": 75.0, + "cpuAssignedPercentUtil": 37.5 + } +``` + +### /api/v1/supervisor/summary (GET) + +Returns summary information for all supervisors. + +Response fields: + +|Field |Value|Description| +|--- |--- |--- +|id| String | Supervisor's id| +|host| String| Supervisor's host name| +|uptime| String| Shows how long the supervisor is running| +|uptimeSeconds| Integer| Shows how long the supervisor is running in seconds| +|slotsTotal| Integer| Total number of available worker slots for this supervisor| +|slotsUsed| Integer| Number of worker slots used on this supervisor| +|schedulerDisplayResource| Boolean | Whether to display scheduler resource information| +|totalMem| Double| Total memory capacity on this supervisor| +|totalCpu| Double| Total CPU capacity on this supervisor| +|usedMem| Double| Used memory capacity on this supervisor| +|usedCpu| Double| Used CPU capacity on this supervisor| + +Sample response: + +```json +{ + "supervisors": [ + { + "id": "0b879808-2a26-442b-8f7d-23101e0c3696", + "host": "10.11.1.7", + "uptime": "5m 58s", + "uptimeSeconds": 358, + "slotsTotal": 4, + "slotsUsed": 3, + "totalMem": 3000, + "totalCpu": 400, + "usedMem": 1280, + "usedCPU": 160 + } + ], + "schedulerDisplayResource": true +} +``` + +### /api/v1/nimbus/summary (GET) + +Returns summary information for all nimbus hosts. + +Response fields: + +|Field |Value|Description| +|--- |--- |--- +|host| String | Nimbus' host name| +|port| int| Nimbus' port number| +|status| String| Possible values are Leader, Not a Leader, Dead| +|nimbusUpTime| String| Shows since how long the nimbus has been running| +|nimbusUpTimeSeconds| String| Shows since how long the nimbus has been running in seconds| +|nimbusLogLink| String| Logviewer url to view the nimbus.log| +|version| String| Version of storm this nimbus host is running| + +Sample response: + +```json +{ + "nimbuses":[ + { + "host":"192.168.202.1", + "port":6627, + "nimbusLogLink":"http:\/\/192.168.202.1:8000\/log?file=nimbus.log", + "status":Leader, + "version":"0.10.0-SNAPSHOT", + "nimbusUpTime":"3m 33s", + "nimbusUpTimeSeconds":"213" + } + ] +} +``` + +### /api/v1/history/summary (GET) + +Returns a list of all running topologies' IDs submitted by the current user. + +Response fields: + +|Field |Value | Description| +|--- |--- |--- +|topo-history| List| List of Topologies' IDs| + +Sample response: + +```json +{ + "topo-history":[ + "wc6-1-1446571009", + "wc8-2-1446587178" + ] +} +``` + +### /api/v1/supervisor (GET) + +Returns summary for a supervisor by id, or all supervisors running on a host. + +Examples: + +```no-highlight + 1. By host: http://ui-daemon-host-name:8080/api/v1/supervisor?host=supervisor-daemon-host-name + 2. By id: http://ui-daemon-host-name:8080/api/v1/supervisor?id=f5449110-1daa-43e2-89e3-69917b16dec9-192.168.1.1 +``` + +Request parameters: + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String. Supervisor id | If specified, respond with the supervisor and worker stats with id. Note that when id is specified, the host argument is ignored. | +|host |String. Host name| If specified, respond with all supervisors and worker stats in the host (normally just one)| +|sys |String. Values 1 or 0. Default value 0| Controls including sys stats part of the response| + +Response fields: + +|Field |Value|Description| +|--- |--- |--- +|supervisors| Array| Array of supervisor summaries| +|workers| Array| Array of worker summaries | +|schedulerDisplayResource| Boolean | Whether to display scheduler resource information| + +Each supervisor is defined by: + +|Field |Value|Description| +|--- |--- |--- +|id| String | Supervisor's id| +|host| String| Supervisor's host name| +|uptime| String| Shows how long the supervisor is running| +|uptimeSeconds| Integer| Shows how long the supervisor is running in seconds| +|slotsTotal| Integer| Total number of worker slots for this supervisor| +|slotsUsed| Integer| Number of worker slots used on this supervisor| +|schedulerDisplayResource| Boolean | Whether to display scheduler resource information| +|totalMem| Double| Total memory capacity on this supervisor| +|totalCpu| Double| Total CPU capacity on this supervisor| +|usedMem| Double| Used memory capacity on this supervisor| +|usedCpu| Double| Used CPU capacity on this supervisor| + +Each worker is defined by: + +|Field |Value |Description| +|-------|-------|-----------| +|supervisorId | String| Supervisor's id| +|host | String | Worker's host name| +|port | Integer | Worker's port| +|topologyId | String | Topology Id| +|topologyName | String | Topology Name| +|executorsTotal | Integer | Number of executors used by the topology in this worker| +|assignedMemOnHeap | Double | Assigned On-Heap Memory by Scheduler (MB)| +|assignedMemOffHeap | Double | Assigned Off-Heap Memory by Scheduler (MB)| +|assignedCpu | Number | Assigned CPU by Scheduler (%)| +|componentNumTasks | Dictionary | Components -> # of executing tasks| +|uptime| String| Shows how long the worker is running| +|uptimeSeconds| Integer| Shows how long the worker is running in seconds| +|workerLogLink | String | Link to worker log viewer page| + +Sample response: + +```json +{ + "supervisors": [{ + "totalMem": 4096.0, + "host":"192.168.10.237", + "id":"bdfe8eff-f1d8-4bce-81f5-9d3ae1bf432e-169.254.129.212", + "uptime":"7m 8s", + "totalCpu":400.0, + "usedCpu":495.0, + "usedMem":3432.0, + "slotsUsed":2, + "version":"0.10.1", + "slotsTotal":4, + "uptimeSeconds":428 + }], + "schedulerDisplayResource":true, + "workers":[{ + "topologyName":"ras", + "topologyId":"ras-4-1460229987", + "host":"192.168.10.237", + "supervisorId":"bdfe8eff-f1d8-4bce-81f5-9d3ae1bf432e-169.254.129.212", + "assignedMemOnHeap":704.0, + "uptime":"2m 47s", + "uptimeSeconds":167, + "port":6707, + "workerLogLink":"http:\/\/192.168.10.237:8000\/log?file=ras-4-1460229987%2F6707%2Fworker.log", + "componentNumTasks": { + "word":5 + }, + "executorsTotal":8, + "assignedCpu":130.0, + "assignedMemOffHeap":80.0 + }, + { + "topologyName":"ras", + "topologyId":"ras-4-1460229987", + "host":"192.168.10.237", + "supervisorId":"bdfe8eff-f1d8-4bce-81f5-9d3ae1bf432e-169.254.129.212", + "assignedMemOnHeap":904.0, + "uptime":"2m 53s", + "port":6706, + "workerLogLink":"http:\/\/192.168.10.237:8000\/log?file=ras-4-1460229987%2F6706%2Fworker.log", + "componentNumTasks":{ + "exclaim2":2, + "exclaim1":3, + "word":5 + }, + "executorsTotal":10, + "uptimeSeconds":173, + "assignedCpu":165.0, + "assignedMemOffHeap":80.0 + }] +} +``` + +### /api/v1/topology/summary (GET) + +Returns summary information for all topologies. + +Response fields: + +|Field |Value | Description| +|--- |--- |--- +|id| String| Topology Id| +|name| String| Topology Name| +|status| String| Topology Status| +|uptime| String| Shows how long the topology is running| +|uptimeSeconds| Integer| Shows how long the topology is running in seconds| +|tasksTotal| Integer |Total number of tasks for this topology| +|workersTotal| Integer |Number of workers used for this topology| +|executorsTotal| Integer |Number of executors used for this topology| +|replicationCount| Integer |Number of nimbus hosts on which this topology code is replicated| +|requestedMemOnHeap| Double|Requested On-Heap Memory by User (MB) +|requestedMemOffHeap| Double|Requested Off-Heap Memory by User (MB)| +|requestedTotalMem| Double|Requested Total Memory by User (MB)| +|requestedCpu| Double|Requested CPU by User (%)| +|assignedMemOnHeap| Double|Assigned On-Heap Memory by Scheduler (MB)| +|assignedMemOffHeap| Double|Assigned Off-Heap Memory by Scheduler (MB)| +|assignedTotalMem| Double|Assigned Total Memory by Scheduler (MB)| +|assignedCpu| Double|Assigned CPU by Scheduler (%)| +|schedulerDisplayResource| Boolean | Whether to display scheduler resource information| + +Sample response: + +```json +{ + "topologies": [ + { + "id": "WordCount3-1-1402960825", + "name": "WordCount3", + "status": "ACTIVE", + "uptime": "6m 5s", + "uptimeSeconds": 365, + "tasksTotal": 28, + "workersTotal": 3, + "executorsTotal": 28, + "replicationCount": 1, + "requestedMemOnHeap": 640, + "requestedMemOffHeap": 128, + "requestedTotalMem": 768, + "requestedCpu": 80, + "assignedMemOnHeap": 640, + "assignedMemOffHeap": 128, + "assignedTotalMem": 768, + "assignedCpu": 80 + } + ], + "schedulerDisplayResource": true +} +``` + +### /api/v1/topology-workers/:id (GET) + +Returns the worker' information (host and port) for a topology. + +Response fields: + +|Field |Value | Description| +|--- |--- |--- +|hostPortList| List| Workers' information for a topology| +|name| Integer| Logviewer Port| + +Sample response: + +```json +{ + "hostPortList":[ + { + "host":"192.168.202.2", + "port":6701 + }, + { + "host":"192.168.202.2", + "port":6702 + }, + { + "host":"192.168.202.3", + "port":6700 + } + ], + "logviewerPort":8000 +} +``` + +### /api/v1/topology/:id (GET) + +Returns topology information and statistics. Substitute id with topology id. + +Request parameters: + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|window |String. Default value :all-time| Window duration for metrics in seconds| +|sys |String. Values 1 or 0. Default value 0| Controls including sys stats part of the response| + + +Response fields: + +|Field |Value |Description| +|--- |--- |--- +|id| String| Topology Id| +|name| String |Topology Name| +|uptime| String |How long the topology has been running| +|uptimeSeconds| Integer |How long the topology has been running in seconds| +|status| String |Current status of the topology, e.g. "ACTIVE"| +|tasksTotal| Integer |Total number of tasks for this topology| +|workersTotal| Integer |Number of workers used for this topology| +|executorsTotal| Integer |Number of executors used for this topology| +|msgTimeout| Integer | Number of seconds a tuple has before the spout considers it failed | +|windowHint| String | window param value in "hh mm ss" format. Default value is "All Time"| +|schedulerDisplayResource| Boolean | Whether to display scheduler resource information| +|replicationCount| Integer |Number of nimbus hosts on which this topology code is replicated| +|debug| Boolean | If debug is enabled for the topology| +|samplingPct| Double| Controls downsampling of events before they are sent to event log (percentage)| +|assignedMemOnHeap| Double|Assigned On-Heap Memory by Scheduler (MB) +|assignedMemOffHeap| Double|Assigned Off-Heap Memory by Scheduler (MB)| +|assignedTotalMem| Double|Assigned Off-Heap + On-Heap Memory by Scheduler(MB)| +|assignedCpu| Double|Assigned CPU by Scheduler(%)| +|requestedMemOnHeap| Double|Requested On-Heap Memory by User (MB) +|requestedMemOffHeap| Double|Requested Off-Heap Memory by User (MB)| +|requestedCpu| Double|Requested CPU by User (%)| +|topologyStats| Array | Array of all the topology related stats per time window| +|topologyStats.windowPretty| String |Duration passed in HH:MM:SS format| +|topologyStats.window| String |User requested time window for metrics| +|topologyStats.emitted| Long |Number of messages emitted in given window| +|topologyStats.trasferred| Long |Number messages transferred in given window| +|topologyStats.completeLatency| String (double value returned in String format) |Total latency for processing the message| +|topologyStats.acked| Long |Number of messages acked in given window| +|topologyStats.failed| Long |Number of messages failed in given window| +|workers| Array | Array of workers in topology| +|workers.supervisorId | String| Supervisor's id| +|workers.host | String | Worker's host name| +|workers.port | Integer | Worker's port| +|workers.topologyId | String | Topology Id| +|workers.topologyName | String | Topology Name| +|workers.executorsTotal | Integer | Number of executors used by the topology in this worker| +|workers.assignedMemOnHeap | Double | Assigned On-Heap Memory by Scheduler (MB)| +|workers.assignedMemOffHeap | Double | Assigned Off-Heap Memory by Scheduler (MB)| +|workers.assignedCpu | Number | Assigned CPU by Scheduler (%)| +|workers.componentNumTasks | Dictionary | Components -> # of executing tasks| +|workers.uptime| String| Shows how long the worker is running| +|workers.uptimeSeconds| Integer| Shows how long the worker is running in seconds| +|workers.workerLogLink | String | Link to worker log viewer page| +|spouts| Array | Array of all the spout components in the topology| +|spouts.spoutId| String |Spout id| +|spouts.executors| Integer |Number of executors for the spout| +|spouts.emitted| Long |Number of messages emitted in given window | +|spouts.completeLatency| String (double value returned in String format) |Total latency for processing the message| +|spouts.transferred| Long |Total number of messages transferred in given window| +|spouts.tasks| Integer |Total number of tasks for the spout| +|spouts.lastError| String |Shows the last error happened in a spout| +|spouts.errorLapsedSecs| Integer | Number of seconds elapsed since that last error happened in a spout| +|spouts.errorWorkerLogLink| String | Link to the worker log that reported the exception | +|spouts.acked| Long |Number of messages acked| +|spouts.failed| Long |Number of messages failed| +|spouts.requestedMemOnHeap| Double|Requested On-Heap Memory by User (MB) +|spouts.requestedMemOffHeap| Double|Requested Off-Heap Memory by User (MB)| +|spouts.requestedCpu| Double|Requested CPU by User (%)| +|bolts| Array | Array of bolt components in the topology| +|bolts.boltId| String |Bolt id| +|bolts.capacity| String (double value returned in String format) |This value indicates number of messages executed * average execute latency / time window| +|bolts.processLatency| String (double value returned in String format) |Average time of the bolt to ack a message after it was received| +|bolts.executeLatency| String (double value returned in String format) |Average time to run the execute method of the bolt| +|bolts.executors| Integer |Number of executor tasks in the bolt component| +|bolts.tasks| Integer |Number of instances of bolt| +|bolts.acked| Long |Number of tuples acked by the bolt| +|bolts.failed| Long |Number of tuples failed by the bolt| +|bolts.lastError| String |Shows the last error occurred in the bolt| +|bolts.errorLapsedSecs| Integer |Number of seconds elapsed since that last error happened in a bolt| +|bolts.errorWorkerLogLink| String | Link to the worker log that reported the exception | +|bolts.emitted| Long |Number of tuples emitted| +|bolts.requestedMemOnHeap| Double|Requested On-Heap Memory by User (MB) +|bolts.requestedMemOffHeap| Double|Requested Off-Heap Memory by User (MB)| +|bolts.requestedCpu| Double|Requested CPU by User (%)| + +Examples: + +```no-highlight + 1. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825 + 2. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825?sys=1 + 3. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825?window=600 +``` + +Sample response: + +```json + { + "name": "WordCount3", + "id": "WordCount3-1-1402960825", + "workersTotal": 3, + "window": "600", + "status": "ACTIVE", + "tasksTotal": 28, + "executorsTotal": 28, + "uptime": "29m 19s", + "uptimeSeconds": 1759, + "msgTimeout": 30, + "windowHint": "10m 0s", + "schedulerDisplayResource": true, + "topologyStats": [ + { + "windowPretty": "10m 0s", + "window": "600", + "emitted": 397960, + "transferred": 213380, + "completeLatency": "0.000", + "acked": 213460, + "failed": 0 + }, + { + "windowPretty": "3h 0m 0s", + "window": "10800", + "emitted": 1190260, + "transferred": 638260, + "completeLatency": "0.000", + "acked": 638280, + "failed": 0 + }, + { + "windowPretty": "1d 0h 0m 0s", + "window": "86400", + "emitted": 1190260, + "transferred": 638260, + "completeLatency": "0.000", + "acked": 638280, + "failed": 0 + }, + { + "windowPretty": "All time", + "window": ":all-time", + "emitted": 1190260, + "transferred": 638260, + "completeLatency": "0.000", + "acked": 638280, + "failed": 0 + } + ], + "workers":[{ + "topologyName":"WordCount3", + "topologyId":"WordCount3-1-1402960825", + "host":"192.168.10.237", + "supervisorId":"bdfe8eff-f1d8-4bce-81f5-9d3ae1bf432e-169.254.129.212", + "uptime":"2m 47s", + "uptimeSeconds":167, + "port":6707, + "workerLogLink":"http:\/\/192.168.10.237:8000\/log?file=WordCount3-1-1402960825%2F6707%2Fworker.log", + "componentNumTasks": { + "spout":5 + }, + "executorsTotal":8, + "assignedMemOnHeap":704.0, + "assignedCpu":130.0, + "assignedMemOffHeap":80.0 + }], + "spouts": [ + { + "executors": 5, + "emitted": 28880, + "completeLatency": "0.000", + "transferred": 28880, + "acked": 0, + "spoutId": "spout", + "tasks": 5, + "lastError": "", + "errorLapsedSecs": null, + "failed": 0 + } + ], + "bolts": [ + { + "executors": 12, + "emitted": 184580, + "transferred": 0, + "acked": 184640, + "executeLatency": "0.048", + "tasks": 12, + "executed": 184620, + "processLatency": "0.043", + "boltId": "count", + "lastError": "", + "errorLapsedSecs": null, + "capacity": "0.003", + "failed": 0 + }, + { + "executors": 8, + "emitted": 184500, + "transferred": 184500, + "acked": 28820, + "executeLatency": "0.024", + "tasks": 8, + "executed": 28780, + "processLatency": "2.112", + "boltId": "split", + "lastError": "", + "errorLapsedSecs": null, + "capacity": "0.000", + "failed": 0 + } + ], + "configuration": { + "storm.id": "WordCount3-1-1402960825", + "dev.zookeeper.path": "/tmp/dev-storm-zookeeper", + "topology.tick.tuple.freq.secs": null, + "topology.builtin.metrics.bucket.size.secs": 60, + "topology.fall.back.on.java.serialization": true, + "topology.max.error.report.per.interval": 5, + "zmq.linger.millis": 5000, + "topology.skip.missing.kryo.registrations": false, + "storm.messaging.netty.client_worker_threads": 1, + "ui.childopts": "-Xmx768m", + "storm.zookeeper.session.timeout": 20000, + "nimbus.reassign": true, + "topology.trident.batch.emit.interval.millis": 500, + "storm.messaging.netty.flush.check.interval.ms": 10, + "nimbus.monitor.freq.secs": 10, + "logviewer.childopts": "-Xmx128m", + "java.library.path": "/usr/local/lib:/opt/local/lib:/usr/lib", + "topology.executor.send.buffer.size": 1024, + "storm.local.dir": "storm-local", + "storm.messaging.netty.buffer_size": 5242880, + "supervisor.worker.start.timeout.secs": 120, + "topology.enable.message.timeouts": true, + "nimbus.cleanup.inbox.freq.secs": 600, + "nimbus.inbox.jar.expiration.secs": 3600, + "drpc.worker.threads": 64, + "topology.worker.shared.thread.pool.size": 4, + "nimbus.seeds": [ + "hw10843.local" + ], + "storm.messaging.netty.min_wait_ms": 100, + "storm.zookeeper.port": 2181, + "transactional.zookeeper.port": null, + "topology.executor.receive.buffer.size": 1024, + "transactional.zookeeper.servers": null, + "storm.zookeeper.root": "/storm", + "storm.zookeeper.retry.intervalceiling.millis": 30000, + "supervisor.enable": true, + "storm.messaging.netty.server_worker_threads": 1 + }, + "replicationCount": 1 +} +``` + +### /api/v1/topology/:id/metrics + +Returns detailed metrics for topology. It shows metrics per component, which are aggregated by stream. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|window |String. Default value :all-time| window duration for metrics in seconds| +|sys |String. Values 1 or 0. Default value 0| Controls including sys stats part of the response| + +Response fields: + +|Field |Value |Description| +|--- |--- |--- +|window |String. Default value ":all-time" | window duration for metrics in seconds| + |windowHint| String | window param value in "hh mm ss" format. Default value is "All Time"| +|spouts| Array | Array of all the spout components in the topology| +|spouts.id| String |Spout id| +|spouts.emitted| Array | Array of all the output streams this spout emits messages | +|spouts.emitted.stream_id| String | Stream id for this stream | +|spouts.emitted.value| Long | Number of messages emitted in given window| +|spouts.transferred | Array | Array of all the output streams this spout transfers messages | +|spouts.transferred.stream_id| String | Stream id for this stream | +|spouts.transferred.value| Long |Number messages transferred in given window| +|spouts.acked| Array | Array of all the output streams this spout receives ack of messages | +|spouts.acked.stream_id| String | Stream id for this stream | +|spouts.acked.value| Long |Number of messages acked in given window| +|spouts.failed| Array | Array of all the output streams this spout receives fail of messages | +|spouts.failed.stream_id| String | Stream id for this stream | +|spouts.failed.value| Long |Number of messages failed in given window| +|spouts.complete_ms_avg| Array | Array of all the output streams this spout receives ack of messages | +|spouts.complete_ms_avg.stream_id| String | Stream id for this stream | +|spouts.complete_ms_avg.value| String (double value returned in String format) | Total latency for processing the message| +|bolts| Array | Array of all the bolt components in the topology| +|bolts.id| String |Bolt id| +|bolts.emitted| Array | Array of all the output streams this bolt emits messages | +|bolts.emitted.stream_id| String | Stream id for this stream | +|bolts.emitted.value| Long | Number of messages emitted in given window| +|bolts.transferred | Array | Array of all the output streams this bolt transfers messages | +|bolts.transferred.stream_id| String | Stream id for this stream | +|bolts.transferred.value| Long |Number messages transferred in given window| +|bolts.acked| Array | Array of all the input streams this bolt acknowledges of messages | +|bolts.acked.component_id| String | Component id for this stream | +|bolts.acked.stream_id| String | Stream id for this stream | +|bolts.acked.value| Long |Number of messages acked in given window| +|bolts.failed| Array | Array of all the input streams this bolt receives fail of messages | +|bolts.failed.component_id| String | Component id for this stream | +|bolts.failed.stream_id| String | Stream id for this stream | +|bolts.failed.value| Long |Number of messages failed in given window| +|bolts.process_ms_avg| Array | Array of all the input streams this spout acks messages | +|bolts.process_ms_avg.component_id| String | Component id for this stream | +|bolts.process_ms_avg.stream_id| String | Stream id for this stream | +|bolts.process_ms_avg.value| String (double value returned in String format) |Average time of the bolt to ack a message after it was received| +|bolts.executed| Array | Array of all the input streams this bolt executes messages | +|bolts.executed.component_id| String | Component id for this stream | +|bolts.executed.stream_id| String | Stream id for this stream | +|bolts.executed.value| Long |Number of messages executed in given window| +|bolts.executed_ms_avg| Array | Array of all the output streams this spout receives ack of messages | +|bolts.executed_ms_avg.component_id| String | Component id for this stream | +|bolts.executed_ms_avg.stream_id| String | Stream id for this stream | +|bolts.executed_ms_avg.value| String (double value returned in String format) | Average time to run the execute method of the bolt| + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/metrics +1. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/metrics?sys=1 +2. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/metrics?window=600 +``` + +Sample response: + +```json +{ + "window":":all-time", + "window-hint":"All time", + "spouts":[ + { + "id":"spout", + "emitted":[ + { + "stream_id":"__metrics", + "value":20 + }, + { + "stream_id":"default", + "value":17350280 + }, + { + "stream_id":"__ack_init", + "value":17328160 + }, + { + "stream_id":"__system", + "value":20 + } + ], + "transferred":[ + { + "stream_id":"__metrics", + "value":20 + }, + { + "stream_id":"default", + "value":17350280 + }, + { + "stream_id":"__ack_init", + "value":17328160 + }, + { + "stream_id":"__system", + "value":0 + } + ], + "acked":[ + { + "stream_id":"default", + "value":17339180 + } + ], + "failed":[ + + ], + "complete_ms_avg":[ + { + "stream_id":"default", + "value":"920.497" + } + ] + } + ], + "bolts":[ + { + "id":"count", + "emitted":[ + { + "stream_id":"__metrics", + "value":120 + }, + { + "stream_id":"default", + "value":190748180 + }, + { + "stream_id":"__ack_ack", + "value":190718100 + }, + { + "stream_id":"__system", + "value":20 + } + ], + "transferred":[ + { + "stream_id":"__metrics", + "value":120 + }, + { + "stream_id":"default", + "value":0 + }, + { + "stream_id":"__ack_ack", + "value":190718100 + }, + { + "stream_id":"__system", + "value":0 + } + ], + "acked":[ + { + "component_id":"split", + "stream_id":"default", + "value":190733160 + } + ], + "failed":[ + + ], + "process_ms_avg":[ + { + "component_id":"split", + "stream_id":"default", + "value":"0.004" + } + ], + "executed":[ + { + "component_id":"split", + "stream_id":"default", + "value":190733140 + } + ], + "executed_ms_avg":[ + { + "component_id":"split", + "stream_id":"default", + "value":"0.005" + } + ] + }, + { + "id":"split", + "emitted":[ + { + "stream_id":"__metrics", + "value":60 + }, + { + "stream_id":"default", + "value":190754740 + }, + { + "stream_id":"__ack_ack", + "value":17317580 + }, + { + "stream_id":"__system", + "value":20 + } + ], + "transferred":[ + { + "stream_id":"__metrics", + "value":60 + }, + { + "stream_id":"default", + "value":190754740 + }, + { + "stream_id":"__ack_ack", + "value":17317580 + }, + { + "stream_id":"__system", + "value":0 + } + ], + "acked":[ + { + "component_id":"spout", + "stream_id":"default", + "value":17339180 + } + ], + "failed":[ + + ], + "process_ms_avg":[ + { + "component_id":"spout", + "stream_id":"default", + "value":"0.051" + } + ], + "executed":[ + { + "component_id":"spout", + "stream_id":"default", + "value":17339240 + } + ], + "executed_ms_avg":[ + { + "component_id":"spout", + "stream_id":"default", + "value":"0.052" + } + ] + } + ] +} +``` + +### /api/v1/topology/:id/component/:component (GET) + +Returns detailed metrics and executor information + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|component |String (required)| Component Id | +|window |String. Default value :all-time| window duration for metrics in seconds| +|sys |String. Values 1 or 0. Default value 0| controls including sys stats part of the response| + +Response fields: + +|Field |Value |Description| +|--- |--- |--- +|user | String | Topology owner| +|id | String | Component id| +|encodedId | String | URL encoded component id| +|name | String | Topology name| +|executors| Integer |Number of executor tasks in the component| +|tasks| Integer |Number of instances of component| +|requestedMemOnHeap| Double|Requested On-Heap Memory by User (MB) +|requestedMemOffHeap| Double|Requested Off-Heap Memory by User (MB)| +|requestedCpu| Double|Requested CPU by User (%)| +|schedulerDisplayResource| Boolean | Whether to display scheduler resource information| +|topologyId| String | Topology id| +|topologyStatus| String | Topology status| +|encodedTopologyId| String | URL encoded topology id| +|window |String. Default value "All Time" | window duration for metrics in seconds| +|componentType | String | component type: SPOUT or BOLT| +|windowHint| String | window param value in "hh mm ss" format. Default value is "All Time"| +|debug| Boolean | If debug is enabled for the component| +|samplingPct| Double| Controls downsampling of events before they are sent to event log (percentage)| +|eventLogLink| String| URL viewer link to event log (debug mode)| +|profilingAndDebuggingCapable| Boolean |true if there is support for Profiling and Debugging Actions| +|profileActionEnabled| Boolean |true if worker profiling (Java Flight Recorder) is enabled| +|profilerActive| Array |Array of currently active Profiler Actions| +|componentErrors| Array of Errors | List of component errors| +|componentErrors.errorTime| Long | Timestamp when the exception occurred (Prior to 0.11.0, this field was named 'time'.)| +|componentErrors.errorHost| String | host name for the error| +|componentErrors.errorPort| String | port for the error| +|componentErrors.error| String |Shows the error happened in a component| +|componentErrors.errorLapsedSecs| Integer | Number of seconds elapsed since the error happened in a component | +|componentErrors.errorWorkerLogLink| String | Link to the worker log that reported the exception | +|spoutSummary| Array | (only for spouts) Array of component stats, one element per window.| +|spoutSummary.windowPretty| String |Duration passed in HH:MM:SS format| +|spoutSummary.window| String | window duration for metrics in seconds| +|spoutSummary.emitted| Long |Number of messages emitted in given window | +|spoutSummary.completeLatency| String (double value returned in String format) |Total latency for processing the message| +|spoutSummary.transferred| Long |Total number of messages transferred in given window| +|spoutSummary.acked| Long |Number of messages acked| +|spoutSummary.failed| Long |Number of messages failed| +|boltStats| Array | (only for bolts) Array of component stats, one element per window.| +|boltStats.windowPretty| String |Duration passed in HH:MM:SS format| +|boltStats.window| String| window duration for metrics in seconds| +|boltStats.transferred| Long |Total number of messages transferred in given window| +|boltStats.processLatency| String (double value returned in String format) |Average time of the bolt to ack a message after it was received| +|boltStats.acked| Long |Number of messages acked| +|boltStats.failed| Long |Number of messages failed| +|inputStats| Array | (only for bolts) Array of input stats| +|inputStats.component| String |Component id| +|inputStats.encodedComponentId| String |URL encoded component id| +|inputStats.executeLatency| Long | The average time a tuple spends in the execute method| +|inputStats.processLatency| Long | The average time it takes to ack a tuple after it is first received| +|inputStats.executed| Long |The number of incoming tuples processed| +|inputStats.acked| Long |Number of messages acked| +|inputStats.failed| Long |Number of messages failed| +|inputStats.stream| String |The name of the tuple stream given in the topology, or "default" if none specified| +|outputStats| Array | Array of output stats| +|outputStats.transferred| Long |Number of tuples emitted that sent to one ore more bolts| +|outputStats.emitted| Long |Number of tuples emitted| +|outputStats.stream| String |The name of the tuple stream given in the topology, or "default" if none specified| + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/component/spout +2. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/component/spout?sys=1 +3. http://ui-daemon-host-name:8080/api/v1/topology/WordCount3-1-1402960825/component/spout?window=600 +``` + +Sample response: + +```json +{ + "name": "WordCount3", + "id": "spout", + "componentType": "spout", + "windowHint": "10m 0s", + "executors": 5, + "componentErrors":[{"errorTime": 1406006074000, + "errorHost": "10.11.1.70", + "errorPort": 6701, + "errorWorkerLogLink": "http://10.11.1.7:8000/log?file=worker-6701.log", + "errorLapsedSecs": 16, + "error": "java.lang.RuntimeException: java.lang.StringIndexOutOfBoundsException: Some Error\n\tat org.apache.storm.utils.DisruptorQueue.consumeBatchToCursor(DisruptorQueue.java:128)\n\tat org.apache.storm.utils.DisruptorQueue.consumeBatchWhenAvailable(DisruptorQueue.java:99)\n\tat org.apache.storm.disruptor$consume_batch_when_available.invoke(disruptor.clj:80)\n\tat backtype...more.." + }], + "topologyId": "WordCount3-1-1402960825", + "tasks": 5, + "window": "600", + "profilerActive": [ + { + "host": "10.11.1.70", + "port": "6701", + "dumplink":"http:\/\/10.11.1.70:8000\/dumps\/ex-1-1452718803\/10.11.1.70%3A6701", + "timestamp":"576328" + } + ], + "profilingAndDebuggingCapable": true, + "profileActionEnabled": true, + "spoutSummary": [ + { + "windowPretty": "10m 0s", + "window": "600", + "emitted": 28500, + "transferred": 28460, + "completeLatency": "0.000", + "acked": 0, + "failed": 0 + }, + { + "windowPretty": "3h 0m 0s", + "window": "10800", + "emitted": 127640, + "transferred": 127440, + "completeLatency": "0.000", + "acked": 0, + "failed": 0 + }, + { + "windowPretty": "1d 0h 0m 0s", + "window": "86400", + "emitted": 127640, + "transferred": 127440, + "completeLatency": "0.000", + "acked": 0, + "failed": 0 + }, + { + "windowPretty": "All time", + "window": ":all-time", + "emitted": 127640, + "transferred": 127440, + "completeLatency": "0.000", + "acked": 0, + "failed": 0 + } + ], + "outputStats": [ + { + "stream": "__metrics", + "emitted": 40, + "transferred": 0, + "completeLatency": "0", + "acked": 0, + "failed": 0 + }, + { + "stream": "default", + "emitted": 28460, + "transferred": 28460, + "completeLatency": "0", + "acked": 0, + "failed": 0 + } + ], + "executorStats": [ + { + "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6701.log", + "emitted": 5720, + "port": 6701, + "completeLatency": "0.000", + "transferred": 5720, + "host": "10.11.1.7", + "acked": 0, + "uptime": "43m 4s", + "uptimeSeconds": 2584, + "id": "[24-24]", + "failed": 0 + }, + { + "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6703.log", + "emitted": 5700, + "port": 6703, + "completeLatency": "0.000", + "transferred": 5700, + "host": "10.11.1.7", + "acked": 0, + "uptime": "42m 57s", + "uptimeSeconds": 2577, + "id": "[25-25]", + "failed": 0 + }, + { + "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6702.log", + "emitted": 5700, + "port": 6702, + "completeLatency": "0.000", + "transferred": 5680, + "host": "10.11.1.7", + "acked": 0, + "uptime": "42m 57s", + "uptimeSeconds": 2577, + "id": "[26-26]", + "failed": 0 + }, + { + "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6701.log", + "emitted": 5700, + "port": 6701, + "completeLatency": "0.000", + "transferred": 5680, + "host": "10.11.1.7", + "acked": 0, + "uptime": "43m 4s", + "uptimeSeconds": 2584, + "id": "[27-27]", + "failed": 0 + }, + { + "workerLogLink": "http://10.11.1.7:8000/log?file=worker-6703.log", + "emitted": 5680, + "port": 6703, + "completeLatency": "0.000", + "transferred": 5680, + "host": "10.11.1.7", + "acked": 0, + "uptime": "42m 57s", + "uptimeSeconds": 2577, + "id": "[28-28]", + "failed": 0 + } + ] +} +``` + +## Profiling and Debugging GET Operations + +### /api/v1/topology/:id/profiling/start/:host-port/:timeout (GET) + +Request to start profiler on worker with timeout. Returns status and link to profiler artifacts for worker. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|host-port |String (required)| Worker Id | +|timeout |String (required)| Time out for profiler to stop in minutes | + +Response fields: + +|Field |Value |Description| +|----- |----- |-----------| +|id | String | Worker id| +|status | String | Response Status | +|timeout | String | Requested timeout +|dumplink | String | Link to logviewer URL for worker profiler documents.| + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/start/10.11.1.7:6701/10 +2. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/start/10.11.1.7:6701/5 +3. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/start/10.11.1.7:6701/20 +``` + +Sample response: + +```json +{ + "status": "ok", + "id": "10.11.1.7:6701", + "timeout": "10", + "dumplink": "http:\/\/10.11.1.7:8000\/dumps\/wordcount-1-1446614150\/10.11.1.7%3A6701" +} +``` + +### /api/v1/topology/:id/profiling/dumpprofile/:host-port (GET) + +Request to dump profiler recording on worker. Returns status and worker id for the request. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|host-port |String (required)| Worker Id | + +Response fields: + +|Field |Value |Description| +|----- |----- |-----------| +|id | String | Worker id| +|status | String | Response Status | + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/dumpprofile/10.11.1.7:6701 +``` + +Sample response: + +```json +{ + "status": "ok", + "id": "10.11.1.7:6701", +} +``` + +### /api/v1/topology/:id/profiling/stop/:host-port (GET) + +Request to stop profiler on worker. Returns status and worker id for the request. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|host-port |String (required)| Worker Id | + +Response fields: + +|Field |Value |Description| +|----- |----- |-----------| +|id | String | Worker id| +|status | String | Response Status | + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/stop/10.11.1.7:6701 +``` + +Sample response: + +```json +{ + "status": "ok", + "id": "10.11.1.7:6701", +} +``` + +### /api/v1/topology/:id/profiling/dumpjstack/:host-port (GET) + +Request to dump jstack on worker. Returns status and worker id for the request. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|host-port |String (required)| Worker Id | + +Response fields: + +|Field |Value |Description| +|----- |----- |-----------| +|id | String | Worker id| +|status | String | Response Status | + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/dumpjstack/10.11.1.7:6701 +``` + +Sample response: + +```json +{ + "status": "ok", + "id": "10.11.1.7:6701", +} +``` + +### /api/v1/topology/:id/profiling/dumpheap/:host-port (GET) + +Request to dump heap (jmap) on worker. Returns status and worker id for the request. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|host-port |String (required)| Worker Id | + +Response fields: + +|Field |Value |Description| +|----- |----- |-----------| +|id | String | Worker id| +|status | String | Response Status | + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/dumpheap/10.11.1.7:6701 +``` + +Sample response: + +```json +{ + "status": "ok", + "id": "10.11.1.7:6701", +} +``` + +### /api/v1/topology/:id/profiling/restartworker/:host-port (GET) + +Request to request the worker. Returns status and worker id for the request. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|host-port |String (required)| Worker Id | + +Response fields: + +|Field |Value |Description| +|----- |----- |-----------| +|id | String | Worker id| +|status | String | Response Status | + +Examples: + +```no-highlight +1. http://ui-daemon-host-name:8080/api/v1/topology/wordcount-1-1446614150/profiling/restartworker/10.11.1.7:6701 +``` + +Sample response: + +```json +{ + "status": "ok", + "id": "10.11.1.7:6701", +} +``` + +## POST Operations + +### /api/v1/topology/:id/activate (POST) + +Activates a topology. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | + +Sample Response: + +```json +{"topologyOperation":"activate","topologyId":"wordcount-1-1420308665","status":"success"} +``` + + +### /api/v1/topology/:id/deactivate (POST) + +Deactivates a topology. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | + +Sample Response: + +```json +{"topologyOperation":"deactivate","topologyId":"wordcount-1-1420308665","status":"success"} +``` + + +### /api/v1/topology/:id/rebalance/:wait-time (POST) + +Rebalances a topology. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|wait-time |String (required)| Wait time before rebalance happens | +|rebalanceOptions| Json (optional) | topology rebalance options | + + +Sample rebalanceOptions json: + +```json +{"rebalanceOptions" : {"numWorkers" : 2, "executors" : {"spout" :4, "count" : 10}}, "callback" : "foo"} +``` + +Examples: + +```no-highlight +curl -i -b ~/cookiejar.txt -c ~/cookiejar.txt -X POST +-H "Content-Type: application/json" +-d '{"rebalanceOptions": {"numWorkers": 2, "executors": { "spout" : "5", "split": 7, "count": 5 }}, "callback":"foo"}' +http://localhost:8080/api/v1/topology/wordcount-1-1420308665/rebalance/0 +``` + +Sample Response: + +```json +{"topologyOperation":"rebalance","topologyId":"wordcount-1-1420308665","status":"success"} +``` + + + +### /api/v1/topology/:id/kill/:wait-time (POST) + +Kills a topology. + +|Parameter |Value |Description | +|----------|--------|-------------| +|id |String (required)| Topology Id | +|wait-time |String (required)| Wait time before rebalance happens | + +Caution: Small wait times (0-5 seconds) may increase the probability of triggering the bug reported in +[STORM-112](https://issues.apache.org/jira/browse/STORM-112), which may result in broker Supervisor +daemons. + +Sample Response: + +```json +{"topologyOperation":"kill","topologyId":"wordcount-1-1420308665","status":"success"} +``` + +## API errors + +The API returns 500 HTTP status codes in case of any errors. + +Sample response: + +```json +{ + "error": "Internal Server Error", + "errorMessage": "java.lang.NullPointerException\n\tat clojure.core$name.invoke(core.clj:1505)\n\tat org.apache.storm.ui.core$component_page.invoke(core.clj:752)\n\tat org.apache.storm.ui.core$fn__7766.invoke(core.clj:782)\n\tat compojure.core$make_route$fn__5755.invoke(core.clj:93)\n\tat compojure.core$if_route$fn__5743.invoke(core.clj:39)\n\tat compojure.core$if_method$fn__5736.invoke(core.clj:24)\n\tat compojure.core$routing$fn__5761.invoke(core.clj:106)\n\tat clojure.core$some.invoke(core.clj:2443)\n\tat compojure.core$routing.doInvoke(core.clj:106)\n\tat clojure.lang.RestFn.applyTo(RestFn.java:139)\n\tat clojure.core$apply.invoke(core.clj:619)\n\tat compojure.core$routes$fn__5765.invoke(core.clj:111)\n\tat ring.middleware.reload$wrap_reload$fn__6880.invoke(reload.clj:14)\n\tat org.apache.storm.ui.core$catch_errors$fn__7800.invoke(core.clj:836)\n\tat ring.middleware.keyword_params$wrap_keyword_params$fn__6319.invoke(keyword_params.clj:27)\n\tat ring.middleware.nested_params$wrap_nested_params$fn__6358.invoke(nested_params.clj:65)\n\tat ring.middleware.params$wrap_params$fn__6291.invoke(params.clj:55)\n\tat ring.middleware.multipart_params$wrap_multipart_params$fn__6386.invoke(multipart_params.clj:103)\n\tat ring.middleware.flash$wrap_flash$fn__6675.invoke(flash.clj:14)\n\tat ring.middleware.session$wrap_session$fn__6664.invoke(session.clj:43)\n\tat ring.middleware.cookies$wrap_cookies$fn__6595.invoke(cookies.clj:160)\n\tat ring.adapter.jetty$proxy_handler$fn__6112.invoke(jetty.clj:16)\n\tat ring.adapter.jetty.proxy$org.mortbay.jetty.handler.AbstractHandler$0.handle(Unknown Source)\n\tat org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)\n\tat org.mortbay.jetty.Server.handle(Server.java:326)\n\tat org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)\n\tat org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:928)\n\tat org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:549)\n\tat org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:212)\n\tat org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)\n\tat org.mortbay.jetty.bio.SocketConnector$Connection.run(SocketConnector.java:228)\n\tat org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)\n" +} +``` diff --git a/docs/documentation/Serialization-(prior-to-0.6.0).md b/docs/Serialization-(prior-to-0.6.0).md similarity index 86% rename from docs/documentation/Serialization-(prior-to-0.6.0).md rename to docs/Serialization-(prior-to-0.6.0).md index 9ef2fdfc68f..e4a0d4fd0d1 100644 --- a/docs/documentation/Serialization-(prior-to-0.6.0).md +++ b/docs/Serialization-(prior-to-0.6.0).md @@ -1,7 +1,5 @@ --- -title: Serialization (Prior to 0.6.0) layout: documentation -documentation: true --- Tuples can be comprised of objects of any types. Since Storm is a distributed system, it needs to know how to serialize and deserialize objects when they're passed between tasks. By default Storm can serialize ints, shorts, longs, floats, doubles, bools, bytes, strings, and byte arrays, but if you want to use another type in your tuples, you'll need to implement a custom serializer. @@ -21,7 +19,7 @@ Let's dive into Storm's API for defining custom serializations. There are two st #### Creating a serializer -Custom serializers implement the [ISerialization](/javadoc/apidocs/backtype/storm/serialization/ISerialization.html) interface. Implementations specify how to serialize and deserialize types into a binary format. +Custom serializers implement the [ISerialization](javadocs/backtype/storm/serialization/ISerialization.html) interface. Implementations specify how to serialize and deserialize types into a binary format. The interface looks like this: @@ -39,7 +37,7 @@ Storm uses the `accept` method to determine if a type can be serialized by this `deserialize` reads the serialized object off of the stream and returns it. -You can see example serialization implementations in the source for [SerializationFactory](https://github.com/apache/storm/blob/0.5.4/src/jvm/backtype/storm/serialization/SerializationFactory.java) +You can see example serialization implementations in the source for [SerializationFactory](https://github.com/apache/incubator-storm/blob/0.5.4/src/jvm/backtype/storm/serialization/SerializationFactory.java) #### Registering a serializer @@ -47,6 +45,6 @@ Once you create a serializer, you need to tell Storm it exists. This is done thr Serializer registrations are done through the Config.TOPOLOGY_SERIALIZATIONS config and is simply a list of serialization class names. -Storm provides helpers for registering serializers in a topology config. The [Config](/javadoc/apidocs/backtype/storm/Config.html) class has a method called `addSerialization` that takes in a serializer class to add to the config. +Storm provides helpers for registering serializers in a topology config. The [Config](javadocs/backtype/storm/Config.html) class has a method called `addSerialization` that takes in a serializer class to add to the config. -There's an advanced config called Config.TOPOLOGY_SKIP_MISSING_SERIALIZATIONS. If you set this to true, Storm will ignore any serializations that are registered but do not have their code available on the classpath. Otherwise, Storm will throw errors when it can't find a serialization. This is useful if you run many topologies on a cluster that each have different serializations, but you want to declare all the serializations across all topologies in the `storm.yaml` files. \ No newline at end of file +There's an advanced config called Config.TOPOLOGY_SKIP_MISSING_SERIALIZATIONS. If you set this to true, Storm will ignore any serializations that are registered but do not have their code available on the classpath. Otherwise, Storm will throw errors when it can't find a serialization. This is useful if you run many topologies on a cluster that each have different serializations, but you want to declare all the serializations across all topologies in the `storm.yaml` files. diff --git a/docs/documentation/Serialization.md b/docs/Serialization.md similarity index 89% rename from docs/documentation/Serialization.md rename to docs/Serialization.md index fb861617ed4..c2e129bbbd3 100644 --- a/docs/documentation/Serialization.md +++ b/docs/Serialization.md @@ -7,7 +7,7 @@ This page is about how the serialization system in Storm works for versions 0.6. Tuples can be comprised of objects of any types. Since Storm is a distributed system, it needs to know how to serialize and deserialize objects when they're passed between tasks. -Storm uses [Kryo](http://code.google.com/p/kryo/) for serialization. Kryo is a flexible and fast serialization library that produces small serializations. +Storm uses [Kryo](https://github.com/EsotericSoftware/kryo) for serialization. Kryo is a flexible and fast serialization library that produces small serializations. By default, Storm can serialize primitive types, strings, byte arrays, ArrayList, HashMap, HashSet, and the Clojure collection types. If you want to use another type in your tuples, you'll need to register a custom serializer. @@ -23,12 +23,12 @@ Finally, another reason for using dynamic typing is so Storm can be used in a st ### Custom serialization -As mentioned, Storm uses Kryo for serialization. To implement custom serializers, you need to register new serializers with Kryo. It's highly recommended that you read over [Kryo's home page](http://code.google.com/p/kryo/) to understand how it handles custom serialization. +As mentioned, Storm uses Kryo for serialization. To implement custom serializers, you need to register new serializers with Kryo. It's highly recommended that you read over [Kryo's home page](https://github.com/EsotericSoftware/kryo) to understand how it handles custom serialization. Adding custom serializers is done through the "topology.kryo.register" property in your topology config. It takes a list of registrations, where each registration can take one of two forms: 1. The name of a class to register. In this case, Storm will use Kryo's `FieldsSerializer` to serialize the class. This may or may not be optimal for the class -- see the Kryo docs for more details. -2. A map from the name of a class to register to an implementation of [com.esotericsoftware.kryo.Serializer](http://code.google.com/p/kryo/source/browse/trunk/src/com/esotericsoftware/kryo/Serializer.java). +2. A map from the name of a class to register to an implementation of [com.esotericsoftware.kryo.Serializer](https://github.com/EsotericSoftware/kryo/blob/master/src/com/esotericsoftware/kryo/Serializer.java). Let's look at an example. @@ -41,7 +41,7 @@ topology.kryo.register: `com.mycompany.CustomType1` and `com.mycompany.CustomType3` will use the `FieldsSerializer`, whereas `com.mycompany.CustomType2` will use `com.mycompany.serializer.CustomType2Serializer` for serialization. -Storm provides helpers for registering serializers in a topology config. The [Config](/javadoc/apidocs/backtype/storm/Config.html) class has a method called `registerSerialization` that takes in a registration to add to the config. +Storm provides helpers for registering serializers in a topology config. The [Config](javadocs/org/apache/storm/Config.html) class has a method called `registerSerialization` that takes in a registration to add to the config. There's an advanced config called `Config.TOPOLOGY_SKIP_MISSING_KRYO_REGISTRATIONS`. If you set this to true, Storm will ignore any serializations that are registered but do not have their code available on the classpath. Otherwise, Storm will throw errors when it can't find a serialization. This is useful if you run many topologies on a cluster that each have different serializations, but you want to declare all the serializations across all topologies in the `storm.yaml` files. @@ -59,4 +59,4 @@ Storm 0.7.0 lets you set component-specific configurations (read more about this When a topology is submitted, a single set of serializations is chosen to be used by all components in the topology for sending messages. This is done by merging the component-specific serializer registrations with the regular set of serialization registrations. If two components define serializers for the same class, one of the serializers is chosen arbitrarily. -To force a serializer for a particular class if there's a conflict between two component-specific registrations, just define the serializer you want to use in the topology-specific configuration. The topology-specific configuration has precedence over component-specific configurations for serialization registrations. \ No newline at end of file +To force a serializer for a particular class if there's a conflict between two component-specific registrations, just define the serializer you want to use in the topology-specific configuration. The topology-specific configuration has precedence over component-specific configurations for serialization registrations. diff --git a/docs/documentation/Serializers.md b/docs/Serializers.md similarity index 75% rename from docs/documentation/Serializers.md rename to docs/Serializers.md index 2ab72660731..071c8851177 100644 --- a/docs/documentation/Serializers.md +++ b/docs/Serializers.md @@ -1,4 +1,4 @@ --- layout: documentation --- -* [storm-json](https://github.com/rapportive-oss/storm-json): Simple JSON serializer for Storm \ No newline at end of file +* [storm-json](https://github.com/rapportive-oss/storm-json): Simple JSON serializer for Storm diff --git a/docs/Setting-up-a-Storm-cluster.md b/docs/Setting-up-a-Storm-cluster.md new file mode 100644 index 00000000000..a251d1a9258 --- /dev/null +++ b/docs/Setting-up-a-Storm-cluster.md @@ -0,0 +1,118 @@ +--- +title: Setting up a Storm Cluster +layout: documentation +documentation: true +--- +This page outlines the steps for getting a Storm cluster up and running. If you're on AWS, you should check out the [storm-deploy](https://github.com/nathanmarz/storm-deploy/wiki) project. [storm-deploy](https://github.com/nathanmarz/storm-deploy/wiki) completely automates the provisioning, configuration, and installation of Storm clusters on EC2. It also sets up Ganglia for you so you can monitor CPU, disk, and network usage. + +If you run into difficulties with your Storm cluster, first check for a solution is in the [Troubleshooting](Troubleshooting.html) page. Otherwise, email the mailing list. + +Here's a summary of the steps for setting up a Storm cluster: + +1. Set up a Zookeeper cluster +2. Install dependencies on Nimbus and worker machines +3. Download and extract a Storm release to Nimbus and worker machines +4. Fill in mandatory configurations into storm.yaml +5. Launch daemons under supervision using "storm" script and a supervisor of your choice + +### Set up a Zookeeper cluster + +Storm uses Zookeeper for coordinating the cluster. Zookeeper **is not** used for message passing, so the load Storm places on Zookeeper is quite low. Single node Zookeeper clusters should be sufficient for most cases, but if you want failover or are deploying large Storm clusters you may want larger Zookeeper clusters. Instructions for deploying Zookeeper are [here](http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html). + +A few notes about Zookeeper deployment: + +1. It's critical that you run Zookeeper under supervision, since Zookeeper is fail-fast and will exit the process if it encounters any error case. See [here](http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html#sc_supervision) for more details. +2. It's critical that you set up a cron to compact Zookeeper's data and transaction logs. The Zookeeper daemon does not do this on its own, and if you don't set up a cron, Zookeeper will quickly run out of disk space. See [here](http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html#sc_maintenance) for more details. + +### Install dependencies on Nimbus and worker machines + +Next you need to install Storm's dependencies on Nimbus and the worker machines. These are: + +1. Java 7 +2. Python 2.6.6 + +These are the versions of the dependencies that have been tested with Storm. Storm may or may not work with different versions of Java and/or Python. + + +### Download and extract a Storm release to Nimbus and worker machines + +Next, download a Storm release and extract the zip file somewhere on Nimbus and each of the worker machines. The Storm releases can be downloaded [from here](http://github.com/apache/storm/releases). + +### Fill in mandatory configurations into storm.yaml + +The Storm release contains a file at `conf/storm.yaml` that configures the Storm daemons. You can see the default configuration values [here]({{page.git-blob-base}}/conf/defaults.yaml). storm.yaml overrides anything in defaults.yaml. There's a few configurations that are mandatory to get a working cluster: + +1) **storm.zookeeper.servers**: This is a list of the hosts in the Zookeeper cluster for your Storm cluster. It should look something like: + +```yaml +storm.zookeeper.servers: + - "111.222.333.444" + - "555.666.777.888" +``` + +If the port that your Zookeeper cluster uses is different than the default, you should set **storm.zookeeper.port** as well. + +2) **storm.local.dir**: The Nimbus and Supervisor daemons require a directory on the local disk to store small amounts of state (like jars, confs, and things like that). + You should create that directory on each machine, give it proper permissions, and then fill in the directory location using this config. For example: + +```yaml +storm.local.dir: "/mnt/storm" +``` +If you run storm on windows, it could be: + +```yaml +storm.local.dir: "C:\\storm-local" +``` +If you use a relative path, it will be relative to where you installed storm(STORM_HOME). +You can leave it empty with default value `$STORM_HOME/storm-local` + +3) **nimbus.seeds**: The worker nodes need to know which machines are the candidate of master in order to download topology jars and confs. For example: + +```yaml +nimbus.seeds: ["111.222.333.44"] +``` +You're encouraged to fill out the value to list of **machine's FQDN**. If you want to set up Nimbus H/A, you have to address all machines' FQDN which run nimbus. You may want to leave it to default value when you just want to set up 'pseudo-distributed' cluster, but you're still encouraged to fill out FQDN. + +4) **supervisor.slots.ports**: For each worker machine, you configure how many workers run on that machine with this config. Each worker uses a single port for receiving messages, and this setting defines which ports are open for use. If you define five ports here, then Storm will allocate up to five workers to run on this machine. If you define three ports, Storm will only run up to three. By default, this setting is configured to run 4 workers on the ports 6700, 6701, 6702, and 6703. For example: + +```yaml +supervisor.slots.ports: + - 6700 + - 6701 + - 6702 + - 6703 +``` + +### Monitoring Health of Supervisors + +Storm provides a mechanism by which administrators can configure the supervisor to run administrator supplied scripts periodically to determine if a node is healthy or not. Administrators can have the supervisor determine if the node is in a healthy state by performing any checks of their choice in scripts located in storm.health.check.dir. If a script detects the node to be in an unhealthy state, it must print a line to standard output beginning with the string ERROR. The supervisor will periodically run the scripts in the health check dir and check the output. If the script’s output contains the string ERROR, as described above, the supervisor will shut down any workers and exit. + +If the supervisor is running with supervision "/bin/storm node-health-check" can be called to determine if the supervisor should be launched or if the node is unhealthy. + +The health check directory location can be configured with: + +```yaml +storm.health.check.dir: "healthchecks" + +``` +The scripts must have execute permissions. +The time to allow any given healthcheck script to run before it is marked failed due to timeout can be configured with: + +```yaml +storm.health.check.timeout.ms: 5000 +``` + +### Configure external libraries and environmental variables (optional) + +If you need support from external libraries or custom plugins, you can place such jars into the extlib/ and extlib-daemon/ directories. Note that the extlib-daemon/ directory stores jars used only by daemons (Nimbus, Supervisor, DRPC, UI, Logviewer), e.g., HDFS and customized scheduling libraries. Accordingly, two environmental variables STORM_EXT_CLASSPATH and STORM_EXT_CLASSPATH_DAEMON can be configured by users for including the external classpath and daemon-only external classpath. + + +### Launch daemons under supervision using "storm" script and a supervisor of your choice + +The last step is to launch all the Storm daemons. It is critical that you run each of these daemons under supervision. Storm is a __fail-fast__ system which means the processes will halt whenever an unexpected error is encountered. Storm is designed so that it can safely halt at any point and recover correctly when the process is restarted. This is why Storm keeps no state in-process -- if Nimbus or the Supervisors restart, the running topologies are unaffected. Here's how to run the Storm daemons: + +1. **Nimbus**: Run the command "bin/storm nimbus" under supervision on the master machine. +2. **Supervisor**: Run the command "bin/storm supervisor" under supervision on each worker machine. The supervisor daemon is responsible for starting and stopping worker processes on that machine. +3. **UI**: Run the Storm UI (a site you can access from the browser that gives diagnostics on the cluster and topologies) by running the command "bin/storm ui" under supervision. The UI can be accessed by navigating your web browser to http://{ui host}:8080. + +As you can see, running the daemons is very straightforward. The daemons will log to the logs/ directory in wherever you extracted the Storm release. diff --git a/docs/documentation/Setting-up-a-Storm-project-in-Eclipse.md b/docs/Setting-up-a-Storm-project-in-Eclipse.md similarity index 100% rename from docs/documentation/Setting-up-a-Storm-project-in-Eclipse.md rename to docs/Setting-up-a-Storm-project-in-Eclipse.md diff --git a/docs/documentation/Setting-up-development-environment.md b/docs/Setting-up-development-environment.md similarity index 84% rename from docs/documentation/Setting-up-development-environment.md rename to docs/Setting-up-development-environment.md index fa450be436d..bfa98a23a76 100644 --- a/docs/documentation/Setting-up-development-environment.md +++ b/docs/Setting-up-development-environment.md @@ -29,13 +29,5 @@ Installing a Storm release locally is only for interacting with remote clusters. The previous step installed the `storm` client on your machine which is used to communicate with remote Storm clusters. Now all you have to do is tell the client which Storm cluster to talk to. To do this, all you have to do is put the host address of the master in the `~/.storm/storm.yaml` file. It should look something like this: ``` -nimbus.host: "123.45.678.890" +nimbus.seeds: ["123.45.678.890"] ``` - -Alternatively, if you use the [storm-deploy](https://github.com/nathanmarz/storm-deploy) project to provision Storm clusters on AWS, it will automatically set up your ~/.storm/storm.yaml file. You can manually attach to a Storm cluster (or switch between multiple clusters) using the "attach" command, like so: - -``` -lein run :deploy --attach --name mystormcluster -``` - -More information is on the storm-deploy [wiki](https://github.com/nathanmarz/storm-deploy/wiki) \ No newline at end of file diff --git a/docs/documentation/Spout-implementations.md b/docs/Spout-implementations.md similarity index 90% rename from docs/documentation/Spout-implementations.md rename to docs/Spout-implementations.md index 9952558d91f..f52e662dc04 100644 --- a/docs/documentation/Spout-implementations.md +++ b/docs/Spout-implementations.md @@ -7,4 +7,4 @@ documentation: true * [storm-amqp-spout](https://github.com/rapportive-oss/storm-amqp-spout): Adapter to use AMQP source as a spout * [storm-jms](https://github.com/ptgoetz/storm-jms): Adapter to use a JMS source as a spout * [storm-redis-pubsub](https://github.com/sorenmacbeth/storm-redis-pubsub): A spout that subscribes to a Redis pubsub stream -* [storm-beanstalkd-spout](https://github.com/haitaoyao/storm-beanstalkd-spout): A spout that subscribes to a beanstalkd queue \ No newline at end of file +* [storm-beanstalkd-spout](https://github.com/haitaoyao/storm-beanstalkd-spout): A spout that subscribes to a beanstalkd queue diff --git a/docs/State-checkpointing.md b/docs/State-checkpointing.md new file mode 100644 index 00000000000..8789508e628 --- /dev/null +++ b/docs/State-checkpointing.md @@ -0,0 +1,164 @@ +--- +title: Storm State Management +layout: documentation +documentation: true +--- +# State support in core storm +Storm core has abstractions for bolts to save and retrieve the state of its operations. There is a default in-memory +based state implementation and also a Redis backed implementation that provides state persistence. + +## State management +Bolts that requires its state to be managed and persisted by the framework should implement the `IStatefulBolt` interface or +extend the `BaseStatefulBolt` and implement `void initState(T state)` method. The `initState` method is invoked by the framework +during the bolt initialization with the previously saved state of the bolt. This is invoked after prepare but before the bolt starts +processing any tuples. + +Currently the only kind of `State` implementation that is supported is `KeyValueState` which provides key-value mapping. + +For example a word count bolt could use the key value state abstraction for the word counts as follows. + +1. Extend the BaseStatefulBolt and type parameterize it with KeyValueState which would store the mapping of word to count. +2. The bolt gets initialized with its previously saved state in the init method. This will contain the word counts +last committed by the framework during the previous run. +3. In the execute method, update the word count. + + public class WordCountBolt extends BaseStatefulBolt> { + private KeyValueState wordCounts; + private OutputCollector collector; + ... + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector = collector; + } + @Override + public void initState(KeyValueState state) { + wordCounts = state; + } + @Override + public void execute(Tuple tuple) { + String word = tuple.getString(0); + Integer count = wordCounts.get(word, 0); + count++; + wordCounts.put(word, count); + collector.emit(tuple, new Values(word, count)); + collector.ack(tuple); + } + ... + } + +4. The framework periodically checkpoints the state of the bolt (default every second). The frequency +can be changed by setting the storm config `topology.state.checkpoint.interval.ms` +5. For state persistence, use a state provider that supports persistence by setting the `topology.state.provider` in the +storm config. E.g. for using Redis based key-value state implementation set `topology.state.provider: org.apache.storm.redis.state.RedisKeyValueStateProvider` +in storm.yaml. The provider implementation jar should be in the class path, which in this case means putting the `storm-redis-*.jar` +in the extlib directory. +6. The state provider properties can be overridden by setting `topology.state.provider.config`. For Redis state this is a +json config with the following properties. + +``` +{ + "keyClass": "Optional fully qualified class name of the Key type.", + "valueClass": "Optional fully qualified class name of the Value type.", + "keySerializerClass": "Optional Key serializer implementation class.", + "valueSerializerClass": "Optional Value Serializer implementation class.", + "jedisPoolConfig": { + "host": "localhost", + "port": 6379, + "timeout": 2000, + "database": 0, + "password": "xyz" + } +} +``` + +## Checkpoint mechanism +Checkpoint is triggered by an internal checkpoint spout at the specified `topology.state.checkpoint.interval.ms`. If there is +at-least one `IStatefulBolt` in the topology, the checkpoint spout is automatically added by the topology builder . For stateful topologies, +the topology builder wraps the `IStatefulBolt` in a `StatefulBoltExecutor` which handles the state commits on receiving the checkpoint tuples. +The non stateful bolts are wrapped in a `CheckpointTupleForwarder` which just forwards the checkpoint tuples so that the checkpoint tuples +can flow through the topology DAG. The checkpoint tuples flow through a separate internal stream namely `$checkpoint`. The topology builder +wires the checkpoint stream across the whole topology with the checkpoint spout at the root. + +``` + default default default +[spout1] ---------------> [statefulbolt1] ----------> [bolt1] --------------> [statefulbolt2] + | ----------> --------------> + | ($chpt) ($chpt) + | +[$checkpointspout] _______| ($chpt) +``` + +At checkpoint intervals the checkpoint tuples are emitted by the checkpoint spout. On receiving a checkpoint tuple, the state of the bolt +is saved and then the checkpoint tuple is forwarded to the next component. Each bolt waits for the checkpoint to arrive on all its input +streams before it saves its state so that the state represents a consistent state across the topology. Once the checkpoint spout receives +ACK from all the bolts, the state commit is complete and the transaction is recorded as committed by the checkpoint spout. + +The state checkpointing does not currently checkpoint the state of the spout. Yet, once the state of all bolts are checkpointed, and once the checkpoint tuples are acked, the tuples emitted by the spout are also acked. +It also implies that `topology.state.checkpoint.interval.ms` is lower than `topology.message.timeout.secs`. + +The state commit works like a three phase commit protocol with a prepare and commit phase so that the state across the topology is saved +in a consistent and atomic manner. + +### Recovery +The recovery phase is triggered when the topology is started for the first time. If the previous transaction was not successfully +prepared, a `rollback` message is sent across the topology so that if a bolt has some prepared transactions it can be discarded. +If the previous transaction was prepared successfully but not committed, a `commit` message is sent across the topology so that +the prepared transactions can be committed. After these steps are complete, the bolts are initialized with the state. + +The recovery is also triggered if one of the bolts fails to acknowledge the checkpoint message or say a worker crashed in +the middle. Thus when the worker is restarted by the supervisor, the checkpoint mechanism makes sure that the bolt gets +initialized with its previous state and the checkpointing continues from the point where it left off. + +### Guarantee +Storm relies on the acking mechanism to replay tuples in case of failures. It is possible that the state is committed +but the worker crashes before acking the tuples. In this case the tuples are replayed causing duplicate state updates. +Also currently the StatefulBoltExecutor continues to process the tuples from a stream after it has received a checkpoint +tuple on one stream while waiting for checkpoint to arrive on other input streams for saving the state. This can also cause +duplicate state updates during recovery. + +The state abstraction does not eliminate duplicate evaluations and currently provides only at-least once guarantee. + +In order to provide the at-least once guarantee, all bolts in a stateful topology are expected to anchor the tuples while emitting and ack the input tuples once its processed. For non-stateful bolts, the anchoring/acking can be automatically managed by extending the `BaseBasicBolt`. Stateful bolts are expected to anchor tuples while emitting and ack the tuple after processing like in the `WordCountBolt` example in the State management section above. + +### IStateful bolt hooks +IStateful bolt interface provides hook methods where in the stateful bolts could implement some custom actions. + +```java + /** + * This is a hook for the component to perform some actions just before the + * framework commits its state. + */ + void preCommit(long txid); + + /** + * This is a hook for the component to perform some actions just before the + * framework prepares its state. + */ + void prePrepare(long txid); + + /** + * This is a hook for the component to perform some actions just before the + * framework rolls back the prepared state. + */ + void preRollback(); +``` + +This is optional and stateful bolts are not expected to provide any implementation. This is provided so that other +system level components can be built on top of the stateful abstractions where we might want to take some actions before the +stateful bolt's state is prepared, committed or rolled back. + +## Providing custom state implementations +Currently the only kind of `State` implementation supported is `KeyValueState` which provides key-value mapping. + +Custom state implementations should provide implementations for the methods defined in the `org.apache.storm.State` interface. +These are the `void prepareCommit(long txid)`, `void commit(long txid)`, `rollback()` methods. `commit()` method is optional +and is useful if the bolt manages the state on its own. This is currently used only by the internal system bolts, +for e.g. the CheckpointSpout to save its state. + +`KeyValueState` implementation should also implement the methods defined in the `org.apache.storm.state.KeyValueState` interface. + +### State provider +The framework instantiates the state via the corresponding `StateProvider` implementation. A custom state should also provide +a `StateProvider` implementation which can load and return the state based on the namespace. Each state belongs to a unique namespace. +The namespace is typically unique per task so that each task can have its own state. The StateProvider and the corresponding +State implementation should be available in the class path of Storm (by placing them in the extlib directory). diff --git a/docs/Storm-Scheduler.md b/docs/Storm-Scheduler.md new file mode 100644 index 00000000000..51b3aac7f00 --- /dev/null +++ b/docs/Storm-Scheduler.md @@ -0,0 +1,27 @@ +--- +title: Scheduler +layout: documentation +documentation: true +--- + +Storm now has 4 kinds of built-in schedulers: [DefaultScheduler]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/scheduler/DefaultScheduler.clj), [IsolationScheduler]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/scheduler/IsolationScheduler.clj), [MultitenantScheduler]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/scheduler/multitenant/MultitenantScheduler.java), [ResourceAwareScheduler](Resource_Aware_Scheduler_overview.html). + +## Pluggable scheduler +You can implement your own scheduler to replace the default scheduler to assign executors to workers. You configure the class to use the "storm.scheduler" config in your storm.yaml, and your scheduler must implement the [IScheduler]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/scheduler/IScheduler.java) interface. + +## Isolation Scheduler +The isolation scheduler makes it easy and safe to share a cluster among many topologies. The isolation scheduler lets you specify which topologies should be "isolated", meaning that they run on a dedicated set of machines within the cluster where no other topologies will be running. These isolated topologies are given priority on the cluster, so resources will be allocated to isolated topologies if there's competition with non-isolated topologies, and resources will be taken away from non-isolated topologies if necessary to get resources for an isolated topology. Once all isolated topologies are allocated, the remaining machines on the cluster are shared among all non-isolated topologies. + +You can configure the isolation scheduler in the Nimbus configuration by setting "storm.scheduler" to "org.apache.storm.scheduler.IsolationScheduler". Then, use the "isolation.scheduler.machines" config to specify how many machines each topology should get. This configuration is a map from topology name to the number of isolated machines allocated to this topology. For example: + +``` +isolation.scheduler.machines: + "my-topology": 8 + "tiny-topology": 1 + "some-other-topology": 3 +``` + +Any topologies submitted to the cluster not listed there will not be isolated. Note that there is no way for a user of Storm to affect their isolation settings – this is only allowed by the administrator of the cluster (this is very much intentional). + +The isolation scheduler solves the multi-tenancy problem – avoiding resource contention between topologies – by providing full isolation between topologies. The intention is that "productionized" topologies should be listed in the isolation config, and test or in-development topologies should not. The remaining machines on the cluster serve the dual role of failover for isolated topologies and for running the non-isolated topologies. + diff --git a/docs/documentation/Storm-multi-language-protocol-(versions-0.7.0-and-below).md b/docs/Storm-multi-language-protocol-(versions-0.7.0-and-below).md similarity index 96% rename from docs/documentation/Storm-multi-language-protocol-(versions-0.7.0-and-below).md rename to docs/Storm-multi-language-protocol-(versions-0.7.0-and-below).md index 093406ce72d..1d4422f7b00 100644 --- a/docs/documentation/Storm-multi-language-protocol-(versions-0.7.0-and-below).md +++ b/docs/Storm-multi-language-protocol-(versions-0.7.0-and-below).md @@ -1,7 +1,5 @@ --- -title: Storm Multi-Lang Protocol (Versions 0.7.0 and below) layout: documentation -documentation: true --- This page explains the multilang protocol for versions 0.7.0 and below. The protocol changed in version 0.7.1. @@ -121,4 +119,4 @@ A "log" will log a message in the worker log. It looks like: Note: This command is not JSON encoded, it is sent as a simple string. -This lets the parent bolt know that the script has finished processing and is ready for another tuple. \ No newline at end of file +This lets the parent bolt know that the script has finished processing and is ready for another tuple. diff --git a/docs/Structure-of-the-codebase.md b/docs/Structure-of-the-codebase.md new file mode 100644 index 00000000000..378c060a230 --- /dev/null +++ b/docs/Structure-of-the-codebase.md @@ -0,0 +1,142 @@ +--- +title: Structure of the Codebase +layout: documentation +documentation: true +--- +There are three distinct layers to Storm's codebase. + +First, Storm was designed from the very beginning to be compatible with multiple languages. Nimbus is a Thrift service and topologies are defined as Thrift structures. The usage of Thrift allows Storm to be used from any language. + +Second, all of Storm's interfaces are specified as Java interfaces. So even though there's a lot of Clojure in Storm's implementation, all usage must go through the Java API. This means that every feature of Storm is always available via Java. + +Third, Storm's implementation is largely in Clojure. Line-wise, Storm is about half Java code, half Clojure code. But Clojure is much more expressive, so in reality the great majority of the implementation logic is in Clojure. + +The following sections explain each of these layers in more detail. + +### storm.thrift + +The first place to look to understand the structure of Storm's codebase is the [storm.thrift]({{page.git-blob-base}}/storm-core/src/storm.thrift) file. + +Storm uses [this fork](https://github.com/nathanmarz/thrift/tree/storm) of Thrift (branch 'storm') to produce the generated code. This "fork" is actually Thrift 7 with all the Java packages renamed to be `org.apache.thrift7`. Otherwise, it's identical to Thrift 7. This fork was done because of the lack of backwards compatibility in Thrift and the need for many people to use other versions of Thrift in their Storm topologies. + +Every spout or bolt in a topology is given a user-specified identifier called the "component id". The component id is used to specify subscriptions from a bolt to the output streams of other spouts or bolts. A [StormTopology]({{page.git-blob-base}}/storm-core/src/storm.thrift#L91) structure contains a map from component id to component for each type of component (spouts and bolts). + +Spouts and bolts have the same Thrift definition, so let's just take a look at the [Thrift definition for bolts]({{page.git-blob-base}}/storm-core/src/storm.thrift#L102). It contains a `ComponentObject` struct and a `ComponentCommon` struct. + +The `ComponentObject` defines the implementation for the bolt. It can be one of three types: + +1. A serialized java object (that implements [IBolt]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/task/IBolt.java)) +2. A `ShellComponent` object that indicates the implementation is in another language. Specifying a bolt this way will cause Storm to instantiate a [ShellBolt]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/task/ShellBolt.java) object to handle the communication between the JVM-based worker process and the non-JVM-based implementation of the component. +3. A `JavaObject` structure which tells Storm the classname and constructor arguments to use to instantiate that bolt. This is useful if you want to define a topology in a non-JVM language. This way, you can make use of JVM-based spouts and bolts without having to create and serialize a Java object yourself. + +`ComponentCommon` defines everything else for this component. This includes: + +1. What streams this component emits and the metadata for each stream (whether it's a direct stream, the fields declaration) +2. What streams this component consumes (specified as a map from component_id:stream_id to the stream grouping to use) +3. The parallelism for this component +4. The component-specific [configuration](Configuration.html) for this component + +Note that the structure spouts also have a `ComponentCommon` field, and so spouts can also have declarations to consume other input streams. Yet the Storm Java API does not provide a way for spouts to consume other streams, and if you put any input declarations there for a spout you would get an error when you tried to submit the topology. The reason that spouts have an input declarations field is not for users to use, but for Storm itself to use. Storm adds implicit streams and bolts to the topology to set up the [acking framework](Acking-framework-implementation.html), and two of these implicit streams are from the acker bolt to each spout in the topology. The acker sends "ack" or "fail" messages along these streams whenever a tuple tree is detected to be completed or failed. The code that transforms the user's topology into the runtime topology is located [here]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/common.clj#L279). + +### Java interfaces + +The interfaces for Storm are generally specified as Java interfaces. The main interfaces are: + +1. [IRichBolt](javadocs/org/apache/storm/topology/IRichBolt.html) +2. [IRichSpout](javadocs/org/apache/storm/topology/IRichSpout.html) +3. [TopologyBuilder](javadocs/org/apache/storm/topology/TopologyBuilder.html) + +The strategy for the majority of the interfaces is to: + +1. Specify the interface using a Java interface +2. Provide a base class that provides default implementations when appropriate + +You can see this strategy at work with the [BaseRichSpout](javadocs/org/apache/storm/topology/base/BaseRichSpout.html) class. + +Spouts and bolts are serialized into the Thrift definition of the topology as described above. + +One subtle aspect of the interfaces is the difference between `IBolt` and `ISpout` vs. `IRichBolt` and `IRichSpout`. The main difference between them is the addition of the `declareOutputFields` method in the "Rich" versions of the interfaces. The reason for the split is that the output fields declaration for each output stream needs to be part of the Thrift struct (so it can be specified from any language), but as a user you want to be able to declare the streams as part of your class. What `TopologyBuilder` does when constructing the Thrift representation is call `declareOutputFields` to get the declaration and convert it into the Thrift structure. The conversion happens [at this portion]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/topology/TopologyBuilder.java#L205) of the `TopologyBuilder` code. + + +### Implementation + +Specifying all the functionality via Java interfaces ensures that every feature of Storm is available via Java. Moreso, the focus on Java interfaces ensures that the user experience from Java-land is pleasant as well. + +The implementation of Storm, on the other hand, is primarily in Clojure. While the codebase is about 50% Java and 50% Clojure in terms of LOC, most of the implementation logic is in Clojure. There are two notable exceptions to this, and that is the [DRPC](https://github.com/apache/storm/wiki/Distributed-RPC) and [transactional topologies](https://github.com/apache/storm/wiki/Transactional-topologies) implementations. These are implemented purely in Java. This was done to serve as an illustration for how to implement a higher level abstraction on Storm. The DRPC and transactional topologies implementations are in the [org.apache.storm.coordination]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/coordination), [org.apache.storm.drpc]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/drpc), and [org.apache.storm.transactional]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/transactional) packages. + +Here's a summary of the purpose of the main Java packages and Clojure namespace: + +#### Java packages + +[org.apache.storm.coordination]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/coordination): Implements the pieces required to coordinate batch-processing on top of Storm, which both DRPC and transactional topologies use. `CoordinatedBolt` is the most important class here. + +[org.apache.storm.drpc]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/drpc): Implementation of the DRPC higher level abstraction + +[org.apache.storm.generated]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/generated): The generated Thrift code for Storm (generated using [this fork](https://github.com/nathanmarz/thrift) of Thrift, which simply renames the packages to org.apache.thrift7 to avoid conflicts with other Thrift versions) + +[org.apache.storm.grouping]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/grouping): Contains interface for making custom stream groupings + +[org.apache.storm.hooks]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/hooks): Interfaces for hooking into various events in Storm, such as when tasks emit tuples, when tuples are acked, etc. User guide for hooks is [here](https://github.com/apache/storm/wiki/Hooks). + +[org.apache.storm.serialization]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/serialization): Implementation of how Storm serializes/deserializes tuples. Built on top of [Kryo](http://code.google.com/p/kryo/). + +[org.apache.storm.spout]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/spout): Definition of spout and associated interfaces (like the `SpoutOutputCollector`). Also contains `ShellSpout` which implements the protocol for defining spouts in non-JVM languages. + +[org.apache.storm.task]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/task): Definition of bolt and associated interfaces (like `OutputCollector`). Also contains `ShellBolt` which implements the protocol for defining bolts in non-JVM languages. Finally, `TopologyContext` is defined here as well, which is provided to spouts and bolts so they can get data about the topology and its execution at runtime. + +[org.apache.storm.testing]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/testing): Contains a variety of test bolts and utilities used in Storm's unit tests. + +[org.apache.storm.topology]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/topology): Java layer over the underlying Thrift structure to provide a clean, pure-Java API to Storm (users don't have to know about Thrift). `TopologyBuilder` is here as well as the helpful base classes for the different spouts and bolts. The slightly-higher level `IBasicBolt` interface is here, which is a simpler way to write certain kinds of bolts. + +[org.apache.storm.transactional]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/transactional): Implementation of transactional topologies. + +[org.apache.storm.tuple]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/tuple): Implementation of Storm's tuple data model. + +[org.apache.storm.utils]({{page.git-tree-base}}/storm-core/src/jvm/org/apache/storm/tuple): Data structures and miscellaneous utilities used throughout the codebase. + + +#### Clojure namespaces + +[org.apache.storm.bootstrap]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/bootstrap.clj): Contains a helpful macro to import all the classes and namespaces that are used throughout the codebase. + +[org.apache.storm.clojure]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/clojure.clj): Implementation of the Clojure DSL for Storm. + +[org.apache.storm.cluster]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/cluster.clj): All Zookeeper logic used in Storm daemons is encapsulated in this file. This code manages how cluster state (like what tasks are running where, what spout/bolt each task runs as) is mapped to the Zookeeper "filesystem" API. + +[org.apache.storm.command.*]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/command): These namespaces implement various commands for the `storm` command line client. These implementations are very short. + +[org.apache.storm.config]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/config.clj): Implementation of config reading/parsing code for Clojure. Also has utility functions for determining what local path nimbus/supervisor/daemons should be using for various things. e.g. the `master-inbox` function will return the local path that Nimbus should use when jars are uploaded to it. + +[org.apache.storm.daemon.acker]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/acker.clj): Implementation of the "acker" bolt, which is a key part of how Storm guarantees data processing. + +[org.apache.storm.daemon.common]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/common.clj): Implementation of common functions used in Storm daemons, like getting the id for a topology based on the name, mapping a user's topology into the one that actually executes (with implicit acking streams and acker bolt added - see `system-topology!` function), and definitions for the various heartbeat and other structures persisted by Storm. + +[org.apache.storm.daemon.drpc]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/drpc.clj): Implementation of the DRPC server for use with DRPC topologies. + +[org.apache.storm.daemon.nimbus]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/nimbus.clj): Implementation of Nimbus. + +[org.apache.storm.daemon.supervisor]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/supervisor.clj): Implementation of Supervisor. + +[org.apache.storm.daemon.task]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/task.clj): Implementation of an individual task for a spout or bolt. Handles message routing, serialization, stats collection for the UI, as well as the spout-specific and bolt-specific execution implementations. + +[org.apache.storm.daemon.worker]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/daemon/worker.clj): Implementation of a worker process (which will contain many tasks within). Implements message transferring and task launching. + +[org.apache.storm.event]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/event.clj): Implements a simple asynchronous function executor. Used in various places in Nimbus and Supervisor to make functions execute in serial to avoid any race conditions. + +[org.apache.storm.log]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/log.clj): Defines the functions used to log messages to log4j. + +[org.apache.storm.messaging.*]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/messaging): Defines a higher level interface to implementing point to point messaging. In local mode Storm uses in-memory Java queues to do this; on a cluster, it uses ZeroMQ. The generic interface is defined in protocol.clj. + +[org.apache.storm.stats]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/stats.clj): Implementation of stats rollup routines used when sending stats to ZK for use by the UI. Does things like windowed and rolling aggregations at multiple granularities. + +[org.apache.storm.testing]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/testing.clj): Implementation of facilities used to test Storm topologies. Includes time simulation, `complete-topology` for running a fixed set of tuples through a topology and capturing the output, tracker topologies for having fine grained control over detecting when a cluster is "idle", and other utilities. + +[org.apache.storm.thrift]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/thrift.clj): Clojure wrappers around the generated Thrift API to make working with Thrift structures more pleasant. + +[org.apache.storm.timer]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/timer.clj): Implementation of a background timer to execute functions in the future or on a recurring interval. Storm couldn't use the [Timer](http://docs.oracle.com/javase/1.4.2/docs/api/java/util/Timer.html) class because it needed integration with time simulation in order to be able to unit test Nimbus and the Supervisor. + +[org.apache.storm.ui.*]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/ui): Implementation of Storm UI. Completely independent from rest of code base and uses the Nimbus Thrift API to get data. + +[org.apache.storm.util]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/util.clj): Contains generic utility functions used throughout the code base. + +[org.apache.storm.zookeeper]({{page.git-blob-base}}/storm-core/src/clj/org/apache/storm/zookeeper.clj): Clojure wrapper around the Zookeeper API and implements some "high-level" stuff like "mkdirs" and "delete-recursive". diff --git a/docs/documentation/Support-for-non-java-languages.md b/docs/Support-for-non-java-languages.md similarity index 100% rename from docs/documentation/Support-for-non-java-languages.md rename to docs/Support-for-non-java-languages.md diff --git a/docs/documentation/Transactional-topologies.md b/docs/Transactional-topologies.md similarity index 89% rename from docs/documentation/Transactional-topologies.md rename to docs/Transactional-topologies.md index 8c999e76094..db5509f6c08 100644 --- a/docs/documentation/Transactional-topologies.md +++ b/docs/Transactional-topologies.md @@ -81,7 +81,7 @@ Finally, another thing to note is that transactional topologies require a source ## The basics through example -You build transactional topologies by using [TransactionalTopologyBuilder](/javadoc/apidocs/backtype/storm/transactional/TransactionalTopologyBuilder.html). Here's the transactional topology definition for a topology that computes the global count of tuples from the input stream. This code comes from [TransactionalGlobalCount](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/storm/starter/TransactionalGlobalCount.java) in storm-starter. +You build transactional topologies by using [TransactionalTopologyBuilder](javadocs/org/apache/storm/transactional/TransactionalTopologyBuilder.html). Here's the transactional topology definition for a topology that computes the global count of tuples from the input stream. This code comes from [TransactionalGlobalCount]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/TransactionalGlobalCount.java) in storm-starter. ```java MemoryTransactionalSpout spout = new MemoryTransactionalSpout(DATA, new Fields("word"), PARTITION_TAKE_PER_BATCH); @@ -130,9 +130,9 @@ public static class BatchCount extends BaseBatchBolt { } ``` -A new instance of this object is created for every batch that's being processed. The actual bolt this runs within is called [BatchBoltExecutor](https://github.com/apache/storm/blob/0.7.0/src/jvm/backtype/storm/coordination/BatchBoltExecutor.java) and manages the creation and cleanup for these objects. +A new instance of this object is created for every batch that's being processed. The actual bolt this runs within is called [BatchBoltExecutor](https://github.com/apache/storm/blob/0.7.0/src/jvm/org/apache/storm/coordination/BatchBoltExecutor.java) and manages the creation and cleanup for these objects. -The `prepare` method parameterizes this batch bolt with the Storm config, the topology context, an output collector, and the id for this batch of tuples. In the case of transactional topologies, the id will be a [TransactionAttempt](/javadoc/apidocs/backtype/storm/transactional/TransactionAttempt.html) object. The batch bolt abstraction can be used in Distributed RPC as well which uses a different type of id for the batches. `BatchBolt` can actually be parameterized with the type of the id, so if you only intend to use the batch bolt for transactional topologies, you can extend `BaseTransactionalBolt` which has this definition: +The `prepare` method parameterizes this batch bolt with the Storm config, the topology context, an output collector, and the id for this batch of tuples. In the case of transactional topologies, the id will be a [TransactionAttempt](javadocs/org/apache/storm/transactional/TransactionAttempt.html) object. The batch bolt abstraction can be used in Distributed RPC as well which uses a different type of id for the batches. `BatchBolt` can actually be parameterized with the type of the id, so if you only intend to use the batch bolt for transactional topologies, you can extend `BaseTransactionalBolt` which has this definition: ```java public abstract class BaseTransactionalBolt extends BaseBatchBolt { @@ -201,7 +201,7 @@ First, notice that this bolt implements the `ICommitter` interface. This tells S The code for `finishBatch` in `UpdateGlobalCount` gets the current value from the database and compares its transaction id to the transaction id for this batch. If they are the same, it does nothing. Otherwise, it increments the value in the database by the partial count for this batch. -A more involved transactional topology example that updates multiple databases idempotently can be found in storm-starter in the [TransactionalWords](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/storm/starter/TransactionalWords.java) class. +A more involved transactional topology example that updates multiple databases idempotently can be found in storm-starter in the [TransactionalWords]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/TransactionalWords.java) class. ## Transactional Topology API @@ -211,9 +211,9 @@ This section outlines the different pieces of the transactional topology API. There are three kinds of bolts possible in a transactional topology: -1. [BasicBolt](/javadoc/apidocs/backtype/storm/topology/base/BaseBasicBolt.html): This bolt doesn't deal with batches of tuples and just emits tuples based on a single tuple of input. -2. [BatchBolt](/javadoc/apidocs/backtype/storm/topology/base/BaseBatchBolt.html): This bolt processes batches of tuples. `execute` is called for each tuple, and `finishBatch` is called when the batch is complete. -3. BatchBolt's that are marked as committers: The only difference between this bolt and a regular batch bolt is when `finishBatch` is called. A committer bolt has `finishedBatch` called during the commit phase. The commit phase is guaranteed to occur only after all prior batches have successfully committed, and it will be retried until all bolts in the topology succeed the commit for the batch. There are two ways to make a `BatchBolt` a committer, by having the `BatchBolt` implement the [ICommitter](/javadoc/apidocs/backtype/storm/transactional/ICommitter.html) marker interface, or by using the `setCommiterBolt` method in `TransactionalTopologyBuilder`. +1. [BasicBolt](javadocs/org/apache/storm/topology/base/BaseBasicBolt.html): This bolt doesn't deal with batches of tuples and just emits tuples based on a single tuple of input. +2. [BatchBolt](javadocs/org/apache/storm/topology/base/BaseBatchBolt.html): This bolt processes batches of tuples. `execute` is called for each tuple, and `finishBatch` is called when the batch is complete. +3. BatchBolt's that are marked as committers: The only difference between this bolt and a regular batch bolt is when `finishBatch` is called. A committer bolt has `finishedBatch` called during the commit phase. The commit phase is guaranteed to occur only after all prior batches have successfully committed, and it will be retried until all bolts in the topology succeed the commit for the batch. There are two ways to make a `BatchBolt` a committer, by having the `BatchBolt` implement the [ICommitter](javadocs/org/apache/storm/transactional/ICommitter.html) marker interface, or by using the `setCommiterBolt` method in `TransactionalTopologyBuilder`. #### Processing phase vs. commit phase in bolts @@ -237,7 +237,7 @@ Notice that you don't have to do any acking or anchoring when working with trans #### Failing a transaction -When using regular bolts, you can call the `fail` method on `OutputCollector` to fail the tuple trees of which that tuple is a member. Since transactional topologies hide the acking framework from you, they provide a different mechanism to fail a batch (and cause the batch to be replayed). Just throw a [FailedException](/javadoc/apidocs/backtype/storm/topology/FailedException.html). Unlike regular exceptions, this will only cause that particular batch to replay and will not crash the process. +When using regular bolts, you can call the `fail` method on `OutputCollector` to fail the tuple trees of which that tuple is a member. Since transactional topologies hide the acking framework from you, they provide a different mechanism to fail a batch (and cause the batch to be replayed). Just throw a [FailedException](javadocs/org/apache/storm/topology/FailedException.html). Unlike regular exceptions, this will only cause that particular batch to replay and will not crash the process. ### Transactional spout @@ -251,11 +251,11 @@ The coordinator on the left is a regular Storm spout that emits a tuple whenever The need to be idempotent with respect to the tuples it emits requires a `TransactionalSpout` to store a small amount of state. The state is stored in Zookeeper. -The details of implementing a `TransactionalSpout` are in [the Javadoc](/javadoc/apidocs/backtype/storm/transactional/ITransactionalSpout.html). +The details of implementing a `TransactionalSpout` are in [the Javadoc](javadocs/org/apache/storm/transactional/ITransactionalSpout.html). #### Partitioned Transactional Spout -A common kind of transactional spout is one that reads the batches from a set of partitions across many queue brokers. For example, this is how [TransactionalKafkaSpout](https://github.com/apache/storm/tree/master/external/storm-kafka/src/jvm/storm/kafka/TransactionalKafkaSpout.java) works. An `IPartitionedTransactionalSpout` automates the bookkeeping work of managing the state for each partition to ensure idempotent replayability. See [the Javadoc](/javadoc/apidocs/backtype/storm/transactional/partitioned/IPartitionedTransactionalSpout.html) for more details. +A common kind of transactional spout is one that reads the batches from a set of partitions across many queue brokers. For example, this is how [TransactionalKafkaSpout]({{page.git-tree-base}}/external/storm-kafka/src/jvm/org/apache/storm/kafka/TransactionalKafkaSpout.java) works. An `IPartitionedTransactionalSpout` automates the bookkeeping work of managing the state for each partition to ensure idempotent replayability. See [the Javadoc](javadocs/org/apache/storm/transactional/partitioned/IPartitionedTransactionalSpout.html) for more details. ### Configuration @@ -325,7 +325,7 @@ In this scenario, tuples 41-50 are skipped. By failing all subsequent transactio By failing all subsequent transactions on failure, no tuples are skipped. This also shows that a requirement of transactional spouts is that they always emit where the last transaction left off. -A non-idempotent transactional spout is more concisely referred to as an "OpaqueTransactionalSpout" (opaque is the opposite of idempotent). [IOpaquePartitionedTransactionalSpout](/javadoc/apidocs/backtype/storm/transactional/partitioned/IOpaquePartitionedTransactionalSpout.html) is an interface for implementing opaque partitioned transactional spouts, of which [OpaqueTransactionalKafkaSpout](https://github.com/apache/storm/tree/master/external/storm-kafka/src/jvm/storm/kafka/OpaqueTransactionalKafkaSpout.java) is an example. `OpaqueTransactionalKafkaSpout` can withstand losing individual Kafka nodes without sacrificing accuracy as long as you use the update strategy as explained in this section. +A non-idempotent transactional spout is more concisely referred to as an "OpaqueTransactionalSpout" (opaque is the opposite of idempotent). [IOpaquePartitionedTransactionalSpout](javadocs/org/apache/storm/transactional/partitioned/IOpaquePartitionedTransactionalSpout.html) is an interface for implementing opaque partitioned transactional spouts, of which [OpaqueTransactionalKafkaSpout]({{page.git-tree-base}}/external/storm-kafka/src/jvm/org/apache/storm/kafka/OpaqueTransactionalKafkaSpout.java) is an example. `OpaqueTransactionalKafkaSpout` can withstand losing individual Kafka nodes without sacrificing accuracy as long as you use the update strategy as explained in this section. ## Implementation diff --git a/docs/Trident-API-Overview.md b/docs/Trident-API-Overview.md new file mode 100644 index 00000000000..ef743dbfcc9 --- /dev/null +++ b/docs/Trident-API-Overview.md @@ -0,0 +1,642 @@ +--- +title: Trident API Overview +layout: documentation +documentation: true +--- + +The core data model in Trident is the "Stream", processed as a series of batches. A stream is partitioned among the nodes in the cluster, and operations applied to a stream are applied in parallel across each partition. + +There are five kinds of operations in Trident: + +1. Operations that apply locally to each partition and cause no network transfer +2. Repartitioning operations that repartition a stream but otherwise don't change the contents (involves network transfer) +3. Aggregation operations that do network transfer as part of the operation +4. Operations on grouped streams +5. Merges and joins + +## Partition-local operations + +Partition-local operations involve no network transfer and are applied to each batch partition independently. + +### Functions + +A function takes in a set of input fields and emits zero or more tuples as output. The fields of the output tuple are appended to the original input tuple in the stream. If a function emits no tuples, the original input tuple is filtered out. Otherwise, the input tuple is duplicated for each output tuple. Suppose you have this function: + +```java +public class MyFunction extends BaseFunction { + public void execute(TridentTuple tuple, TridentCollector collector) { + for(int i=0; i < tuple.getInteger(0); i++) { + collector.emit(new Values(i)); + } + } +} +``` + +Now suppose you have a stream in the variable "mystream" with the fields ["a", "b", "c"] with the following tuples: + +``` +[1, 2, 3] +[4, 1, 6] +[3, 0, 8] +``` + +If you run this code: + +```java +mystream.each(new Fields("b"), new MyFunction(), new Fields("d"))) +``` + +The resulting tuples would have fields ["a", "b", "c", "d"] and look like this: + +``` +[1, 2, 3, 0] +[1, 2, 3, 1] +[4, 1, 6, 0] +``` + +### Filters + +Filters take in a tuple as input and decide whether or not to keep that tuple or not. Suppose you had this filter: + +```java +public class MyFilter extends BaseFilter { + public boolean isKeep(TridentTuple tuple) { + return tuple.getInteger(0) == 1 && tuple.getInteger(1) == 2; + } +} +``` + +Now suppose you had these tuples with fields ["a", "b", "c"]: + +``` +[1, 2, 3] +[2, 1, 1] +[2, 3, 4] +``` + +If you ran this code: + +```java +mystream.filter(new MyFilter()) +``` + +The resulting tuples would be: + +``` +[1, 2, 3] +``` + +### map and flatMap + +`map` returns a stream consisting of the result of applying the given mapping function to the tuples of the stream. This +can be used to apply a one-one transformation to the tuples. + +For example, if there is a stream of words and you wanted to convert it to a stream of upper case words, +you could define a mapping function as follows, + +```java +public class UpperCase extends MapFunction { + @Override + public Values execute(TridentTuple input) { + return new Values(input.getString(0).toUpperCase()); + } +} +``` + +The mapping function can then be applied on the stream to produce a stream of uppercase words. + +```java +mystream.map(new UpperCase()) +``` + +`flatMap` is similar to `map` but has the effect of applying a one-to-many transformation to the values of the stream, +and then flattening the resulting elements into a new stream. + +For example, if there is a stream of sentences and you wanted to convert it to a stream of words, +you could define a flatMap function as follows, + +```java +public class Split extends FlatMapFunction { + @Override + public Iterable execute(TridentTuple input) { + List valuesList = new ArrayList<>(); + for (String word : input.getString(0).split(" ")) { + valuesList.add(new Values(word)); + } + return valuesList; + } +} +``` + +The flatMap function can then be applied on the stream of sentences to produce a stream of words, + +```java +mystream.flatMap(new Split()) +``` + +Of course these operations can be chained, so a stream of uppercase words can be obtained from a stream of sentences as follows, + +```java +mystream.flatMap(new Split()).map(new UpperCase()) +``` + +If you don't pass output fields as parameter, map and flatMap preserves the input fields to output fields. + +If you want to apply MapFunction or FlatMapFunction with replacing old fields with new output fields, +you can call map / flatMap with additional Fields parameter as follows, + +```java +mystream.map(new UpperCase(), new Fields("uppercased")) +``` + +Output stream wil have only one output field "uppercased" regardless of what output fields previous stream had. +Same thing applies to flatMap, so following is valid as well, + +```java +mystream.flatMap(new Split(), new Fields("word")) +``` + +### peek +`peek` can be used to perform an additional action on each trident tuple as they flow through the stream. + This could be useful for debugging to see the tuples as they flow past a certain point in a pipeline. + +For example, the below code would print the result of converting the words to uppercase before they are passed to `groupBy` + +```java + mystream.flatMap(new Split()).map(new UpperCase()) + .peek(new Consumer() { + @Override + public void accept(TridentTuple input) { + System.out.println(input.getString(0)); + } + }) + .groupBy(new Fields("word")) + .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count")) +``` + +### min and minBy +`min` and `minBy` operations return minimum value on each partition of a batch of tuples in a trident stream. + +Suppose, a trident stream contains fields ["device-id", "count"] and the following partitions of tuples + +``` +Partition 0: +[123, 2] +[113, 54] +[23, 28] +[237, 37] +[12, 23] +[62, 17] +[98, 42] + +Partition 1: +[64, 18] +[72, 54] +[2, 28] +[742, 71] +[98, 45] +[62, 12] +[19, 174] + + +Partition 2: +[27, 94] +[82, 23] +[9, 86] +[53, 71] +[74, 37] +[51, 49] +[37, 98] +``` + +`minBy` operation can be applied on the above stream of tuples like below which results in emitting tuples with minimum values of `count` field in each partition. + +```java + mystream.minBy(new Fields("count")) +``` + +Result of the above code on mentioned partitions is: + +``` +Partition 0: +[123, 2] + + +Partition 1: +[62, 12] + + +Partition 2: +[82, 23] +``` + +You can look at other `min` and `minBy` operations on Stream +``` java + public Stream minBy(String inputFieldName, Comparator comparator) + public Stream min(Comparator comparator) +``` +Below example shows how these APIs can be used to find minimum using respective Comparators on a tuple. + +``` java + + FixedBatchSpout spout = new FixedBatchSpout(allFields, 10, Vehicle.generateVehicles(20)); + + TridentTopology topology = new TridentTopology(); + Stream vehiclesStream = topology.newStream("spout1", spout). + each(allFields, new Debug("##### vehicles")); + + Stream slowVehiclesStream = + vehiclesStream + .min(new SpeedComparator()) // Comparator w.r.t speed on received tuple. + .each(vehicleField, new Debug("#### slowest vehicle")); + + vehiclesStream + .minBy(Vehicle.FIELD_NAME, new EfficiencyComparator()) // Comparator w.r.t efficiency on received tuple. + .each(vehicleField, new Debug("#### least efficient vehicle")); + +``` +Example applications of these APIs can be located at [TridentMinMaxOfDevicesTopology](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/org/apache/storm/starter/trident/TridentMinMaxOfDevicesTopology.java) and [TridentMinMaxOfVehiclesTopology](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/org/apache/storm/starter/trident/TridentMinMaxOfVehiclesTopology.java) + +### max and maxBy +`max` and `maxBy` operations return maximum value on each partition of a batch of tuples in a trident stream. + +Suppose, a trident stream contains fields ["device-id", "count"] as mentioned in the above section. + +`max` and `maxBy` operations can be applied on the above stream of tuples like below which results in emitting tuples with maximum values of `count` field for each partition. + +``` java + mystream.maxBy(new Fields("count")) +``` +Result of the above code on mentioned partitions is: + +``` +Partition 0: +[113, 54] + + +Partition 1: +[19, 174] + + +Partition 2: +[37, 98] + +``` + +You can look at other `max` and `maxBy` functions on Stream + +``` java + + public Stream maxBy(String inputFieldName, Comparator comparator) + public Stream max(Comparator comparator) + +``` + +Below example shows how these APIs can be used to find maximum using respective Comparators on a tuple. + +``` java + + FixedBatchSpout spout = new FixedBatchSpout(allFields, 10, Vehicle.generateVehicles(20)); + + TridentTopology topology = new TridentTopology(); + Stream vehiclesStream = topology.newStream("spout1", spout). + each(allFields, new Debug("##### vehicles")); + + vehiclesStream + .max(new SpeedComparator()) // Comparator w.r.t speed on received tuple. + .each(vehicleField, new Debug("#### fastest vehicle")) + .project(driverField) + .each(driverField, new Debug("##### fastest driver")); + + vehiclesStream + .maxBy(Vehicle.FIELD_NAME, new EfficiencyComparator()) // Comparator w.r.t efficiency on received tuple. + .each(vehicleField, new Debug("#### most efficient vehicle")); + +``` + +Example applications of these APIs can be located at [TridentMinMaxOfDevicesTopology](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/org/apache/storm/starter/trident/TridentMinMaxOfDevicesTopology.java) and [TridentMinMaxOfVehiclesTopology](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/org/apache/storm/starter/trident/TridentMinMaxOfVehiclesTopology.java) + +### Windowing +Trident streams can process tuples in batches which are of the same window and emit aggregated result to the next operation. +There are two kinds of windowing supported which are based on processing time or tuples count: + 1. Tumbling window + 2. Sliding window + +#### Tumbling window +Tuples are grouped in a single window based on processing time or count. Any tuple belongs to only one of the windows. + +```java + + /** + * Returns a stream of tuples which are aggregated results of a tumbling window with every {@code windowCount} of tuples. + */ + public Stream tumblingWindow(int windowCount, WindowsStoreFactory windowStoreFactory, + Fields inputFields, Aggregator aggregator, Fields functionFields); + + /** + * Returns a stream of tuples which are aggregated results of a window that tumbles at duration of {@code windowDuration} + */ + public Stream tumblingWindow(BaseWindowedBolt.Duration windowDuration, WindowsStoreFactory windowStoreFactory, + Fields inputFields, Aggregator aggregator, Fields functionFields); + +``` + +#### Sliding window +Tuples are grouped in windows and window slides for every sliding interval. A tuple can belong to more than one window. + +```java + + /** + * Returns a stream of tuples which are aggregated results of a sliding window with every {@code windowCount} of tuples + * and slides the window after {@code slideCount}. + */ + public Stream slidingWindow(int windowCount, int slideCount, WindowsStoreFactory windowStoreFactory, + Fields inputFields, Aggregator aggregator, Fields functionFields); + + /** + * Returns a stream of tuples which are aggregated results of a window which slides at duration of {@code slidingInterval} + * and completes a window at {@code windowDuration} + */ + public Stream slidingWindow(BaseWindowedBolt.Duration windowDuration, BaseWindowedBolt.Duration slidingInterval, + WindowsStoreFactory windowStoreFactory, Fields inputFields, Aggregator aggregator, Fields functionFields); +``` + +Examples of tumbling and sliding windows can be found [here](Windowing.html) + +#### Common windowing API +Below is the common windowing API which takes `WindowConfig` for any supported windowing configurations. + +```java + + public Stream window(WindowConfig windowConfig, WindowsStoreFactory windowStoreFactory, Fields inputFields, + Aggregator aggregator, Fields functionFields) + +``` + +`windowConfig` can be any of the below. + - `SlidingCountWindow.of(int windowCount, int slidingCount)` + - `SlidingDurationWindow.of(BaseWindowedBolt.Duration windowDuration, BaseWindowedBolt.Duration slidingDuration)` + - `TumblingCountWindow.of(int windowLength)` + - `TumblingDurationWindow.of(BaseWindowedBolt.Duration windowLength)` + + +Trident windowing APIs need `WindowsStoreFactory` to store received tuples and aggregated values. Currently, basic implementation +for HBase is given with `HBaseWindowsStoreFactory`. It can further be extended to address respective usecases. +Example of using `HBaseWindowStoreFactory` for windowing can be seen below. + +```java + + // window-state table should already be created with cf:tuples column + HBaseWindowsStoreFactory windowStoreFactory = new HBaseWindowsStoreFactory(new HashMap(), "window-state", "cf".getBytes("UTF-8"), "tuples".getBytes("UTF-8")); + FixedBatchSpout spout = new FixedBatchSpout(new Fields("sentence"), 3, new Values("the cow jumped over the moon"), + new Values("the man went to the store and bought some candy"), new Values("four score and seven years ago"), + new Values("how many apples can you eat"), new Values("to be or not to be the person")); + spout.setCycle(true); + + TridentTopology topology = new TridentTopology(); + + Stream stream = topology.newStream("spout1", spout).parallelismHint(16).each(new Fields("sentence"), + new Split(), new Fields("word")) + .window(TumblingCountWindow.of(1000), windowStoreFactory, new Fields("word"), new CountAsAggregator(), new Fields("count")) + .peek(new Consumer() { + @Override + public void accept(TridentTuple input) { + LOG.info("Received tuple: [{}]", input); + } + }); + + StormTopology stormTopology = topology.build(); + +``` + +Detailed description of all the above APIs in this section can be found [here](javadocs/org/apache/storm/trident/Stream.html) + +#### Example applications +Example applications of these APIs are located at [TridentHBaseWindowingStoreTopology]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/trident/TridentHBaseWindowingStoreTopology.java) +and [TridentWindowingInmemoryStoreTopology]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/trident/TridentWindowingInmemoryStoreTopology.java) + + +### partitionAggregate + +partitionAggregate runs a function on each partition of a batch of tuples. Unlike functions, the tuples emitted by partitionAggregate replace the input tuples given to it. Consider this example: + +```java +mystream.partitionAggregate(new Fields("b"), new Sum(), new Fields("sum")) +``` + +Suppose the input stream contained fields ["a", "b"] and the following partitions of tuples: + +``` +Partition 0: +["a", 1] +["b", 2] + +Partition 1: +["a", 3] +["c", 8] + +Partition 2: +["e", 1] +["d", 9] +["d", 10] +``` + +Then the output stream of that code would contain these tuples with one field called "sum": + +``` +Partition 0: +[3] + +Partition 1: +[11] + +Partition 2: +[20] +``` + +There are three different interfaces for defining aggregators: CombinerAggregator, ReducerAggregator, and Aggregator. + +Here's the interface for CombinerAggregator: + +```java +public interface CombinerAggregator extends Serializable { + T init(TridentTuple tuple); + T combine(T val1, T val2); + T zero(); +} +``` + +A CombinerAggregator returns a single tuple with a single field as output. CombinerAggregators run the init function on each input tuple and use the combine function to combine values until there's only one value left. If there's no tuples in the partition, the CombinerAggregator emits the output of the zero function. For example, here's the implementation of Count: + +```java +public class Count implements CombinerAggregator { + public Long init(TridentTuple tuple) { + return 1L; + } + + public Long combine(Long val1, Long val2) { + return val1 + val2; + } + + public Long zero() { + return 0L; + } +} +``` + +The benefits of CombinerAggregators are seen when you use them with the aggregate method instead of partitionAggregate. In that case, Trident automatically optimizes the computation by doing partial aggregations before transferring tuples over the network. + +A ReducerAggregator has the following interface: + +```java +public interface ReducerAggregator extends Serializable { + T init(); + T reduce(T curr, TridentTuple tuple); +} +``` + +A ReducerAggregator produces an initial value with init, and then it iterates on that value for each input tuple to produce a single tuple with a single value as output. For example, here's how you would define Count as a ReducerAggregator: + +```java +public class Count implements ReducerAggregator { + public Long init() { + return 0L; + } + + public Long reduce(Long curr, TridentTuple tuple) { + return curr + 1; + } +} +``` + +ReducerAggregator can also be used with persistentAggregate, as you'll see later. + +The most general interface for performing aggregations is Aggregator, which looks like this: + +```java +public interface Aggregator extends Operation { + T init(Object batchId, TridentCollector collector); + void aggregate(T state, TridentTuple tuple, TridentCollector collector); + void complete(T state, TridentCollector collector); +} +``` + +Aggregators can emit any number of tuples with any number of fields. They can emit tuples at any point during execution. Aggregators execute in the following way: + +1. The init method is called before processing the batch. The return value of init is an Object that will represent the state of the aggregation and will be passed into the aggregate and complete methods. +2. The aggregate method is called for each input tuple in the batch partition. This method can update the state and optionally emit tuples. +3. The complete method is called when all tuples for the batch partition have been processed by aggregate. + +Here's how you would implement Count as an Aggregator: + +```java +public class CountAgg extends BaseAggregator { + static class CountState { + long count = 0; + } + + public CountState init(Object batchId, TridentCollector collector) { + return new CountState(); + } + + public void aggregate(CountState state, TridentTuple tuple, TridentCollector collector) { + state.count+=1; + } + + public void complete(CountState state, TridentCollector collector) { + collector.emit(new Values(state.count)); + } +} +``` + +Sometimes you want to execute multiple aggregators at the same time. This is called chaining and can be accomplished like this: + +```java +mystream.chainedAgg() + .partitionAggregate(new Count(), new Fields("count")) + .partitionAggregate(new Fields("b"), new Sum(), new Fields("sum")) + .chainEnd() +``` + +This code will run the Count and Sum aggregators on each partition. The output will contain a single tuple with the fields ["count", "sum"]. + +### stateQuery and partitionPersist + +stateQuery and partitionPersist query and update sources of state, respectively. You can read about how to use them on [Trident state doc](Trident-state.html). + +### projection + +The projection method on Stream keeps only the fields specified in the operation. If you had a Stream with fields ["a", "b", "c", "d"] and you ran this code: + +```java +mystream.project(new Fields("b", "d")) +``` + +The output stream would contain only the fields ["b", "d"]. + + +## Repartitioning operations + +Repartitioning operations run a function to change how the tuples are partitioned across tasks. The number of partitions can also change as a result of repartitioning (for example, if the parallelism hint is greater after repartioning). Repartitioning requires network transfer. Here are the repartitioning functions: + +1. shuffle: Use random round robin algorithm to evenly redistribute tuples across all target partitions +2. broadcast: Every tuple is replicated to all target partitions. This can useful during DRPC – for example, if you need to do a stateQuery on every partition of data. +3. partitionBy: partitionBy takes in a set of fields and does semantic partitioning based on that set of fields. The fields are hashed and modded by the number of target partitions to select the target partition. partitionBy guarantees that the same set of fields always goes to the same target partition. +4. global: All tuples are sent to the same partition. The same partition is chosen for all batches in the stream. +5. batchGlobal: All tuples in the batch are sent to the same partition. Different batches in the stream may go to different partitions. +6. partition: This method takes in a custom partitioning function that implements org.apache.storm.grouping.CustomStreamGrouping + +## Aggregation operations + +Trident has aggregate and persistentAggregate methods for doing aggregations on Streams. aggregate is run on each batch of the stream in isolation, while persistentAggregate will aggregation on all tuples across all batches in the stream and store the result in a source of state. + +Running aggregate on a Stream does a global aggregation. When you use a ReducerAggregator or an Aggregator, the stream is first repartitioned into a single partition, and then the aggregation function is run on that partition. When you use a CombinerAggregator, on the other hand, first Trident will compute partial aggregations of each partition, then repartition to a single partition, and then finish the aggregation after the network transfer. CombinerAggregator's are far more efficient and should be used when possible. + +Here's an example of using aggregate to get a global count for a batch: + +```java +mystream.aggregate(new Count(), new Fields("count")) +``` + +Like partitionAggregate, aggregators for aggregate can be chained. However, if you chain a CombinerAggregator with a non-CombinerAggregator, Trident is unable to do the partial aggregation optimization. + +You can read more about how to use persistentAggregate in the [Trident state doc](Trident-state.html). + +## Operations on grouped streams + +The groupBy operation repartitions the stream by doing a partitionBy on the specified fields, and then within each partition groups tuples together whose group fields are equal. For example, here's an illustration of a groupBy operation: + +![Grouping](images/grouping.png) + +If you run aggregators on a grouped stream, the aggregation will be run within each group instead of against the whole batch. persistentAggregate can also be run on a GroupedStream, in which case the results will be stored in a [MapState]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/state/map/MapState.java) with the key being the grouping fields. You can read more about persistentAggregate in the [Trident state doc](Trident-state.html). + +Like regular streams, aggregators on grouped streams can be chained. + +## Merges and joins + +The last part of the API is combining different streams together. The simplest way to combine streams is to merge them into one stream. You can do that with the TridentTopology#merge method, like so: + +```java +topology.merge(stream1, stream2, stream3); +``` + +Trident will name the output fields of the new, merged stream as the output fields of the first stream. + +Another way to combine streams is with a join. Now, a standard join, like the kind from SQL, require finite input. So they don't make sense with infinite streams. Joins in Trident only apply within each small batch that comes off of the spout. + +Here's an example join between a stream containing fields ["key", "val1", "val2"] and another stream containing ["x", "val1"]: + +```java +topology.join(stream1, new Fields("key"), stream2, new Fields("x"), new Fields("key", "a", "b", "c")); +``` + +This joins stream1 and stream2 together using "key" and "x" as the join fields for each respective stream. Then, Trident requires that all the output fields of the new stream be named, since the input streams could have overlapping field names. The tuples emitted from the join will contain: + +1. First, the list of join fields. In this case, "key" corresponds to "key" from stream1 and "x" from stream2. +2. Next, a list of all non-join fields from all streams, in order of how the streams were passed to the join method. In this case, "a" and "b" correspond to "val1" and "val2" from stream1, and "c" corresponds to "val1" from stream2. + +When a join happens between streams originating from different spouts, those spouts will be synchronized with how they emit batches. That is, a batch of processing will include tuples from each spout. + +You might be wondering – how do you do something like a "windowed join", where tuples from one side of the join are joined against the last hour of tuples from the other side of the join. + +To do this, you would make use of partitionPersist and stateQuery. The last hour of tuples from one side of the join would be stored and rotated in a source of state, keyed by the join field. Then the stateQuery would do lookups by the join field to perform the "join". diff --git a/docs/Trident-RAS-API.md b/docs/Trident-RAS-API.md new file mode 100644 index 00000000000..ce18e02671e --- /dev/null +++ b/docs/Trident-RAS-API.md @@ -0,0 +1,56 @@ +--- +title: Trident RAS API +layout: documentation +documentation: true +--- + +## Trident RAS API + +The Trident RAS (Resource Aware Scheduler) API provides a mechanism to allow users to specify the resource consumption of a Trident topology. The API looks exactly like the base RAS API, only it is called on Trident Streams instead of Bolts and Spouts. + +In order to avoid duplication and inconsistency in documentation, the purpose and effects of resource setting are not described here, but are instead found in the [Resource Aware Scheduler Overview](Resource_Aware_Scheduler_overview.html) + +### Use + +First, an example: + +```java + TridentTopology topo = new TridentTopology(); + topo.setResourceDefaults(new DefaultResourceDeclarer(); + .setMemoryLoad(128) + .setCPULoad(20)); + TridentState wordCounts = + topology + .newStream("words", feeder) + .parallelismHint(5) + .setCPULoad(20) + .setMemoryLoad(512,256) + .each( new Fields("sentence"), new Split(), new Fields("word")) + .setCPULoad(10) + .setMemoryLoad(512) + .each(new Fields("word"), new BangAdder(), new Fields("word!")) + .parallelismHint(10) + .setCPULoad(50) + .setMemoryLoad(1024) + .each(new Fields("word!"), new QMarkAdder(), new Fields("word!?")) + .groupBy(new Fields("word!")) + .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count")) + .setCPULoad(100) + .setMemoryLoad(2048); +``` + +Resources can be set for each operation (except for grouping, shuffling, partitioning). +Operations that are combined by Trident into single Bolts will have their resources summed. + +Every Bolt is given **at least** the default resources, regardless of user settings. + +In the above case, we end up with + + +- a spout and spout coordinator with a CPU load of 20% each, and a memory load of 512MiB on-heap and 256MiB off-heap. +- a bolt with 80% cpu load (10% + 50% + 20%) and a memory load of 1664MiB (1024 + 512 + 128) on-heap from the combined `Split` and `BangAdder` and the `QMarkAdder` which used the default resources contained in the DefaultResourceDeclarer +- a bolt with 100% cpu load and a memory load of 2048MiB on-heap, with default value for off-heap. + +Resource declarations may be called after any operation. The operations without explicit resources will get the defaults. If you choose to set resources for only some operations, defaults must be declared, or topology submission will fail. +Resource declarations have the same *boundaries* as parallelism hints. They don't cross any groupings, shufflings, or any other kind of repartitioning. +Resources are declared per operation, but get combined within boundaries. diff --git a/docs/Trident-spouts.md b/docs/Trident-spouts.md new file mode 100644 index 00000000000..e433c4e4e58 --- /dev/null +++ b/docs/Trident-spouts.md @@ -0,0 +1,44 @@ +--- +title: Trident Spouts +layout: documentation +documentation: true +--- +# Trident spouts + +Like in the vanilla Storm API, spouts are the source of streams in a Trident topology. On top of the vanilla Storm spouts, Trident exposes additional APIs for more sophisticated spouts. + +There is an inextricable link between how you source your data streams and how you update state (e.g. databases) based on those data streams. See [Trident state doc](Trident-state.html) for an explanation of this – understanding this link is imperative for understanding the spout options available. + +Regular Storm spouts will be non-transactional spouts in a Trident topology. To use a regular Storm IRichSpout, create the stream like this in a TridentTopology: + +```java +TridentTopology topology = new TridentTopology(); +topology.newStream("myspoutid", new MyRichSpout()); +``` + +All spouts in a Trident topology are required to be given a unique identifier for the stream – this identifier must be unique across all topologies run on the cluster. Trident will use this identifier to store metadata about what the spout has consumed in Zookeeper, including the txid and any metadata associated with the spout. + +You can configure the Zookeeper storage of spout metadata via the following configuration options: + +1. `transactional.zookeeper.servers`: A list of Zookeeper hostnames +2. `transactional.zookeeper.port`: The port of the Zookeeper cluster +3. `transactional.zookeeper.root`: The root dir in Zookeeper where metadata is stored. Metadata will be stored at the path / + +## Pipelining + +By default, Trident processes a single batch at a time, waiting for the batch to succeed or fail before trying another batch. You can get significantly higher throughput – and lower latency of processing of each batch – by pipelining the batches. You configure the maximum amount of batches to be processed simultaneously with the "topology.max.spout.pending" property. + +Even while processing multiple batches simultaneously, Trident will order any state updates taking place in the topology among batches. For example, suppose you're doing a global count aggregation into a database. The idea is that while you're updating the count in the database for batch 1, you can still be computing the partial counts for batches 2 through 10. Trident won't move on to the state updates for batch 2 until the state updates for batch 1 have succeeded. This is essential for achieving exactly-once processing semantics, as outline in [Trident state doc](Trident-state.html). + +## Trident spout types + +Here are the following spout APIs available: + +1. [ITridentSpout]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/spout/ITridentSpout.java): The most general API that can support transactional or opaque transactional semantics. Generally you'll use one of the partitioned flavors of this API rather than this one directly. +2. [IBatchSpout]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/spout/IBatchSpout.java): A non-transactional spout that emits batches of tuples at a time +3. [IPartitionedTridentSpout]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/spout/IPartitionedTridentSpout.java): A transactional spout that reads from a partitioned data source (like a cluster of Kafka servers) +4. [IOpaquePartitionedTridentSpout]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/spout/IOpaquePartitionedTridentSpout.java): An opaque transactional spout that reads from a partitioned data source + +And, like mentioned in the beginning of this tutorial, you can use regular IRichSpout's as well. + + diff --git a/docs/documentation/Trident-state.md b/docs/Trident-state.md similarity index 91% rename from docs/documentation/Trident-state.md rename to docs/Trident-state.md index d7fc75ef85d..bb5b1ee70b0 100644 --- a/docs/documentation/Trident-state.md +++ b/docs/Trident-state.md @@ -28,7 +28,7 @@ Remember, Trident processes tuples as small batches with each batch being given 2. There's no overlap between batches of tuples (tuples are in one batch or another, never multiple). 3. Every tuple is in a batch (no tuples are skipped) -This is a pretty easy type of spout to understand, the stream is divided into fixed batches that never change. storm-contrib has [an implementation of a transactional spout](https://github.com/apache/storm/tree/master/external/storm-kafka/src/jvm/storm/kafka/trident/TransactionalTridentKafkaSpout.java) for Kafka. +This is a pretty easy type of spout to understand, the stream is divided into fixed batches that never change. storm-contrib has [an implementation of a transactional spout]({{page.git-tree-base}}/external/storm-kafka/src/jvm/org/apache/storm/kafka/trident/TransactionalTridentKafkaSpout.java) for Kafka. You might be wondering – why wouldn't you just always use a transactional spout? They're simple and easy to understand. One reason you might not use one is because they're not necessarily very fault-tolerant. For example, the way TransactionalTridentKafkaSpout works is the batch for a txid will contain tuples from all the Kafka partitions for a topic. Once a batch has been emitted, any time that batch is re-emitted in the future the exact same set of tuples must be emitted to meet the semantics of transactional spouts. Now suppose a batch is emitted from TransactionalTridentKafkaSpout, the batch fails to process, and at the same time one of the Kafka nodes goes down. You're now incapable of replaying the same batch as you did before (since the node is down and some partitions for the topic are not unavailable), and processing will halt. @@ -72,7 +72,7 @@ As described before, an opaque transactional spout cannot guarantee that the bat 1. Every tuple is *successfully* processed in exactly one batch. However, it's possible for a tuple to fail to process in one batch and then succeed to process in a later batch. -[OpaqueTridentKafkaSpout](https://github.com/apache/storm/tree/master/external/storm-kafka/src/jvm/storm/kafka/trident/OpaqueTridentKafkaSpout.java) is a spout that has this property and is fault-tolerant to losing Kafka nodes. Whenever it's time for OpaqueTridentKafkaSpout to emit a batch, it emits tuples starting from where the last batch finished emitting. This ensures that no tuple is ever skipped or successfully processed by multiple batches. +[OpaqueTridentKafkaSpout]({{page.git-tree-base}}/external/storm-kafka/src/jvm/org/apache/storm/kafka/trident/OpaqueTridentKafkaSpout.java) is a spout that has this property and is fault-tolerant to losing Kafka nodes. Whenever it's time for OpaqueTridentKafkaSpout to emit a batch, it emits tuples starting from where the last batch finished emitting. This ensures that no tuple is ever skipped or successfully processed by multiple batches. With opaque transactional spouts, it's no longer possible to use the trick of skipping state updates if the transaction id in the database is the same as the transaction id for the current batch. This is because the batch may have changed between state updates. @@ -309,7 +309,7 @@ public interface Snapshottable extends State { } ``` -[MemoryMapState](https://github.com/apache/storm/blob/master/storm-core/src/jvm/storm/trident/testing/MemoryMapState.java) and [MemcachedState](https://github.com/nathanmarz/trident-memcached/blob/master/src/jvm/trident/memcached/MemcachedState.java) each implement both of these interfaces. +[MemoryMapState]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/testing/MemoryMapState.java) and [MemcachedState](https://github.com/nathanmarz/trident-memcached/blob/{{page.version}}/src/jvm/trident/memcached/MemcachedState.java) each implement both of these interfaces. ## Implementing Map States @@ -322,10 +322,10 @@ public interface IBackingMap { } ``` -OpaqueMap's will call multiPut with [OpaqueValue](https://github.com/apache/storm/blob/master/storm-core/src/jvm/storm/trident/state/OpaqueValue.java)'s for the vals, TransactionalMap's will give [TransactionalValue](https://github.com/apache/storm/blob/master/storm-core/src/jvm/storm/trident/state/TransactionalValue.java)'s for the vals, and NonTransactionalMaps will just pass the objects from the topology through. +OpaqueMap's will call multiPut with [OpaqueValue]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/state/OpaqueValue.java)'s for the vals, TransactionalMap's will give [TransactionalValue]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/state/TransactionalValue.java)'s for the vals, and NonTransactionalMaps will just pass the objects from the topology through. -Trident also provides the [CachedMap](https://github.com/apache/storm/blob/master/storm-core/src/jvm/storm/trident/state/map/CachedMap.java) class to do automatic LRU caching of map key/vals. +Trident also provides the [CachedMap]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/state/map/CachedMap.java) class to do automatic LRU caching of map key/vals. -Finally, Trident provides the [SnapshottableMap](https://github.com/apache/storm/blob/master/storm-core/src/jvm/storm/trident/state/map/SnapshottableMap.java) class that turns a MapState into a Snapshottable object, by storing global aggregations into a fixed key. +Finally, Trident provides the [SnapshottableMap]({{page.git-blob-base}}/storm-core/src/jvm/org/apache/storm/trident/state/map/SnapshottableMap.java) class that turns a MapState into a Snapshottable object, by storing global aggregations into a fixed key. Take a look at the implementation of [MemcachedState](https://github.com/nathanmarz/trident-memcached/blob/master/src/jvm/trident/memcached/MemcachedState.java) to see how all these utilities can be put together to make a high performance MapState implementation. MemcachedState allows you to choose between opaque transactional, transactional, and non-transactional semantics. diff --git a/docs/documentation/Trident-tutorial.md b/docs/Trident-tutorial.md similarity index 99% rename from docs/documentation/Trident-tutorial.md rename to docs/Trident-tutorial.md index 48fdd8dc9ef..6ad9103d905 100644 --- a/docs/documentation/Trident-tutorial.md +++ b/docs/Trident-tutorial.md @@ -235,7 +235,7 @@ Trident solves this problem by doing two things: With these two primitives, you can achieve exactly-once semantics with your state updates. Rather than store just the count in the database, what you can do instead is store the transaction id with the count in the database as an atomic value. Then, when updating the count, you can just compare the transaction id in the database with the transaction id for the current batch. If they're the same, you skip the update – because of the strong ordering, you know for sure that the value in the database incorporates the current batch. If they're different, you increment the count. -Of course, you don't have to do this logic manually in your topologies. This logic is wrapped by the State abstraction and done automatically. Nor is your State object required to implement the transaction id trick: if you don't want to pay the cost of storing the transaction id in the database, you don't have to. In that case the State will have at-least-once-processing semantics in the case of failures (which may be fine for your application). You can read more about how to implement a State and the various fault-tolerance tradeoffs possible [in this doc](/documentation/Trident-state). +Of course, you don't have to do this logic manually in your topologies. This logic is wrapped by the State abstraction and done automatically. Nor is your State object required to implement the transaction id trick: if you don't want to pay the cost of storing the transaction id in the database, you don't have to. In that case the State will have at-least-once-processing semantics in the case of failures (which may be fine for your application). You can read more about how to implement a State and the various fault-tolerance tradeoffs possible [in this doc](/documentation/Trident-state.html). A State is allowed to use whatever strategy it wants to store state. So it could store state in an external database or it could keep the state in-memory but backed by HDFS (like how HBase works). State's are not required to hold onto state forever. For example, you could have an in-memory State implementation that only keeps the last X hours of data available and drops anything older. Take a look at the implementation of the [Memcached integration](https://github.com/nathanmarz/trident-memcached/blob/master/src/jvm/trident/memcached/MemcachedState.java) for an example State implementation. @@ -251,4 +251,4 @@ It would compile into Storm spouts/bolts like this: ## Conclusion -Trident makes realtime computation elegant. You've seen how high throughput stream processing, state manipulation, and low-latency querying can be seamlessly intermixed via Trident's API. Trident lets you express your realtime computations in a natural way while still getting maximal performance. \ No newline at end of file +Trident makes realtime computation elegant. You've seen how high throughput stream processing, state manipulation, and low-latency querying can be seamlessly intermixed via Trident's API. Trident lets you express your realtime computations in a natural way while still getting maximal performance. diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md new file mode 100644 index 00000000000..d01015a0e54 --- /dev/null +++ b/docs/Troubleshooting.md @@ -0,0 +1,114 @@ +--- +title: Troubleshooting +layout: documentation +documentation: true +--- + +This page lists issues people have run into when using Storm along with their solutions. + +### Worker processes are crashing on startup with no stack trace + +Possible symptoms: + + * Topologies work with one node, but workers crash with multiple nodes + +Solutions: + + * You may have a misconfigured subnet, where nodes can't locate other nodes based on their hostname. ZeroMQ sometimes crashes the process when it can't resolve a host. There are two solutions: + * Make a mapping from hostname to IP address in /etc/hosts + * Set up an internal DNS so that nodes can locate each other based on hostname. + +### Nodes are unable to communicate with each other + +Possible symptoms: + + * Every spout tuple is failing + * Processing is not working + +Solutions: + + * Storm doesn't work with ipv6. You can force ipv4 by adding `-Djava.net.preferIPv4Stack=true` to the supervisor child options and restarting the supervisor. + * You may have a misconfigured subnet. See the solutions for `Worker processes are crashing on startup with no stack trace` + +### Topology stops processing tuples after awhile + +Symptoms: + + * Processing works fine for awhile, and then suddenly stops and spout tuples start failing en masse. + +Solutions: + + * This is a known issue with ZeroMQ 2.1.10. Downgrade to ZeroMQ 2.1.7. + +### Not all supervisors appear in Storm UI + +Symptoms: + + * Some supervisor processes are missing from the Storm UI + * List of supervisors in Storm UI changes on refreshes + +Solutions: + + * Make sure the supervisor local dirs are independent (e.g., not sharing a local dir over NFS) + * Try deleting the local dirs for the supervisors and restarting the daemons. Supervisors create a unique id for themselves and store it locally. When that id is copied to other nodes, Storm gets confused. + +### "Multiple defaults.yaml found" error + +Symptoms: + + * When deploying a topology with "storm jar", you get this error + +Solution: + + * You're most likely including the Storm jars inside your topology jar. When packaging your topology jar, don't include the Storm jars as Storm will put those on the classpath for you. + +### "NoSuchMethodError" when running storm jar + +Symptoms: + + * When running storm jar, you get a cryptic "NoSuchMethodError" + +Solution: + + * You're deploying your topology with a different version of Storm than you built your topology against. Make sure the storm client you use comes from the same version as the version you compiled your topology against. + + +### Kryo ConcurrentModificationException + +Symptoms: + + * At runtime, you get a stack trace like the following: + +``` +java.lang.RuntimeException: java.util.ConcurrentModificationException + at org.apache.storm.utils.DisruptorQueue.consumeBatchToCursor(DisruptorQueue.java:84) + at org.apache.storm.utils.DisruptorQueue.consumeBatchWhenAvailable(DisruptorQueue.java:55) + at org.apache.storm.disruptor$consume_batch_when_available.invoke(disruptor.clj:56) + at org.apache.storm.disruptor$consume_loop_STAR_$fn__1597.invoke(disruptor.clj:67) + at org.apache.storm.util$async_loop$fn__465.invoke(util.clj:377) + at clojure.lang.AFn.run(AFn.java:24) + at java.lang.Thread.run(Thread.java:679) +Caused by: java.util.ConcurrentModificationException + at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:390) + at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:409) + at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:408) + at java.util.HashMap.writeObject(HashMap.java:1016) + at sun.reflect.GeneratedMethodAccessor17.invoke(Unknown Source) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:616) + at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:959) + at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1480) + at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1416) + at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174) + at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:346) + at org.apache.storm.serialization.SerializableSerializer.write(SerializableSerializer.java:21) + at com.esotericsoftware.kryo.Kryo.writeClassAndObject(Kryo.java:554) + at com.esotericsoftware.kryo.serializers.CollectionSerializer.write(CollectionSerializer.java:77) + at com.esotericsoftware.kryo.serializers.CollectionSerializer.write(CollectionSerializer.java:18) + at com.esotericsoftware.kryo.Kryo.writeObject(Kryo.java:472) + at org.apache.storm.serialization.KryoValuesSerializer.serializeInto(KryoValuesSerializer.java:27) +``` + +Solution: + + * This means that you're emitting a mutable object as an output tuple. Everything you emit into the output collector must be immutable. What's happening is that your bolt is modifying the object while it is being serialized to be sent over the network. diff --git a/docs/documentation/Tutorial.md b/docs/Tutorial.md similarity index 90% rename from docs/documentation/Tutorial.md rename to docs/Tutorial.md index 3e1c0161f1b..5dad8340de1 100644 --- a/docs/documentation/Tutorial.md +++ b/docs/Tutorial.md @@ -7,7 +7,7 @@ In this tutorial, you'll learn how to create Storm topologies and deploy them to ## Preliminaries -This tutorial uses examples from the [storm-starter](https://github.com/apache/storm/blob/master/examples/storm-starter) project. It's recommended that you clone the project and follow along with the examples. Read [Setting up a development environment](Setting-up-development-environment.html) and [Creating a new Storm project](Creating-a-new-Storm-project.html) to get your machine set up. +This tutorial uses examples from the [storm-starter]({{page.git-blob-base}}/examples/storm-starter) project. It's recommended that you clone the project and follow along with the examples. Read [Setting up a development environment](Setting-up-development-environment.html) and [Creating a new Storm project](Creating-a-new-Storm-project.html) to get your machine set up. ## Components of a Storm cluster @@ -28,10 +28,10 @@ To do realtime computation on Storm, you create what are called "topologies". A Running a topology is straightforward. First, you package all your code and dependencies into a single jar. Then, you run a command like the following: ``` -storm jar all-my-code.jar backtype.storm.MyTopology arg1 arg2 +storm jar all-my-code.jar org.apache.storm.MyTopology arg1 arg2 ``` -This runs the class `backtype.storm.MyTopology` with the arguments `arg1` and `arg2`. The main function of the class defines the topology and submits it to Nimbus. The `storm jar` part takes care of connecting to Nimbus and uploading the jar. +This runs the class `org.apache.storm.MyTopology` with the arguments `arg1` and `arg2`. The main function of the class defines the topology and submits it to Nimbus. The `storm jar` part takes care of connecting to Nimbus and uploading the jar. Since topology definitions are just Thrift structs, and Nimbus is a Thrift service, you can create and submit topologies using any programming language. The above example is the easiest way to do it from a JVM-based language. See [Running topologies on a production cluster](Running-topologies-on-a-production-cluster.html)] for more information on starting and stopping topologies. @@ -103,11 +103,11 @@ This topology contains a spout and two bolts. The spout emits words, and each bo This code defines the nodes using the `setSpout` and `setBolt` methods. These methods take as input a user-specified id, an object containing the processing logic, and the amount of parallelism you want for the node. In this example, the spout is given id "words" and the bolts are given ids "exclaim1" and "exclaim2". -The object containing the processing logic implements the [IRichSpout](/javadoc/apidocs/backtype/storm/topology/IRichSpout.html) interface for spouts and the [IRichBolt](/javadoc/apidocs/backtype/storm/topology/IRichBolt.html) interface for bolts. +The object containing the processing logic implements the [IRichSpout](javadocs/org/apache/storm/topology/IRichSpout.html) interface for spouts and the [IRichBolt](javadocs/org/apache/storm/topology/IRichBolt.html) interface for bolts. The last parameter, how much parallelism you want for the node, is optional. It indicates how many threads should execute that component across the cluster. If you omit it, Storm will only allocate one thread for that node. -`setBolt` returns an [InputDeclarer](/javadoc/apidocs/backtype/storm/topology/InputDeclarer.html) object that is used to define the inputs to the Bolt. Here, component "exclaim1" declares that it wants to read all the tuples emitted by component "words" using a shuffle grouping, and component "exclaim2" declares that it wants to read all the tuples emitted by component "exclaim1" using a shuffle grouping. "shuffle grouping" means that tuples should be randomly distributed from the input tasks to the bolt's tasks. There are many ways to group data between components. These will be explained in a few sections. +`setBolt` returns an [InputDeclarer](javadocs/org/apache/storm/topology/InputDeclarer.html) object that is used to define the inputs to the Bolt. Here, component "exclaim1" declares that it wants to read all the tuples emitted by component "words" using a shuffle grouping, and component "exclaim2" declares that it wants to read all the tuples emitted by component "exclaim1" using a shuffle grouping. "shuffle grouping" means that tuples should be randomly distributed from the input tasks to the bolt's tasks. There are many ways to group data between components. These will be explained in a few sections. If you wanted component "exclaim2" to read all the tuples emitted by both component "words" and component "exclaim1", you would write component "exclaim2"'s definition like this: @@ -139,23 +139,28 @@ As you can see, the implementation is very straightforward. public static class ExclamationBolt implements IRichBolt { OutputCollector _collector; + @Override public void prepare(Map conf, TopologyContext context, OutputCollector collector) { _collector = collector; } + @Override public void execute(Tuple tuple) { _collector.emit(tuple, new Values(tuple.getString(0) + "!!!")); _collector.ack(tuple); } + @Override public void cleanup() { } + @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); } - public Map getComponentConfiguration() { + @Override + public Map getComponentConfiguration() { return null; } } @@ -163,9 +168,9 @@ public static class ExclamationBolt implements IRichBolt { The `prepare` method provides the bolt with an `OutputCollector` that is used for emitting tuples from this bolt. Tuples can be emitted at anytime from the bolt -- in the `prepare`, `execute`, or `cleanup` methods, or even asynchronously in another thread. This `prepare` implementation simply saves the `OutputCollector` as an instance variable to be used later on in the `execute` method. -The `execute` method receives a tuple from one of the bolt's inputs. The `ExclamationBolt` grabs the first field from the tuple and emits a new tuple with the string "!!!" appended to it. If you implement a bolt that subscribes to multiple input sources, you can find out which component the [Tuple](/javadoc/apidocs/backtype/storm/tuple/Tuple.html) came from by using the `Tuple#getSourceComponent` method. +The `execute` method receives a tuple from one of the bolt's inputs. The `ExclamationBolt` grabs the first field from the tuple and emits a new tuple with the string "!!!" appended to it. If you implement a bolt that subscribes to multiple input sources, you can find out which component the [Tuple](/javadoc/apidocs/org/apache/storm/tuple/Tuple.html) came from by using the `Tuple#getSourceComponent` method. -There's a few other things going in in the `execute` method, namely that the input tuple is passed as the first argument to `emit` and the input tuple is acked on the final line. These are part of Storm's reliability API for guaranteeing no data loss and will be explained later in this tutorial. +There's a few other things going on in the `execute` method, namely that the input tuple is passed as the first argument to `emit` and the input tuple is acked on the final line. These are part of Storm's reliability API for guaranteeing no data loss and will be explained later in this tutorial. The `cleanup` method is called when a Bolt is being shutdown and should cleanup any resources that were opened. There's no guarantee that this method will be called on the cluster: for example, if the machine the task is running on blows up, there's no way to invoke the method. The `cleanup` method is intended for when you run topologies in [local mode](Local-mode.html) (where a Storm cluster is simulated in process), and you want to be able to run and kill many topologies without suffering any resource leaks. @@ -179,15 +184,18 @@ Methods like `cleanup` and `getComponentConfiguration` are often not needed in a public static class ExclamationBolt extends BaseRichBolt { OutputCollector _collector; + @Override public void prepare(Map conf, TopologyContext context, OutputCollector collector) { _collector = collector; } + @Override public void execute(Tuple tuple) { _collector.emit(tuple, new Values(tuple.getString(0) + "!!!")); _collector.ack(tuple); } + @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); } @@ -225,7 +233,7 @@ The configuration is used to tune various aspects of the running topology. The t 1. **TOPOLOGY_WORKERS** (set with `setNumWorkers`) specifies how many _processes_ you want allocated around the cluster to execute the topology. Each component in the topology will execute as many _threads_. The number of threads allocated to a given component is configured through the `setBolt` and `setSpout` methods. Those _threads_ exist within worker _processes_. Each worker _process_ contains within it some number of _threads_ for some number of components. For instance, you may have 300 threads specified across all your components and 50 worker processes specified in your config. Each worker process will execute 6 threads, each of which of could belong to a different component. You tune the performance of Storm topologies by tweaking the parallelism for each component and the number of worker processes those threads should run within. 2. **TOPOLOGY_DEBUG** (set with `setDebug`), when set to true, tells Storm to log every message every emitted by a component. This is useful in local mode when testing topologies, but you probably want to keep this turned off when running topologies on the cluster. -There's many other configurations you can set for the topology. The various configurations are detailed on [the Javadoc for Config](/javadoc/apidocs/backtype/storm/Config.html). +There's many other configurations you can set for the topology. The various configurations are detailed on [the Javadoc for Config](javadocs/org/apache/storm/Config.html). To learn about how to set up your development environment so that you can run topologies in local mode (such as in Eclipse), see [Creating a new Storm project](Creating-a-new-Storm-project.html). @@ -237,7 +245,7 @@ A stream grouping tells a topology how to send tuples between two components. Re When a task for Bolt A emits a tuple to Bolt B, which task should it send the tuple to? -A "stream grouping" answers this question by telling Storm how to send tuples between sets of tasks. Before we dig into the different kinds of stream groupings, let's take a look at another topology from [storm-starter](http://github.com/apache/storm/blob/master/examples/storm-starter). This [WordCountTopology](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/storm/starter/WordCountTopology.java) reads sentences off of a spout and streams out of `WordCountBolt` the total number of times it has seen that word before: +A "stream grouping" answers this question by telling Storm how to send tuples between sets of tasks. Before we dig into the different kinds of stream groupings, let's take a look at another topology from [storm-starter](http://github.com/apache/storm/blob/{{page.version}}/examples/storm-starter). This [WordCountTopology]({{page.git-blob-base}}/examples/storm-starter/src/jvm/org/apache/storm/starter/WordCountTopology.java) reads sentences off of a spout and streams out of `WordCountBolt` the total number of times it has seen that word before: ```java TopologyBuilder builder = new TopologyBuilder(); @@ -309,4 +317,4 @@ This tutorial showed how to do basic stream processing on top of Storm. There's ## Conclusion -This tutorial gave a broad overview of developing, testing, and deploying Storm topologies. The rest of the documentation dives deeper into all the aspects of using Storm. \ No newline at end of file +This tutorial gave a broad overview of developing, testing, and deploying Storm topologies. The rest of the documentation dives deeper into all the aspects of using Storm. diff --git a/docs/documentation/Understanding-the-parallelism-of-a-Storm-topology.md b/docs/Understanding-the-parallelism-of-a-Storm-topology.md similarity index 82% rename from docs/documentation/Understanding-the-parallelism-of-a-Storm-topology.md rename to docs/Understanding-the-parallelism-of-a-Storm-topology.md index efa6a9b9192..6193a2bf576 100644 --- a/docs/documentation/Understanding-the-parallelism-of-a-Storm-topology.md +++ b/docs/Understanding-the-parallelism-of-a-Storm-topology.md @@ -30,25 +30,25 @@ The following sections give an overview of the various configuration options and ### Number of worker processes * Description: How many worker processes to create _for the topology_ across machines in the cluster. -* Configuration option: [TOPOLOGY_WORKERS](/javadoc/apidocs/backtype/storm/Config.html#TOPOLOGY_WORKERS) +* Configuration option: [TOPOLOGY_WORKERS](javadocs/org/apache/storm/Config.html#TOPOLOGY_WORKERS) * How to set in your code (examples): - * [Config#setNumWorkers](/javadoc/apidocs/backtype/storm/Config.html) + * [Config#setNumWorkers](javadocs/org/apache/storm/Config.html) ### Number of executors (threads) * Description: How many executors to spawn _per component_. -* Configuration option: ? +* Configuration option: None (pass ``parallelism_hint`` parameter to ``setSpout`` or ``setBolt``) * How to set in your code (examples): - * [TopologyBuilder#setSpout()](/javadoc/apidocs/backtype/storm/topology/TopologyBuilder.html) - * [TopologyBuilder#setBolt()](/javadoc/apidocs/backtype/storm/topology/TopologyBuilder.html) + * [TopologyBuilder#setSpout()](javadocs/org/apache/storm/topology/TopologyBuilder.html) + * [TopologyBuilder#setBolt()](javadocs/org/apache/storm/topology/TopologyBuilder.html) * Note that as of Storm 0.8 the ``parallelism_hint`` parameter now specifies the initial number of executors (not tasks!) for that bolt. ### Number of tasks * Description: How many tasks to create _per component_. -* Configuration option: [TOPOLOGY_TASKS](/javadoc/apidocs/backtype/storm/Config.html#TOPOLOGY_TASKS) +* Configuration option: [TOPOLOGY_TASKS](javadocs/org/apache/storm/Config.html#TOPOLOGY_TASKS) * How to set in your code (examples): - * [ComponentConfigurationDeclarer#setNumTasks()](/javadoc/apidocs/backtype/storm/topology/ComponentConfigurationDeclarer.html) + * [ComponentConfigurationDeclarer#setNumTasks()](javadocs/org/apache/storm/topology/ComponentConfigurationDeclarer.html) Here is an example code snippet to show these settings in practice: @@ -56,7 +56,7 @@ Here is an example code snippet to show these settings in practice: ```java topologyBuilder.setBolt("green-bolt", new GreenBolt(), 2) .setNumTasks(4) - .shuffleGrouping("blue-spout); + .shuffleGrouping("blue-spout"); ``` In the above code we configured Storm to run the bolt ``GreenBolt`` with an initial number of two executors and four associated tasks. Storm will run two tasks per executor (thread). If you do not explicitly configure the number of tasks, Storm will run by default one task per executor. @@ -91,7 +91,7 @@ StormSubmitter.submitTopology( And of course Storm comes with additional configuration settings to control the parallelism of a topology, including: -* [TOPOLOGY_MAX_TASK_PARALLELISM](/javadoc/apidocs/backtype/storm/Config.html#TOPOLOGY_MAX_TASK_PARALLELISM): This setting puts a ceiling on the number of executors that can be spawned for a single component. It is typically used during testing to limit the number of threads spawned when running a topology in local mode. You can set this option via e.g. [Config#setMaxTaskParallelism()](/javadoc/apidocs/backtype/storm/Config.html#setMaxTaskParallelism(int)). +* [TOPOLOGY_MAX_TASK_PARALLELISM](javadocs/org/apache/storm/Config.html#TOPOLOGY_MAX_TASK_PARALLELISM): This setting puts a ceiling on the number of executors that can be spawned for a single component. It is typically used during testing to limit the number of threads spawned when running a topology in local mode. You can set this option via e.g. [Config#setMaxTaskParallelism()](javadocs/org/apache/storm/Config.html#setMaxTaskParallelism(int)). ## How to change the parallelism of a running topology @@ -119,5 +119,5 @@ $ storm rebalance mytopology -n 5 -e blue-spout=3 -e yellow-bolt=10 * [Running topologies on a production cluster](Running-topologies-on-a-production-cluster.html)] * [Local mode](Local-mode.html) * [Tutorial](Tutorial.html) -* [Storm API documentation](/javadoc/apidocs/), most notably the class ``Config`` +* [Storm API documentation](javadocs/), most notably the class ``Config`` diff --git a/docs/documentation/Using-non-JVM-languages-with-Storm.md b/docs/Using-non-JVM-languages-with-Storm.md similarity index 98% rename from docs/documentation/Using-non-JVM-languages-with-Storm.md rename to docs/Using-non-JVM-languages-with-Storm.md index 2e5c67a3f1a..1b3ae451b91 100644 --- a/docs/documentation/Using-non-JVM-languages-with-Storm.md +++ b/docs/Using-non-JVM-languages-with-Storm.md @@ -1,4 +1,5 @@ --- +title: Using non JVM languages with Storm layout: documentation --- - two pieces: creating topologies and implementing spouts and bolts in other languages @@ -49,4 +50,4 @@ Then you can connect to Nimbus using the Thrift API and submit the topology, pas ``` void submitTopology(1: string name, 2: string uploadedJarLocation, 3: string jsonConf, 4: StormTopology topology) throws (1: AlreadyAliveException e, 2: InvalidTopologyException ite); -``` \ No newline at end of file +``` diff --git a/docs/Windowing.md b/docs/Windowing.md new file mode 100644 index 00000000000..cc4dfe4064b --- /dev/null +++ b/docs/Windowing.md @@ -0,0 +1,268 @@ +--- +title: Windowing Support in Core Storm +layout: documentation +documentation: true +--- + +Storm core has support for processing a group of tuples that falls within a window. Windows are specified with the +following two parameters, + +1. Window length - the length or duration of the window +2. Sliding interval - the interval at which the windowing slides + +## Sliding Window + +Tuples are grouped in windows and window slides every sliding interval. A tuple can belong to more than one window. + +For example a time duration based sliding window with length 10 secs and sliding interval of 5 seconds. + +``` +........| e1 e2 | e3 e4 e5 e6 | e7 e8 e9 |... +-5 0 5 10 15 -> time +|<------- w1 -->| + |<---------- w2 ----->| + |<-------------- w3 ---->| +``` + +The window is evaluated every 5 seconds and some of the tuples in the first window overlaps with the second one. + +Note: The window first slides at t = 5 secs and would contain events received up to the first five secs. + +## Tumbling Window + +Tuples are grouped in a single window based on time or count. Any tuple belongs to only one of the windows. + +For example a time duration based tumbling window with length 5 secs. + +``` +| e1 e2 | e3 e4 e5 e6 | e7 e8 e9 |... +0 5 10 15 -> time + w1 w2 w3 +``` + +The window is evaluated every five seconds and none of the windows overlap. + +Storm supports specifying the window length and sliding intervals as a count of the number of tuples or as a time duration. + +The bolt interface `IWindowedBolt` is implemented by bolts that needs windowing support. + +```java +public interface IWindowedBolt extends IComponent { + void prepare(Map stormConf, TopologyContext context, OutputCollector collector); + /** + * Process tuples falling within the window and optionally emit + * new tuples based on the tuples in the input window. + */ + void execute(TupleWindow inputWindow); + void cleanup(); +} +``` + +Every time the window activates, the `execute` method is invoked. The TupleWindow parameter gives access to the current tuples +in the window, the tuples that expired and the new tuples that are added since last window was computed which will be useful +for efficient windowing computations. + +Bolts that needs windowing support typically would extend `BaseWindowedBolt` which has the apis for specifying the +window length and sliding intervals. + +E.g. + +```java +public class SlidingWindowBolt extends BaseWindowedBolt { + private OutputCollector collector; + + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector = collector; + } + + @Override + public void execute(TupleWindow inputWindow) { + for(Tuple tuple: inputWindow.get()) { + // do the windowing computation + ... + } + // emit the results + collector.emit(new Values(computedValue)); + } +} + +public static void main(String[] args) { + TopologyBuilder builder = new TopologyBuilder(); + builder.setSpout("spout", new RandomSentenceSpout(), 1); + builder.setBolt("slidingwindowbolt", + new SlidingWindowBolt().withWindow(new Count(30), new Count(10)), + 1).shuffleGrouping("spout"); + Config conf = new Config(); + conf.setDebug(true); + conf.setNumWorkers(1); + + StormSubmitter.submitTopologyWithProgressBar(args[0], conf, builder.createTopology()); + +} +``` + +The following window configurations are supported. + +```java +withWindow(Count windowLength, Count slidingInterval) +Tuple count based sliding window that slides after `slidingInterval` number of tuples. + +withWindow(Count windowLength) +Tuple count based window that slides with every incoming tuple. + +withWindow(Count windowLength, Duration slidingInterval) +Tuple count based sliding window that slides after `slidingInterval` time duration. + +withWindow(Duration windowLength, Duration slidingInterval) +Time duration based sliding window that slides after `slidingInterval` time duration. + +withWindow(Duration windowLength) +Time duration based window that slides with every incoming tuple. + +withWindow(Duration windowLength, Count slidingInterval) +Time duration based sliding window configuration that slides after `slidingInterval` number of tuples. + +withTumblingWindow(BaseWindowedBolt.Count count) +Count based tumbling window that tumbles after the specified count of tuples. + +withTumblingWindow(BaseWindowedBolt.Duration duration) +Time duration based tumbling window that tumbles after the specified time duration. + +``` + +## Tuple timestamp and out of order tuples +By default the timestamp tracked in the window is the time when the tuple is processed by the bolt. The window calculations +are performed based on the processing timestamp. Storm has support for tracking windows based on the source generated timestamp. + +```java +/** +* Specify a field in the tuple that represents the timestamp as a long value. If this +* field is not present in the incoming tuple, an {@link IllegalArgumentException} will be thrown. +* +* @param fieldName the name of the field that contains the timestamp +*/ +public BaseWindowedBolt withTimestampField(String fieldName) +``` + +The value for the above `fieldName` will be looked up from the incoming tuple and considered for windowing calculations. +If the field is not present in the tuple an exception will be thrown. Alternatively a [TimestampExtractor](../storm-core/src/jvm/org/apache/storm/windowing/TimestampExtractor.java) can be used to +derive a timestamp value from a tuple (e.g. extract timestamp from a nested field within the tuple). + +```java +/** +* Specify the timestamp extractor implementation. +* +* @param timestampExtractor the {@link TimestampExtractor} implementation +*/ +public BaseWindowedBolt withTimestampExtractor(TimestampExtractor timestampExtractor) +``` + + +Along with the timestamp field name/extractor, a time lag parameter can also be specified which indicates the max time limit for tuples with out of order timestamps. + +```java +/** +* Specify the maximum time lag of the tuple timestamp in milliseconds. It means that the tuple timestamps +* cannot be out of order by more than this amount. +* +* @param duration the max lag duration +*/ +public BaseWindowedBolt withLag(Duration duration) +``` + +E.g. If the lag is 5 secs and a tuple `t1` arrived with timestamp `06:00:05` no tuples may arrive with tuple timestamp earlier than `06:00:00`. If a tuple +arrives with timestamp 05:59:59 after `t1` and the window has moved past `t1`, it will be treated as a late tuple. Late tuples are not processed by default, +just logged in the worker log files at INFO level. + +```java +/** + * Specify a stream id on which late tuples are going to be emitted. They are going to be accessible via the + * {@link org.apache.storm.topology.WindowedBoltExecutor#LATE_TUPLE_FIELD} field. + * It must be defined on a per-component basis, and in conjunction with the + * {@link BaseWindowedBolt#withTimestampField}, otherwise {@link IllegalArgumentException} will be thrown. + * + * @param streamId the name of the stream used to emit late tuples on + */ +public BaseWindowedBolt withLateTupleStream(String streamId) + +``` +This behaviour can be changed by specifying the above `streamId`. In this case late tuples are going to be emitted on the specified stream and accessible +via the field `WindowedBoltExecutor.LATE_TUPLE_FIELD`. + + +### Watermarks +For processing tuples with timestamp field, storm internally computes watermarks based on the incoming tuple timestamp. Watermark is +the minimum of the latest tuple timestamps (minus the lag) across all the input streams. At a higher level this is similar to the watermark concept +used by Flink and Google's MillWheel for tracking event based timestamps. + +Periodically (default every sec), the watermark timestamps are emitted and this is considered as the clock tick for the window calculation if +tuple based timestamps are in use. The interval at which watermarks are emitted can be changed with the below api. + +```java +/** +* Specify the watermark event generation interval. For tuple based timestamps, watermark events +* are used to track the progress of time +* +* @param interval the interval at which watermark events are generated +*/ +public BaseWindowedBolt withWatermarkInterval(Duration interval) +``` + + +When a watermark is received, all windows up to that timestamp will be evaluated. + +For example, consider tuple timestamp based processing with following window parameters, + +`Window length = 20s, sliding interval = 10s, watermark emit frequency = 1s, max lag = 5s` + +``` +|-----|-----|-----|-----|-----|-----|-----| +0 10 20 30 40 50 60 70 +```` + +Current ts = `09:00:00` + +Tuples `e1(6:00:03), e2(6:00:05), e3(6:00:07), e4(6:00:18), e5(6:00:26), e6(6:00:36)` are received between `9:00:00` and `9:00:01` + +At time t = `09:00:01`, watermark w1 = `6:00:31` is emitted since no tuples earlier than `6:00:31` can arrive. + +Three windows will be evaluated. The first window end ts (06:00:10) is computed by taking the earliest event timestamp (06:00:03) +and computing the ceiling based on the sliding interval (10s). + +1. `5:59:50 - 06:00:10` with tuples e1, e2, e3 +2. `6:00:00 - 06:00:20` with tuples e1, e2, e3, e4 +3. `6:00:10 - 06:00:30` with tuples e4, e5 + +e6 is not evaluated since watermark timestamp `6:00:31` is older than the tuple ts `6:00:36`. + +Tuples `e7(8:00:25), e8(8:00:26), e9(8:00:27), e10(8:00:39)` are received between `9:00:01` and `9:00:02` + +At time t = `09:00:02` another watermark w2 = `08:00:34` is emitted since no tuples earlier than `8:00:34` can arrive now. + +Three windows will be evaluated, + +1. `6:00:20 - 06:00:40` with tuples e5, e6 (from earlier batch) +2. `6:00:30 - 06:00:50` with tuple e6 (from earlier batch) +3. `8:00:10 - 08:00:30` with tuples e7, e8, e9 + +e10 is not evaluated since the tuple ts `8:00:39` is beyond the watermark time `8:00:34`. + +The window calculation considers the time gaps and computes the windows based on the tuple timestamp. + +## Guarantees +The windowing functionality in storm core currently provides at-least once guarentee. The values emitted from the bolts +`execute(TupleWindow inputWindow)` method are automatically anchored to all the tuples in the inputWindow. The downstream +bolts are expected to ack the received tuple (i.e the tuple emitted from the windowed bolt) to complete the tuple tree. +If not the tuples will be replayed and the windowing computation will be re-evaluated. + +The tuples in the window are automatically acked when the expire, i.e. when they fall out of the window after +`windowLength + slidingInterval`. Note that the configuration `topology.message.timeout.secs` should be sufficiently more +than `windowLength + slidingInterval` for time based windows; otherwise the tuples will timeout and get replayed and can result +in duplicate evaluations. For count based windows, the configuration should be adjusted such that `windowLength + slidingInterval` +tuples can be received within the timeout period. + +## Example topology +An example toplogy `SlidingWindowTopology` shows how to use the apis to compute a sliding window sum and a tumbling window +average. + diff --git a/docs/_config.yml b/docs/_config.yml index 14aaa5d1328..036d007fc3f 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -8,4 +8,11 @@ github_username: apache/storm # Build settings markdown: redcarpet redcarpet: - extensions: ["no_intra_emphasis", "fenced_code_blocks", "autolink", "tables", "with_toc_data"] \ No newline at end of file + extensions: ["no_intra_emphasis", "fenced_code_blocks", "autolink", "tables", "with_toc_data"] + +keep_files: [".git", ".svn"] +encoding: "utf-8" +exclude: + - README.md + +storm_release_only: true diff --git a/docs/_includes/footer.html b/docs/_includes/footer.html index bf766f4df87..3dde496e985 100644 --- a/docs/_includes/footer.html +++ b/docs/_includes/footer.html @@ -1,16 +1,55 @@ -
- - - - - - - - - \ No newline at end of file +
+
+
+
+ +
+
+ +
+ +
+ +
+
+
+
+
+

Copyright © 2015 Apache Software Foundation. All Rights Reserved. +
Apache Storm, Apache, the Apache feather logo, and the Apache Storm project logos are trademarks of The Apache Software Foundation. +
All other marks mentioned may be trademarks or registered trademarks of their respective owners.

+
+
+
+
+ + + diff --git a/docs/_includes/head.html b/docs/_includes/head.html index 781492c7f14..8f51c94343a 100644 --- a/docs/_includes/head.html +++ b/docs/_includes/head.html @@ -2,9 +2,9 @@ - - - + + + {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %} @@ -14,14 +14,17 @@ - - - - + + + + + + + + - - + - +
  • Home
  • +
  • Getting Help
  • +
  • Project Information
  • + +
  • Talks and Slideshows
  • + +
  • News
  • + + +
    +
    diff --git a/docs/_layouts/about.html b/docs/_layouts/about.html index 28b958b1427..7ca3e79363b 100644 --- a/docs/_layouts/about.html +++ b/docs/_layouts/about.html @@ -1,10 +1,7 @@ --- layout: default -title: About Storm +title: Project Information items: - - - - "/about/integrates.html" - - "Integrates" - - "/about/simple-api.html" - "Simple API" @@ -27,14 +24,20 @@ - "/about/free-and-open-source.html" - "Free and open source" --- +
    +
    +
    +
      + {% for post in page.items %} +
    • + {{ post[1] }} +
    • + {% endfor %} +
    +
    +
    + {{ content }} - -
    -{{ content }} -
    +
    +
    +
    \ No newline at end of file diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index de13299a4e5..80b404e9f6b 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -1,19 +1,18 @@ - {% include head.html %} - -
    {% include header.html %} - - - {{ content }} - - - {% include footer.html %} -
    - - +
    +

    {{ page.title }}

    +
    +
    + {{ content }} +
    +
    +
    +{% include footer.html %} + + diff --git a/docs/_layouts/documentation.html b/docs/_layouts/documentation.html index 6d175a30457..81cc09fa74a 100644 --- a/docs/_layouts/documentation.html +++ b/docs/_layouts/documentation.html @@ -2,15 +2,8 @@ layout: default --- - -
    -

    {{ page.title }}

    - -
    + -
    - {{ content }} -
    +{{ content }} - diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html index 74c1a118445..e230861d3db 100644 --- a/docs/_layouts/page.html +++ b/docs/_layouts/page.html @@ -1,14 +1,5 @@ --- layout: default --- -
    - -
    -

    {{ page.title }}

    -
    - -
    {{ content }} -
    -
    diff --git a/docs/_layouts/post.html b/docs/_layouts/post.html index 932ce2859a9..5080868a103 100644 --- a/docs/_layouts/post.html +++ b/docs/_layouts/post.html @@ -1,15 +1,61 @@ ---- -layout: default ---- -
    + + -
    -

    {{ page.title }}

    -

    Posted on {{ page.date | date: "%b %-d, %Y" }}{% if page.author %} by {{ page.author }}{% endif %}{% if page.meta %} • {{ page.meta }}{% endif %}

    -
    + {% include head.html %} -
    - {{ content }} -
    + + + {% include header.html %} +
    +
    +
    +
    +
    + +
    +
    +

    + {{ page.title }} +

    + +
    +
    +

    Posted on {{ page.date | date: "%b %-d, %Y" }}{% if page.author %} by {{ page.author }}{% endif %}{% if page.meta %} • {{ page.meta }}{% endif %}

    +
    + + +
    +
    +
    +
    + {{ content }} +
    +
    +
    +
    +
    +
    + {% include footer.html %} + + + -
    diff --git a/docs/_plugins/releases.rb b/docs/_plugins/releases.rb new file mode 100644 index 00000000000..f28ccd2db9e --- /dev/null +++ b/docs/_plugins/releases.rb @@ -0,0 +1,84 @@ +module Releases + class Generator < Jekyll::Generator + def dir_to_releasename(dir) + ret = nil + splitdir = dir.split("/").select{ |a| a != ""}; + if (splitdir[0] == 'releases') + ret = splitdir[1] + if (ret == 'current') + ret = File.readlink(splitdir.join("/")).split("/")[-1] + end + end + return ret + end + + def set_if_unset(hash, key, value) + hash[key] = hash[key] || value; + end + + def parse_version(version_string) + return version_string.split('.').map{|e| e.to_i} + end + + def release_from_pom() + text= `mvn -f ../pom.xml help:evaluate -Dexpression=project.version` + return text.split("\n").select{|a| !a.start_with?('[')}[0] + end + + def branch_from_git() + return `git rev-parse --abbrev-ref HEAD` + end + + def generate(site) + if site.config['storm_release_only'] + release_name = release_from_pom() + puts "release: #{release_name}" + git_branch = branch_from_git() + puts "branch: #{git_branch}" + for page in site.pages do + page.data['version'] = release_name; + page.data['git-tree-base'] = "http://github.com/apache/storm/tree/#{git_branch}" + page.data['git-blob-base'] = "http://github.com/apache/storm/blob/#{git_branch}" + end + return + end + + releases = Hash.new + if (site.data['releases']) + for rel_data in site.data['releases'] do + releases[rel_data['name']] = rel_data + end + end + + for page in site.pages do + release_name = dir_to_releasename(page.dir) + if (release_name != nil) + if !releases.has_key?(release_name) + releases[release_name] = {'name' => release_name}; + end + releases[release_name]['documented'] = true + end + end + + releases.each { |release_name, release_data| + set_if_unset(release_data, 'git-tag-or-branch', "v#{release_data['name']}") + set_if_unset(release_data, 'git-tree-base', "http://github.com/apache/storm/tree/#{release_data['git-tag-or-branch']}") + set_if_unset(release_data, 'git-blob-base', "http://github.com/apache/storm/blob/#{release_data['git-tag-or-branch']}") + set_if_unset(release_data, 'base-name', "apache-storm-#{release_data['name']}") + set_if_unset(release_data, 'has-download', !release_name.end_with?('-SNAPSHOT')) + } + + for page in site.pages do + release_name = dir_to_releasename(page.dir) + if (release_name != nil) + release_data = releases[release_name] + page.data['version'] = release_name; + page.data['git-tree-base'] = release_data['git-tree-base']; + page.data['git-blob-base'] = release_data['git-blob-base']; + end + end + site.data['releases'] = releases.values.sort{|x,y| parse_version(y['name']) <=> + parse_version(x['name'])}; + end + end +end diff --git a/docs/_posts/2012-08-02-storm080-released.md b/docs/_posts/2012-08-02-storm080-released.md deleted file mode 100644 index d5636e4f297..00000000000 --- a/docs/_posts/2012-08-02-storm080-released.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -layout: post -title: Storm 0.8.0 and Trident released -author: Nathan Marz ---- - -I'm happy to announce the release of Storm 0.8.0. This is a *huge* release. Thanks to everyone who tested out the dev release and helped find and fix issues. And thanks to everyone who contributed pull requests. There's one big new thing available in the release: Trident. You may have heard me hint about Trident before, and now it's finally public. - -Trident is a higher level abstraction for Storm. It allows you to seamlessly mix high throughput (millions of messages per second), stateful stream processing with low latency distributed querying. If you're familiar with high level batch processing tools like Pig or Cascading, the concepts of Trident will be very familiar - Trident has joins, aggregations, grouping, functions, and filters. In addition to these, Trident adds primitives for doing stateful, incremental processing on top of any database or persistence store. Trident has consistent, exactly-once semantics, so it is easy to reason about Trident topologies. Trident is bundled with Storm, and you can read documentation about it on [this page](https://github.com/apache/incubator-storm/wiki/Documentation) (start with "Trident tutorial"). - -Trident supersedes both LinearDRPCTopologyBuilder and transactional topologies, both of which will be deprecated soon and eventually removed from the codebase. - -Here are the other highlights of this release: - - -Executors ---------- - -Prior to Storm 0.8.0, a running topology consisted of some number of workers and some number of tasks that ran on those workers. In the old model, worker = process and task = thread. Storm 0.8.0 changes this model by introducing executors. In this model, a worker = process, an executor = thread, and one executor runs many tasks from the same spout/bolt. - -The reason for the change is that the old model complected the semantics of the topology with its physical execution. For example, if you had a bolt with 4 tasks doing a fields grouping on some stream, in order to maintain the semantics of the fields grouping (that the same value always goes to the same task id for that bolt), the number of tasks for that bolt needs to be fixed for the lifetime of the topology, and since task = thread, the number of threads for that bolt is fixed for the lifetime of the topology. In the new model, the number of threads for a bolt is disassociated from the number of tasks, meaning you can change the number of threads for a spout/bolt dynamically without affecting semantics. Similarly, if you're keeping large amounts of state in your bolts, and you want to increase the parallelism of the bolt without having to repartition the state, you can do this with the new executors. - -At the API level, the "parallelism_hint" now specifies the initial number of executors for that bolt. You can specify the number of tasks using the TOPOLOGY_TASKS component config. For example: - - -builder.setBolt(new MyBolt(), 3).setNumTasks(128).shuffleGrouping("spout"); - - -This sets the initial number of executors to 3 and the number of tasks to 128. If you don't specify the number of tasks for a component, it will be fixed to the initial number of executors for the lifetime of the topology. - -Finally, you can change the number of workers and/or number of executors for components using the "storm rebalance" command. The following command changes the number of workers for the "demo" topology to 3, the number of executors for the "myspout" component to 5, and the number of executors for the "mybolt" component to 1: - - -storm rebalance demo -n 3 -e myspout=5 -e mybolt=1 - - -Pluggable scheduler -------------------- - -You can now implement your own scheduler to replace the default scheduler to assign executors to workers. You configure the class to use using the "storm.scheduler" config in your storm.yaml, and your scheduler must implement [this interface](https://github.com/apache/incubator-storm/blob/master/src/jvm/backtype/storm/scheduler/IScheduler.java). - -Throughput improvements ------------------------ - -The internals of Storm have been rearchitected for extremely significant performance gains. I'm seeing throughput increases of anywhere from 5-10x of what it was before. I've benchmarked Storm at 1M tuples / sec / node on an internal Twitter cluster. - -The key changes made were: - -a) Replacing all the internal in-memory queuing with the LMAX Disruptor -b) Doing intelligent auto-batching of processing so that the consumers can keep up with the producers - -Here are the configs which affect how buffering/batching is done: - -topology.executor.receive.buffer.size -topology.executor.send.buffer.size -topology.receiver.buffer.size -topology.transfer.buffer.size - -These may require some tweaking to optimize your topologies, but most likely the default values will work fine for you out of the box. - -Decreased Zookeeper load / increased Storm UI performance ------------------------ -Storm sends significantly less traffic to Zookeeper now (on the order of 10x less). Since it also uses so many fewer znodes to store state, the UI is significantly faster as well. - -Abstractions for shared resources ------------------------ - -The TopologyContext has methods "getTaskData", "getExecutorData", and "getResource" for sharing resources at the task level, executor level, or worker level respectively. Currently you can't set any worker resources, but this is in development. Storm currently provides a shared ExecutorService worker resource (via "getSharedExecutor" method) that can be used for launching background tasks on a shared thread pool. - -Tick tuples ------------------------ - -It's common to require a bolt to "do something" at a fixed interval, like flush writes to a database. Many people have been using variants of a ClockSpout to send these ticks. The problem with a ClockSpout is that you can't internalize the need for ticks within your bolt, so if you forget to set up your bolt correctly within your topology it won't work correctly. 0.8.0 introduces a new "tick tuple" config that lets you specify the frequency at which you want to receive tick tuples via the "topology.tick.tuple.freq.secs" component- specific config, and then your bolt will receive a tuple from the __system component and __tick stream at that frequency. - -Improved Storm UI ------------------------ - -The Storm UI now has a button for showing/hiding the "system stats" (tuples sent on streams other than ones you've defined, like acker streams), making it easier to digest what your topology is doing. - -Here's the full changelog for Storm 0.8.0: - - * Added Trident, the new high-level abstraction for intermixing high throughput, stateful stream processing with low-latency distributed querying - * Added executor abstraction between workers and tasks. Workers = processes, executors = threads that run many tasks from the same spout or bolt. - * Pluggable scheduler (thanks xumingming) - * Eliminate explicit storage of task->component in Zookeeper - * Number of workers can be dynamically changed at runtime through rebalance command and -n switch - * Number of executors for a component can be dynamically changed at runtime through rebalance command and -e switch (multiple -e switches allowed) - * Use worker heartbeats instead of task heartbeats (thanks xumingming) - * UI performance for topologies with many executors/tasks much faster due to optimized usage of Zookeeper (10x improvement) - * Added button to show/hide system stats (e.g., acker component and stream stats) from the Storm UI (thanks xumingming) - * Stats are tracked on a per-executor basis instead of per-task basis - * Major optimization for unreliable spouts and unanchored tuples (will use far less CPU) - * Revamped internals of Storm to use LMAX disruptor for internal queuing. Dramatic reductions in contention and CPU usage. - * Numerous micro-optimizations all throughout the codebase to reduce CPU usage. - * Optimized internals of Storm to use much fewer threads - two fewer threads per spout and one fewer thread per acker. - * Removed error method from task hooks (to be re-added at a later time) - * Validate that subscriptions come from valid components and streams, and if it's a field grouping that the schema is correct (thanks xumingming) - * MemoryTransactionalSpout now works in cluster mode - * Only track errors on a component by component basis to reduce the amount stored in zookeeper (to speed up UI). A side effect of this change is the removal of the task page in the UI. - * Add TOPOLOGY-TICK-TUPLE-FREQ-SECS config to have Storm automatically send "tick" tuples to a bolt's execute method coming from the __system component and __tick stream at the configured frequency. Meant to be used as a component-specific configuration. - * Upgrade Kryo to v2.17 - * Tuple is now an interface and is much cleaner. The Clojure DSL helpers have been moved to TupleImpl - * Added shared worker resources. Storm provides a shared ExecutorService thread pool by default. The number of threads in the pool can be configured with topology.worker.shared.thread.pool.size - * Improve CustomStreamGrouping interface to make it more flexible by providing more information - * Enhanced INimbus interface to allow for forced schedulers and better integration with global scheduler - * Added assigned method to ISupervisor so it knows exactly what's running and not running - * Custom serializers can now have one of four constructors: (), (Kryo), (Class), or (Kryo, Class) - * Disallow ":", ".", and "\" from topology names - * Errors in multilang subprocesses that go to stderr will be captured and logged to the worker logs (thanks vinodc) - * Workers detect and warn for missing outbound connections from assignment, drop messages for which there's no outbound connection - * Zookeeper connection timeout is now configurable (via storm.zookeeper.connection.timeout config) - * Storm is now less aggressive about halting process when there are Zookeeper errors, preferring to wait until client calls return exceptions. - * Can configure Zookeeper authentication for Storm's Zookeeper clients via "storm.zookeeper.auth.scheme" and "storm.zookeeper.auth.payload" configs - * Supervisors only download code for topologies assigned to them - * Include task id information in task hooks (thanks velvia) - * Use execvp to spawn daemons (replaces the python launcher process) (thanks ept) - * Expanded INimbus/ISupervisor interfaces to provide more information (used in Storm/Mesos integration) - * Bug fix: Realize task ids when worker heartbeats to supervisor. Some users were hitting deserialization problems here in very rare cases (thanks herberteuler) - * Bug fix: Fix bug where a topology's status would get corrupted to true if nimbus is restarted while status is rebalancing - \ No newline at end of file diff --git a/docs/_posts/2012-09-06-storm081-released.md b/docs/_posts/2012-09-06-storm081-released.md deleted file mode 100644 index da24349a6d0..00000000000 --- a/docs/_posts/2012-09-06-storm081-released.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -layout: post -title: Storm 0.8.1 released -author: Nathan Marz ---- - -Storm 0.8.1 is now available on the downloads page and in Maven. This release contains many bug fixes as well as a few important new features. These include: - -Storm's unit testing facilities have been exposed via Java ------------------------ -This is an extremely powerful API that lets you do things like: - a) Easily bring up and tear down local clusters - b) Run a fixed set of tuples through a topology and see all the tuples emitted by all components - c) Feed some tuples through the topology, wait until they've been processed, and then run your assertions - d) Use time simulation and step through time via an API. This is useful for testing topologies that do things based on time advancing. You can see examples of the unit testing API [here](https://github.com/xumingming/storm-lib/blob/master/src/jvm/storm/TestingApiDemo.java). - -Spout wait strategies ---------------------- - -There's two situations in which a spout needs to wait. The first is when the max spout pending limit is reached. The second is when nothing is emitted from nextTuple. Previously, Storm would just have that spout sit in a busy loop in those cases. What Storm does in those situations is now pluggable, and the default is now for the spout to sleep for 1 ms. This will cause the spout to use dramatically less CPU when it hits those cases, and it also obviates the need for spouts to do any sleeping in their implementation to be "polite". The wait strategy can be configured with TOPOLOGY_SPOUT_WAIT_STRATEGY and can be configured on a spout by spout basis. The interface to implement for a wait strategy is backtype.storm.spout.ISpoutWaitStrategy - -The full changelog is below: - - * Exposed Storm's unit testing facilities via the backtype.storm.Testing class. Notable functions are Testing/withLocalCluster and Testing/completeTopology - * Implemented pluggable spout wait strategy that is invoked when a spout emits nothing from nextTuple or when a spout hits the MAX_SPOUT_PENDING limit - * Spouts now have a default wait strategy of a 1 millisecond sleep - * Changed log level of "Failed message" logging to DEBUG - * Deprecated LinearDRPCTopologyBuilder, TimeCacheMap, and transactional topologies - * During "storm jar", whether topology is already running or not is checked before submitting jar to save time (thanks jasonjckn) - * Added BaseMultiReducer class to Trident that provides empty implementations of prepare and cleanup - * Added Negate builtin operation to reverse a Filter - * Added topology.kryo.decorators config that allows functions to be plugged in to customize Kryo (thanks jasonjckn) - * Enable message timeouts when using LocalCluster - * Multilang subprocesses can set "need_task_ids" to false when emitting tuples to tell Storm not to send task ids back (performance optimization) (thanks barrywhart) - * Add contains method on Tuple (thanks okapies) - * Added ISchemableSpout interface - * Bug fix: When an item is consumed off an internal buffer, the entry on the buffer is nulled to allow GC to happen on that data - * Bug fix: Helper class for Trident MapStates now clear their read cache when a new commit happens, preventing updates from spilling over from a failed batch attempt to the next attempt - * Bug fix: Fix NonTransactionalMap to take in an IBackingMap for regular values rather than TransactionalValue (thanks sjoerdmulder) - * Bug fix: Fix NPE when no input fields given for regular Aggregator - * Bug fix: Fix IndexOutOfBoundsExceptions when a bolt for global aggregation had a parallelism greater than 1 (possible with splitting, stateQuerying, and multiReduce) - * Bug fix: Fix "fields size" error that would sometimes occur when splitting a stream with multiple eaches - * Bug fix: Fix bug where a committer spout (including opaque spouts) could cause Trident batches to fail - * Bug fix: Fix Trident bug where multiple groupings on same stream would cause tuples to be duplicated to all consumers - * Bug fix: Fixed error when repartitioning stream twice in a row without any operations in between - * Bug fix: Fix rare bug in supervisor where it would continuously fail to clean up workers because the worker was already partially cleaned up - * Bug fix: Fix emitDirect in storm.py diff --git a/docs/_posts/2013-01-11-storm082-released.md b/docs/_posts/2013-01-11-storm082-released.md deleted file mode 100644 index e657d0978bd..00000000000 --- a/docs/_posts/2013-01-11-storm082-released.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -layout: post -title: Storm 0.8.2 released -author: Nathan Marz ---- - -Storm 0.8.2 has been released and is available from [the downloads page](/downloads.html). This release contains a ton of improvements and fixes and is a highly recommended upgrade for everyone. - -Isolation Scheduler -------------------- - -The highlight of this release is the new "Isolation scheduler" that makes it easy and safe to share a cluster among many topologies. The isolation scheduler lets you specify which topologies should be "isolated", meaning that they run on a dedicated set of machines within the cluster where no other topologies will be running. These isolated topologies are given priority on the cluster, so resources will be allocated to isolated topologies if there's competition with non-isolated topologies, and resources will be taken away from non-isolated topologies if necessary to get resources for an isolated topology. Once all isolated topologies are allocated, the remaining machines on the cluster are shared among all non-isolated topologies. - -You configure the isolation scheduler in the Nimbus configuration. Set "storm.scheduler" to "backtype.storm.scheduler.IsolationScheduler". Then, use the "isolation.scheduler.machines" config to specify how many machines each topology should get. This config is a map from topology name to number of machines. For example: - - - -Any topologies submitted to the cluster not listed there will not be isolated. Note that there is no way for a user of Storm to affect their isolation settings – this is only allowed by the administrator of the cluster (this is very much intentional). - -The isolation scheduler solves the multi-tenancy problem – avoiding resource contention between topologies – by providing full isolation between topologies. The intention is that "productionized" topologies should be listed in the isolation config, and test or in-development topologies should not. The remaining machines on the cluster serve the dual role of failover for isolated topologies and for running the non-isolated topologies. - -Storm UI improvements -------------------- - -The Storm UI has also been made significantly more useful. There are new stats "#executed", "execute latency", and "capacity" tracked for all bolts. The "capacity" metric is very useful and tells you what % of the time in the last 10 minutes the bolt spent executing tuples. If this value is close to 1, then the bolt is "at capacity" and is a bottleneck in your topology. The solution to at-capacity bolts is to increase the parallelism of that bolt. - -Another useful improvement is the ability to kill, activate, deactivate, and rebalance topologies from the Storm UI. - - -Important bug fixes -------------------- - -There are also a few important bug fixes in this release. We fixed two bugs that could cause a topology to freeze up and stop processing. One of these bugs was extremely unlikely to hit, but the other one was a regression in 0.8.1 and there was a small chance of hitting it anytime a worker was restarted. - - -Changelog ---------- - - * Added backtype.storm.scheduler.IsolationScheduler. This lets you run topologies that are completely isolated at the machine level. Configure Nimbus to isolate certain topologies, and how many machines to give to each of those topologies, with the isolation.scheduler.machines config in Nimbus's storm.yaml. Topologies run on the cluster that are not listed there will share whatever remaining machines there are on the cluster after machines are allocated to the listed topologies. - * Storm UI now uses nimbus.host to find Nimbus rather than always using localhost (thanks Frostman) - * Added report-error! to Clojure DSL - * Automatically throttle errors sent to Zookeeper/Storm UI when too many are reported in a time interval (all errors are still logged) Configured with TOPOLOGY_MAX_ERROR_REPORT_PER_INTERVAL and TOPOLOGY_ERROR_THROTTLE_INTERVAL_SECS - * Kryo instance used for serialization can now be controlled via IKryoFactory interface and TOPOLOGY_KRYO_FACTORY config - * Add ability to plug in custom code into Nimbus to allow/disallow topologies to be submitted via NIMBUS_TOPOLOGY_VALIDATOR config - * Added TOPOLOGY_TRIDENT_BATCH_EMIT_INTERVAL_MILLIS config to control how often a batch can be emitted in a Trident topology. Defaults to 500 milliseconds. This is used to prevent too much load from being placed on Zookeeper in the case that batches are being processed super quickly. - * Log any topology submissions errors in nimbus.log - * Add static helpers in Config when using regular maps - * Make Trident much more memory efficient during failures by immediately removing state for failed attempts when a more recent attempt is seen - * Add ability to name portions of a Trident computation and have those names appear in the Storm UI - * Show Nimbus and topology configurations through Storm UI (thanks rnfein) - * Added ITupleCollection interface for TridentState's and TupleCollectionGet QueryFunction for getting the full contents of a state. MemoryMapState and LRUMemoryMapState implement this - * Can now submit a topology in inactive state. Storm will wait to call open/prepare on the spouts/bolts until it is first activated. - * Can now activate, deactive, rebalance, and kill topologies from the Storm UI (thanks Frostman) - * Can now use --config option to override which yaml file from ~/.storm to use for the config (thanks tjun) - * Redesigned the pluggable resource scheduler (INimbus, ISupervisor) interfaces to allow for much simpler integrations - * Added prepare method to IScheduler - * Added "throws Exception" to TestJob interface - * Added reportError to multilang protocol and updated Python and Ruby adapters to use it (thanks Lazyshot) - * Number tuples executed now tracked and shown in Storm UI - * Added ReportedFailedException which causes a batch to fail without killing worker and reports the error to the UI - * Execute latency now tracked and shown in Storm UI - * Adding testTuple methods for easily creating Tuple instances to Testing API (thanks xumingming) - * Trident now throws an error during construction of a topology when try to select fields that don't exist in a stream (thanks xumingming) - * Compute the capacity of a bolt based on execute latency and #executed over last 10 minutes and display in UI - * Storm UI displays exception instead of blank page when there's an error rendering the page (thanks Frostman) - * Added MultiScheme interface (thanks sritchie) - * Added MockTridentTuple for testing (thanks emblem) - * Add whitelist methods to Cluster to allow only a subset of hosts to be revealed as available slots - * Updated Trident Debug filter to take in an identifier to use when logging (thanks emblem) - * Number of DRPC server worker threads now customizable (thanks xiaokang) - * DRPC server now uses a bounded queue for requests to prevent being overloaded with requests (thanks xiaokang) - * Add __hash__ method to all generated Python Thrift objects so that Python code can read Nimbus stats which use Thrift objects as dict keys - * Bug fix: Fix for bug that could cause topology to hang when ZMQ blocks sending to a worker that got reassigned - * Bug fix: Fix deadlock bug due to variant of dining philosophers problem. Spouts now use an overflow buffer to prevent blocking and guarantee that it can consume the incoming queue of acks/fails. - * Bug fix: Fix race condition in supervisor that would lead to supervisor continuously crashing due to not finding "stormconf.ser" file for an already killed topology - * Bug fix: bin/storm script now displays a helpful error message when an invalid command is specified - * Bug fix: fixed NPE when emitting during emit method of Aggregator - * Bug fix: URLs with periods in them in Storm UI now route correctly - * Bug fix: Fix occasional cascading worker crashes due when a worker dies due to not removing connections from connection cache appropriately - - - diff --git a/docs/_posts/2013-12-08-storm090-released.md b/docs/_posts/2013-12-08-storm090-released.md deleted file mode 100644 index 8e1e9483bd1..00000000000 --- a/docs/_posts/2013-12-08-storm090-released.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -layout: post -title: Storm 0.9.0 Released -author: P. Taylor Goetz ---- - -We are pleased to announce that Storm 0.9.0 has been released and is available from [the downloads page](/downloads.html). This release represents an important milestone in the evolution of Storm. - -While a number of new features have been added, a key focus area for this release has been stability-related fixes. Though many users are successfully running work-in-progress builds for Storm 0.9.x in production, this release represents the most stable version to-date, and is highly recommended for everyone, especially users of 0.8.x versions. - - -Netty Transport ---------------- -The first hightlight of this release is the new [Netty](http://netty.io/index.html) Transport contributed by [Yahoo! Engineering](http://yahooeng.tumblr.com/). Storm's core network transport mechanism is now plugable, and Storm now comes with two implementations: The original 0MQ transport, and a new Netty-based implementation. - -In earlier versions, Storm relied solely on 0MQ for transport. Since 0MQ is a native library, it was highly platform-dependent and, at times, challenging to install properly. In addition, stability between versions varied widely between versions and only a relatively old 0MQ version (2.1.7) was certified to work with Storm. - -The Netty transport offers a pure Java alternative that eliminates Storm's dependency on native libraries. The Netty transport's performance is up to twice as fast as 0MQ, and it will open the door for authorization and authentication between worker processes. For an in-depth performance comparison of the 0MQ and Netty transports, see [this blog post](http://yahooeng.tumblr.com/post/64758709722/making-storm-fly-with-netty) by Storm contributor [Bobby Evans](https://github.com/revans2). - -To configure Storm to use the Netty transport simply add the following to your `storm.yaml` configuration and adjust the values to best suit your use-case: - -``` -storm.messaging.transport: "backtype.storm.messaging.netty.Context" -storm.messaging.netty.server_worker_threads: 1 -storm.messaging.netty.client_worker_threads: 1 -storm.messaging.netty.buffer_size: 5242880 -storm.messaging.netty.max_retries: 100 -storm.messaging.netty.max_wait_ms: 1000 -storm.messaging.netty.min_wait_ms: 100 -``` -You can also write your own transport implementation by implementing the [`backtype.storm.messaging.IContext`](https://github.com/apache/incubator-storm/blob/master/storm-core/src/jvm/backtype/storm/messaging/IContext.java) interface. - - -Log Viewer UI -------------- -Storm now includes a helpful new feature for debugging and monitoring topologies: The `logviewer` daemon. - -In earlier versions of Storm, viewing worker logs involved determining a worker's location (host/port), typically through Storm UI, then `ssh`ing to that host and `tail`ing the corresponding worker log file. With the new log viewer. You can now easily access a specific worker's log in a web browser by clicking on a worker's port number right from Storm UI. - -The `logviewer` daemon runs as a separate process on Storm supervisor nodes. To enable the `logviewer` run the following command (under supervision) on your cluster's supervisor nodes: - -``` -$ storm logviewer -``` - - -Improved Windows Support ------------------------- -In previous versions, running Storm on Microsoft Windows required installing third-party binaries (0MQ), modifying Storm's source, and adding Windows-specific scripts. With the addition of the platform-independent Netty transport, as well as numerous enhancements to make Storm more platform-independent, running Storm on Windows is easier than ever. - - -Security Improvements ---------------------- -Security, Authentication, and Authorization have been and will continue to be important focus areas for upcoming features. Storm 0.9.0 introduces an API for pluggable tuple serialization and a blowfish encryption based implementation for encrypting tuple data for sensitive use cases. - - -API Compatibility and Upgrading -------------------------------- -For most Storm topology developers, upgrading to 0.9.0 is simply a matter of updating the [dependency](https://clojars.org/storm). Storm's core API has changed very little since the 0.8.2 release. - -On the devops side, when upgrading to a new Storm release, it is safest to clear any existing state (Zookeeper, `storm.local.dir`), prior to upgrading. - -Logging Changes ---------------- -Another important change in 0.9.0 has to do with logging. Storm has largely switched over to the [slf4j API](http://www.slf4j.org) (backed by a [logback](http://logback.qos.ch) logger implementation). Some Storm dependencies rely on the log4j API, so Storm currently depends on [log4j-over-slf4j](http://www.slf4j.org/legacy.html#log4j-over-slf4j). - -These changes have implications for existing topologies and topology components that use the log4j API. - -In general, and when possible, Storm topologies and topology components should use the [slf4j API](http://www.slf4j.org) for logging. - - -Thanks ------- -Special thanks are due to all those who have contributed to Storm -- whether through direct code contributions, documentation, bug reports, or helping other users on the mailing lists. Your efforts are much appreciated. - - -Changelog ---------- - -* Update build configuration to force compatibility with Java 1.6 -* Fixed a netty client issue where sleep times for reconnection could be negative (thanks brndnmtthws) -* Fixed an issue that would cause storm-netty unit tests to fail -* Added configuration to limit ShellBolt internal _pendingWrites queue length (thanks xiaokang) -* Fixed a a netty client issue where sleep times for reconnection could be negative (thanks brndnmtthws) -* Fixed a display issue with system stats in Storm UI (thanks d2r) -* Nimbus now does worker heartbeat timeout checks as soon as heartbeats are updated (thanks d2r) -* The logviewer now determines log file location by examining the logback configuration (thanks strongh) -* Allow tick tuples to work with the system bolt (thanks xumingming) -* Add default configuration values for the netty transport and the ability to configure the number of worker threads (thanks revans2) -* Added timeout to unit tests to prevent a situation where tests would hang indefinitely (thanks d2r) -* Fixed an issue in the system bolt where local mode would not be detected accurately (thanks miofthena) -* Fixed `storm jar` command to work properly when STORM_JAR_JVM_OPTS is not specified (thanks roadkill001) -* All logging now done with slf4j -* Replaced log4j logging system with logback -* Logs are now limited to 1GB per worker (configurable via logging configuration file) -* Build upgraded to leiningen 2.0 -* Revamped Trident spout interfaces to support more dynamic spouts, such as a spout who reads from a changing set of brokers -* How tuples are serialized is now pluggable (thanks anfeng) -* Added blowfish encryption based tuple serialization (thanks anfeng) -* Have storm fall back to installed storm.yaml (thanks revans2) -* Improve error message when Storm detects bundled storm.yaml to show the URL's for offending resources (thanks revans2) -* Nimbus throws NotAliveException instead of FileNotFoundException from various query methods when topology is no longer alive (thanks revans2) -* Escape HTML and Javascript appropriately in Storm UI (thanks d2r) -* Storm's Zookeeper client now uses bounded exponential backoff strategy on failures -* Automatically drain and log error stream of multilang subprocesses -* Append component name to thread name of running executors so that logs are easier to read -* Messaging system used for passing messages between workers is now pluggable (thanks anfeng) -* Netty implementation of messaging (thanks anfeng) -* Include topology id, worker port, and worker id in properties for worker processes, useful for logging (thanks d2r) -* Tick tuples can now be scheduled using floating point seconds (thanks tscurtu) -* Added log viewer daemon and links from UI to logviewers (thanks xiaokang) -* DRPC server childopts now configurable (thanks strongh) -* Default number of ackers to number of workers, instead of just one (thanks lyogavin) -* Validate that Storm configs are of proper types/format/structure (thanks d2r) -* FixedBatchSpout will now replay batches appropriately on batch failure (thanks ptgoetz) -* Can set JAR_JVM_OPTS env variable to add jvm options when calling 'storm jar' (thanks srmelody) -* Throw error if batch id for transaction is behind the batch id in the opaque value (thanks mrflip) -* Sort topologies by name in UI (thanks jaked) -* Added LoggingMetricsConsumer to log all metrics to a file, by default not enabled (thanks mrflip) -* Add prepare(Map conf) method to TopologyValidator (thanks ankitoshniwal) -* Bug fix: Supervisor provides full path to workers to logging config rather than relative path (thanks revans2) -* Bug fix: Call ReducerAggregator#init properly when used within persistentAggregate (thanks lorcan) -* Bug fix: Set component-specific configs correctly for Trident spouts - - - - diff --git a/docs/_posts/2014-04-10-storm-logo-contest.md b/docs/_posts/2014-04-10-storm-logo-contest.md deleted file mode 100644 index 59f2a5d6c5f..00000000000 --- a/docs/_posts/2014-04-10-storm-logo-contest.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -layout: post -title: Apache Storm Logo Contest -author: P. Taylor Goetz ---- - -Apache Storm is a platform for distributed, real-time computation that has been undergoing incubation at Apache since September 18, 2013. You can contribute by helping establish the Storm brand by submitting a proposal for the Apache Storm logo. - -Your proposal will be published on the [Storm project website](http://storm.incubator.apache.org) next to the other proposals. On **Wednesday, April 30, 2014** the community will choose the new logo through a vote on the [Apache Storm Mailing List](http://mail-archives.apache.org/mod_mbox/incubator-storm-dev/). If more than 10 proposals are received, voting will take place in two rounds, with the first round selecting 10 proposals to move to round two. - -The winning artist will be credited on the Apache Storm website as having designed the logo, and may include a link to her/his website or blog. Prizes for the winner(s) are currently TBD. - -##GUIDELINES - -The logo should meet the following requirements: - - * Include 2 versions of the logo: a rectangular layout (full brand) and a square layout (icon, stand-alone logo) - * Be scalable up or down (hi-res raster, or vector) - * Translate to a 2-color representation (black/white, 2 greys, etc.) - * Able to be inverted if running on a dark background - * Comply with the [Apache branding requirements](http://www.apache.org/foundation/marks/) - -Optional, but nice to have: - - * Icons representing storm components (suitable for use in diagrams, presentations, etc.): - * Spouts - * Bolts - * Streams - * Trident - * Functions/Filters - -Note: The winning design as accepted by popular vote will be treated as a proposal that can be modified and iterated upon to reach its final form. - -There is no limit on the number of entries per individual. - -##INSPIRATION - -While designing the logo, keep in mind what describes Storm: - - * Distributed Computation - * High Performance - * Fault Tolerance - * Community Driven - - -##DEADLINE - -The contest will be open until **Wednesday, April 30, 2014**. - -##HOW TO ENTER - -Send the [Apache Storm Mailing List](mailto:dev@storm.incubator.apache.org) links to: - - * A PNG version of the logo (and it's scalable variations) - * A ZIP file with all assets - * (Optional) A link to a blog post, etc. describing the proposal - -Alternatively, contributers can mail the above to the [contest coordinator](mailto:ptgoetz@apache.org) to have them forwarded to the mailing list. - -Contributors may also publicise their proposal by tweeting links to a proposal with the hashtag #stormlogocontest - - -##LICENSE - -The authors of the selected Apache Storm logo(s) will be required to donate them to the Apache Storm project and complete and [Apache Individual Contributor License Agreement(ICLA)](http://www.apache.org/licenses/icla.txt) - diff --git a/docs/_posts/2014-04-17-logo-pforrest.md b/docs/_posts/2014-04-17-logo-pforrest.md deleted file mode 100644 index cd479388bd0..00000000000 --- a/docs/_posts/2014-04-17-logo-pforrest.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: post -title: Logo Entry No. 1 - Patricia Forrest -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/pforrest/storm1.png) - -![Storm Brand](/images/logocontest/pforrest/storm_logo_composite.png) - diff --git a/docs/_posts/2014-04-17-logo-squinones.md b/docs/_posts/2014-04-17-logo-squinones.md deleted file mode 100644 index 31c00a3c2b4..00000000000 --- a/docs/_posts/2014-04-17-logo-squinones.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -layout: post -title: Logo Entry No. 2 - Samuel Quiñones -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/squinones/storm_logo1.png) - -![Storm Brand](/images/logocontest/squinones/storm_logo.png) \ No newline at end of file diff --git a/docs/_posts/2014-04-19-logo-ssuleman.md b/docs/_posts/2014-04-19-logo-ssuleman.md deleted file mode 100644 index e0d49b5f49a..00000000000 --- a/docs/_posts/2014-04-19-logo-ssuleman.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -layout: post -title: Logo Entry No. 3- Shaan Shiv Suleman -author: P. Taylor Goetz ---- - - -![Storm Brand](/images/logocontest/ssuleman/storm_logo.png) \ No newline at end of file diff --git a/docs/_posts/2014-04-21-logo-rmarshall.md b/docs/_posts/2014-04-21-logo-rmarshall.md deleted file mode 100644 index 98edbb9efc0..00000000000 --- a/docs/_posts/2014-04-21-logo-rmarshall.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -layout: post -title: Logo Entry No. 4 - Richard Brownlie-Marshall -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/rmarshall/StormLogo_Square.png) - -![Storm Brand](/images/logocontest/rmarshall/StormLogo_Horizontal.png) - -![Storm Brand](/images/logocontest/rmarshall/StormLogo_Horizontal_NoColour.png) - diff --git a/docs/_posts/2014-04-22-logo-zsayari.md b/docs/_posts/2014-04-22-logo-zsayari.md deleted file mode 100644 index 859b32ede7c..00000000000 --- a/docs/_posts/2014-04-22-logo-zsayari.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -layout: post -title: Logo Entry No. 5 - Ziba Sayari -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/zsayari/storm_logo.png) - - diff --git a/docs/_posts/2014-04-23-logo-abartos.md b/docs/_posts/2014-04-23-logo-abartos.md deleted file mode 100644 index eee2e8226de..00000000000 --- a/docs/_posts/2014-04-23-logo-abartos.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -layout: post -title: Logo Entry No. 6 - Alec Bartos -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/abartos/storm_logo.png) - -![Storm Brand](/images/logocontest/abartos/storm_logo2.png) - -![Storm Brand](/images/logocontest/abartos/storm_logo3.png) - -![Storm Brand](/images/logocontest/abartos/stationery_mockup.jpg) - - diff --git a/docs/_posts/2014-04-27-logo-cboustead.md b/docs/_posts/2014-04-27-logo-cboustead.md deleted file mode 100644 index b2b053b4959..00000000000 --- a/docs/_posts/2014-04-27-logo-cboustead.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -layout: post -title: Logo Entry No. 7 - Calum Boustead -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/cboustead/storm_logo1.png) - -![Storm Brand](/images/logocontest/cboustead/storm_logo.png) - - - diff --git a/docs/_posts/2014-04-27-logo-sasili.md b/docs/_posts/2014-04-27-logo-sasili.md deleted file mode 100644 index bcd8c4f3e7a..00000000000 --- a/docs/_posts/2014-04-27-logo-sasili.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: post -title: Logo Entry No. 8 - Stefano Asili -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/sasili/storm_logo.png) - - - diff --git a/docs/_posts/2014-04-29-logo-jlee1.md b/docs/_posts/2014-04-29-logo-jlee1.md deleted file mode 100644 index 1f99b1a26c0..00000000000 --- a/docs/_posts/2014-04-29-logo-jlee1.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: post -title: Logo Entry No. 9 - Jennifer Lee -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/jlee1/storm_logo.jpg) - - - diff --git a/docs/_posts/2014-04-29-logo-jlee2.md b/docs/_posts/2014-04-29-logo-jlee2.md deleted file mode 100644 index 7149b3ceed6..00000000000 --- a/docs/_posts/2014-04-29-logo-jlee2.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: post -title: Logo Entry No. 10 - Jennifer Lee -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/jlee3/storm_logo.jpg) - - - diff --git a/docs/_posts/2014-04-29-logo-jlee3.md b/docs/_posts/2014-04-29-logo-jlee3.md deleted file mode 100644 index c034fca1589..00000000000 --- a/docs/_posts/2014-04-29-logo-jlee3.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: post -title: Logo Entry No. 11 - Jennifer Lee -author: P. Taylor Goetz ---- - -![Storm Brand](/images/logocontest/jlee2/storm_logo.jpg) - - - diff --git a/docs/_posts/2014-05-27-round1-results.md b/docs/_posts/2014-05-27-round1-results.md deleted file mode 100644 index 015e167902c..00000000000 --- a/docs/_posts/2014-05-27-round1-results.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -layout: post -title: Logo Contest - Round 1 Results -author: P. Taylor Goetz ---- - -Round one of the Apache Storm logo contest is now complete and was a great success. We received votes from 7 PPMC members as well as 46 votes from the greater Storm community. - -We would like to extend a very special thanks to all those who took the time and effort to create and submit a logo proposal. We would also like to thank everyone who voted. - -## Results ## -The Storm PPMC and community votes were closely aligned, with the community vote resolving a PPMC tie for the 3rd finalist as shown in the result table below. - -The three finalists entering the final round are: - - * [No. 6 - Alec Bartos](/2014/04/23/logo-abartos.html) - * [No. 9 - Jennifer Lee](/2014/04/29/logo-jlee1.html) - * [No. 10 - Jennifer Lee](/2014/04/29/logo-jlee2.html) - -Congratulations Alec and Jennifer! - -Stay tuned for the final vote, which will be announced shortly. - ------- - -| Entry | PPMC | Community | -|:-----------------------------|-----:|----------:| -|1 - Patricia Forrest | 2 | 23 | -|2 - Samuel Quiñones | 3 | 6 | -|3- Shaan Shiv Suleman | 0 | 7 | -|4 - Richard Brownlie-Marshall | 0 | 6 | -|5 - Ziba Sayari | 0 | 9 | -|6 - Alec Bartos | 3 | 32 | -|7 - Calum Boustead | 0 | 0 | -|8 - Stefano Asili | 0 | 2 | -|9 - Jennifer Lee | 9 | 63 | -|10 - Jennifer Lee | 18 | 64 | -|11 - Jennifer Lee | 0 | 18 | \ No newline at end of file diff --git a/docs/_posts/2014-06-17-contest-results.md b/docs/_posts/2014-06-17-contest-results.md deleted file mode 100644 index 4b6b7de756f..00000000000 --- a/docs/_posts/2014-06-17-contest-results.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: post -title: Storm Logo Contest Results -author: P. Taylor Goetz ---- - -The Apache Storm logo contest is now complete and was a great success. We received votes from 7 PPMC members as well as 55 votes from the greater Storm community. Thank you to everyone who participated by voting. - -## The Winner ## -Congratulations to Jennifer Lee, whose [3rd entry](/2014/04/29/logo-jlee2.html) received the most points from both the PPMC as well as the Storm community. Final vote tallies are listed below. - -![Storm Logo](/images/logocontest/storm_logo_winner.png) - -The Apache Storm project management team will now begin the process of finalizing the logo and making it official. - -Congratulations Jennifer, and thanks again to everyone who participated! - ------- - -| Entry | PPMC |    Community | -|:-----------------------------|-----:|----------:| -|[6 - Alec Bartos](/2014/04/23/logo-abartos.html) | 2 | 41 | -|[9 - Jennifer Lee](/2014/04/29/logo-jlee1.html) | 7 | 111 | -|[10 - Jennifer Lee](/2014/04/29/logo-jlee2.html) | 26 | 123 | diff --git a/docs/_posts/2014-06-25-storm092-released.md b/docs/_posts/2014-06-25-storm092-released.md deleted file mode 100644 index 7f8c438445b..00000000000 --- a/docs/_posts/2014-06-25-storm092-released.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -layout: post -title: Storm 0.9.2 released -author: P. Taylor Goetz ---- - -We are pleased to announce that Storm 0.9.2-incubating has been released and is available from [the downloads page](/downloads.html). This release includes many important fixes and improvements. - -Netty Transport Improvements ----------------------------- -Storm's Netty-based transport has been overhauled to significantly improve performance through better utilization of thread, CPU, and network resources, particularly in cases where message sizes are small. Storm contributor Sean Zhong ([@clockfly](https://github.com/clockfly)) deserves a great deal of credit not only for discovering, analyzing, documenting and fixing the root cause, but also for persevering through an extended review process and promptly addressing all concerns. - -Those interested in the technical details and evolution of this patch can find out more in the [JIRA ticket for STORM-297](https://issues.apache.org/jira/browse/STORM-297). - -Sean also discovered and fixed an [elusive bug](https://issues.apache.org/jira/browse/STORM-342) in Storm's usage of the Disruptor queue that could lead to out-of-order or lost messages. - -Many thanks to Sean for contributing these important fixes. - -Storm UI Improvements ---------------------- -This release also includes a number of improvements to the Storm UI service. Contributor Sriharsha Chintalapani([@harshach](https://github.com/harshach)) added a REST API to the Storm UI service to expose metrics and operations in JSON format, and updated the UI to use that API. - -The new REST API will make it considerably easier for other services to consume availabe cluster and topology metrics for monitoring and visualization applications. Kyle Nusbaum ([@knusbaum](https://github.com/knusbaum)) has already leveraged the REST API to create a topology visualization tool now included in Storm UI and illustrated in the screenshot below. - -  - -![Storm UI Topology Visualization](/images/ui_topology_viz.png) - -  - -In the visualization, spout components are represented as blue, while bolts are colored between green and red depending on their associated capacity metric. The width of the lines between the components represent the flow of tuples relative to the other visible streams. - -Kafka Spout ------------ -This is the first Storm release to include official support for consuming data from Kafka 0.8.x. In the past, development of Kafka spouts for Storm had become somewhat fragmented and finding an implementation that worked with certain versions of Storm and Kafka proved burdensome for some developers. This is no longer the case, as the `storm-kafka` module is now part of the Storm project and associated artifacts are released to official channels (Maven Central) along with Storm's other components. - -Thanks are due to GitHub user [@wurstmeister]() for picking up Nathan Marz' original Kafka 0.7.x implementation, updating it to work with Kafka 0.8.x, and contributing that work back to the Storm community. - -The `storm-kafka` module can be found in the `/external/` directory of the source tree and binary distributions. The `external` area has been set up to contain projects that while not required by Storm, are often used in conjunction with Storm to integrate with some other technology. Such projects also come with a maintenance committment from at least one Storm committer to ensure compatibility with Storm's main codebase as it evolves. - -The `storm-kafka` dependency is available now from Maven Central at the following coordinates: - - - groupId: org.apache.storm - artifactId: storm-kafka - version: 0.9.2-incubating - -Users, and Scala developers in particular, should note that the Kafka dependency is listed as `provided`. This allows users to choose a specific Scala version as described in the [project README](https://github.com/apache/incubator-storm/tree/v0.9.2-incubating/external/storm-kafka). - - -Storm Starter and Examples --------------------------- - -Similar to the `external` section of the codebase, we have also added an `examples` directory and pulled in the `storm-starter` project to ensure it will be maintained in lock-step with Storm's main codebase. - -Thank you to Storm committer Michael G. Noll for his continued work in maintaining and improving the `storm-starter` project. - - -Plugable Serialization for Multilang ------------------------------------- -In previous versions of Storm, serialization of data to and from multilang components was limited to JSON, imposing somewhat of performance penalty. Thanks to a contribution from John Gilmore ([@jsgilmore](https://github.com/jsgilmore)) the serialization mechanism is now plugable and enables the use of more performant serialization frameworks like protocol buffers in addition to JSON. - - -Thanks ------- -Special thanks are due to all those who have contributed to Storm -- whether through direct code contributions, documentation, bug reports, or helping other users on the mailing lists. Your efforts are much appreciated. - - -Changelog ---------- - - * STORM-352: [storm-kafka] PartitionManager does not save offsets to ZooKeeper - * STORM-66: send taskid on initial handshake - * STORM-342: Contention in Disruptor Queue which may cause out of order or lost messages - * STORM-338: Move towards idiomatic Clojure style - * STORM-335: add drpc test for removing timed out requests from queue - * STORM-69: Storm UI Visualizations for Topologies - * STORM-297: Performance scaling with CPU - * STORM-244: DRPC timeout can return null instead of throwing an exception - * STORM-63: remove timeout drpc request from its function's request queue - * STORM-313: Remove log-level-page from logviewer - * STORM-205: Add REST API To Storm UI - * STORM-326: tasks send duplicate metrics - * STORM-331: Update the Kafka dependency of storm-kafka to 0.8.1.1 - * STORM-308: Add support for config_value to {supervisor,nimbus,ui,drpc,logviewer} childopts - * STORM-309: storm-starter Readme: windows documentation update - * STORM-318: update storm-kafka to use apache curator-2.4.0 - * STORM-303: storm-kafka reliability improvements - * STORM-233: Removed inline heartbeat to nimbus to avoid workers being killed when under heavy ZK load - * STORM-267: fix package name of LoggingMetricsConsumer in storm.yaml.example - * STORM-265: upgrade to clojure 1.5.1 - * STORM-232: ship JNI dependencies with the topology jar - * STORM-295: Add storm configuration to define JAVA_HOME - * STORM-138: Pluggable serialization for multilang - * STORM-264: Removes references to the deprecated topology.optimize - * STORM-245: implement Stream.localOrShuffle() for trident - * STORM-317: Add SECURITY.md to release binaries - * STORM-310: Change Twitter authentication - * STORM-305: Create developer documentation - * STORM-280: storm unit tests are failing on windows - * STORM-298: Logback file does not include full path for metrics appender fileNamePattern - * STORM-316: added validation to registermetrics to have timebucketSizeInSecs >= 1 - * STORM-315: Added progress bar when submitting topology - * STORM-214: Windows: storm.cmd does not properly handle multiple -c arguments - * STORM-306: Add security documentation - * STORM-302: Fix Indentation for pom.xml in storm-dist - * STORM-235: Registering a null metric should blow up early - * STORM-113: making thrift usage thread safe for local cluster - * STORM-223: use safe parsing for reading YAML - * STORM-238: LICENSE and NOTICE files are duplicated in storm-core jar - * STORM-276: Add support for logviewer in storm.cmd. - * STORM-286: Use URLEncoder#encode with the encoding specified. - * STORM-296: Storm kafka unit tests are failing on windows - * STORM-291: upgrade http-client to 4.3.3 - * STORM-252: Upgrade curator to latest version - * STORM-294: Commas not escaped in command line - * STORM-287: Fix the positioning of documentation strings in clojure code - * STORM-290: Fix a log binding conflict caused by curator dependencies - * STORM-289: Fix Trident DRPC memory leak - * STORM-173: Treat command line "-c" option number config values as such - * STORM-194: Support list of strings in *.worker.childopts, handle spaces - * STORM-288: Fixes version spelling in pom.xml - * STORM-208: Add storm-kafka as an external module - * STORM-285: Fix storm-core shade plugin config - * STORM-12: reduce thread usage of netty transport - * STORM-281: fix and issue with config parsing that could lead to leaking file descriptors - * STORM-196: When JVM_OPTS are set, storm jar fails to detect storm.jar from environment - * STORM-260: Fix a potential race condition with simulated time in Storm's unit tests - * STORM-258: Update commons-io version to 2.4 - * STORM-270: don't package .clj files in release jars. - * STORM-273: Error while running storm topologies on Windows using "storm jar" - * STROM-247: Replace links to github resources in storm script - * STORM-263: Update Kryo version to 2.21+ - * STORM-187: Fix Netty error "java.lang.IllegalArgumentException: timeout value is negative" - * STORM-186: fix float secs to millis long convertion - * STORM-70: Upgrade to ZK-3.4.5 and curator-1.3.3 - * STORM-146: Unit test regression when storm is compiled with 3.4.5 zookeeper diff --git a/docs/_posts/2014-10-20-storm093-release-candidate.md b/docs/_posts/2014-10-20-storm093-release-candidate.md deleted file mode 100644 index 627028322af..00000000000 --- a/docs/_posts/2014-10-20-storm093-release-candidate.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -layout: post -title: Storm 0.9.3 release candidate 1 available -author: Michael G. Noll ---- - -We are pleased to announce that Apache Storm version 0.9.3 has reached the release candidate stage. The latest release is 0.9.3-rc1. We are moving closer to the GA release, so this is a good time to give this new 0.9.3 version a try! - -Storm 0.9.3 is a maintenance release with bug fixes and better stability as summarized in the [0.9.3 changelog](https://github.com/apache/storm/blob/master/CHANGELOG.md). On top of that version 0.9.3 continues to improve the integration with [Apache Kafka](http://kafka.apache.org/) as well as the [documentation of the core spout and Trident spout for Kafka](https://github.com/apache/storm/blob/master/external/storm-kafka/README.md), which will provide our users with the best Kafka connectivity for Storm to date. - -We heartily encourage you to [test the 0.9.3 release candidate](http://storm.apache.org/downloads.html) and provide your feedback regarding any issues via [our mailing lists](http://storm.apache.org/community.html), which is an easy and valuable way to contribute back to the Storm community and to help us moving to an official 0.9.3 release. You can find the [0.9.3 release candidate in our Downloads section](http://storm.apache.org/downloads.html). diff --git a/docs/_posts/2014-11-25-storm093-released.md b/docs/_posts/2014-11-25-storm093-released.md deleted file mode 100644 index df3da75e1ff..00000000000 --- a/docs/_posts/2014-11-25-storm093-released.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -layout: post -title: Storm 0.9.3 released -author: P. Taylor Goetz ---- - -We are pleased to announce that Apache Storm 0.9.3 has been released and is available from [the downloads page](/downloads.html). This release includes 100 new fixes and improvements from 62 individual contributors. - -Improved Kafka Integration ----------------------------- - -Apache Storm has supported [Apache Kafka](http://kafka.apache.org/) as a streaming data source since version 0.9.2-incubating. Version 0.9.3 brings a number of improvements to the Kafka integration including the ability to write data to one or more Kafka clusters and topics. - -The ability to both read from and write to Kafka further unlocks potential of the already powerful combination of Storm and Kafka. Storm users can now use Kafka as a source of and destination for streaming data. This allows for inter-topology communication, topology chaining, combining spout/bolt-based topologies with Trident-based data flows, and integration with any external system that supports data ingest from Kafka. - -More information about Storm's Kafka integration can be found in the [storm-kafka project documentation](https://github.com/apache/storm/blob/v0.9.3/external/storm-kafka/README.md). - - -HDFS Integration ----------------- - -Many stream processing use cases involve storing data in HDFS for further analysis and offline (batch) processing. Apache Storm’s HDFS integration consists of several bolt and Trident state implementations that allow topology developers to easily write data to HDFS from any Storm topology. - -More information about Storm's HDFS integration can be found in the [storm-hdfs project documentation](https://github.com/apache/storm/tree/v0.9.3/external/storm-hdfs). - - -HBase Integration ------------------ - -Apache Storm’s HBase integration includes a number of components that allow Storm topologies to both write to and query HBase in real-time. Many organizations use Apache HBase as part of their big data strategy for batch, interactive, and real-time workflows. Storm’s HBase integration allows users to leverage data assets in HBase as streaming queries, as well as using HBase as a destination for streaming computation results. - -More information about Storm's HBase integration can be found in the [storm-hbase project documentation](https://github.com/apache/storm/tree/v0.9.3/external/storm-hbase). - -Reduced Dependency Conflicts ----------------------------- -In previous Storm releases, it was not uncommon for users' topology dependencies to conflict with the libraries used by Storm. In Storm 0.9.3 several dependency packages that were common sources of conflicts have been package-relocated (shaded) to avoid this situation. Developers are free to use the Storm-packaged versions, or supply their own version. - -The following table lists the dependency package relocations: - -| Dependency | Original Package | Storm Package | -|:---|:---|:---| -| Apache Thrift | `org.apache.thrift` | `org.apache.thrift7` | -| Netty | `org.jboss.netty` | `org.apache.storm.netty` | -| Google Guava | `com.google.common` | `org.apache.storm.guava` | -| | `com.google.thirdparty` | `org.apache.storm.guava.thirdparty` | -| Apache HTTPClient | `org.apache.http` | `org.apache.storm.http` | -| Apache ZooKeeper | `org.apache.zookeeper` | `org.apache.storm.zookeeper` | -| Apache Curator | `org.apache.curator` | `org.apache.storm.curator` | - -Multi-Lang Improvements ------------------------ -Apache Storm 0.9.3 includes a new [Node.js](http://nodejs.org) multi-lang implementation that allows developers to write spouts and bolts in JavaScript. - -In addition to the Node.js implementation, the multi-lang protocol has been substantially improved in terms of robustness and error handling capabilities. As a result, **the multi-lang API has changed in a non-backward-compatible way**. Users with existing multi-lang topologies should consult the Python, Ruby, and JavaScript multi-lang examples to determine the impact prior to upgrading. - - -Thanks ------- -Special thanks are due to all those who have contributed to Storm -- whether through direct code contributions, documentation, bug reports, or helping other users on the mailing lists. Your efforts are much appreciated. - - -Full Changelog ---------- - - * STORM-558: change "swap!" to "reset!" to fix assignment-versions in supervisor - * STORM-555: Storm json response should set charset to UTF-8 - * STORM-513: check heartbeat from multilang subprocess - * STORM-549: "topology.enable.message.timeouts" does nothing - * STORM-546: Local hostname configuration ignored by executor - * STORM-492: Test timeout should be configurable - * STORM-540: Change default time format in logs to ISO8601 in order to include timezone - * STORM-511: Storm-Kafka spout keeps sending fetch requests with invalid offset - * STORM-538: Guava com.google.thirdparty.publicsuffix is not shaded - * STORM-497: don't modify the netty server taskToQueueId mapping while the someone could be reading it. - * STORM-537: A worker reconnects infinitely to another dead worker (Sergey Tryuber) - * STORM-519: add tuple as an input param to HBaseValueMapper - * STORM-488: Exit with 254 error code if storm CLI is run with unknown command - * STORM-506: Do not count bolt acks & fails in total stats - * STORM-490: fix build under Windows - * STORM-439: Replace purl.js qith jquery URL plugin - * STORM-499: Document and clean up shaded dependncy resolution with maven - * STORM-210: Add storm-hbase module - * STORM-507: Topology visualization should not block ui - * STORM-504: Class used by `repl` command is deprecated. - * STORM-330: Implement storm exponential backoff stategy for netty client and curator - * STORM-461: exit-process! does not always exit the process, but throws an exception - * STORM-341: fix assignment sorting - * STORM-476: external/storm-kafka: avoid NPE on null message payloads - * STORM-424: fix error message related to kafka offsets - * STORM-454: correct documentation in STORM-UI-REST-API.md - * STORM-474: Reformat UI HTML code - * STORM-447: shade/relocate packages of dependencies that are common causes of dependency conflicts - * STORM-279: cluster.xml doesn't take in STORM_LOG_DIR values. - * STORM-380: Kafka spout: throw RuntimeException if a leader cannot be found for a partition - * STORM-435: Improve storm-kafka documentation - * STORM-405: Add kafka trident state so messages can be sent to kafka topic - * STORM-211: Add module for HDFS integration - * STORM-337: Expose managed spout ids publicly - * STORM-320: Support STORM_CONF_DIR environment variable to support - * STORM-360: Add node details for Error Topology and Component pages - * STORM-54: Per-Topology Classpath and Environment for Workers - * STORM-355: excluding outdated netty transitively included via curator - * STORM-183: Replacing RunTime.halt() with RunTime.exit() - * STORM-213: Decouple In-Process ZooKeeper from LocalCluster. - * STORM-365: Add support for Python 3 to storm command. - * STORM-332: Enable Kryo serialization in storm-kafka - * STORM-370: Add check for empty table before sorting dom in UI - * STORM-359: add logviewer paging and download - * STORM-372: Typo in storm_env.ini - * STORM-266: Adding shell process pid and name in the log message - * STORM-367: Storm UI REST api documentation. - * STORM-200: Proposal for Multilang's Metrics feature - * STORM-351: multilang python process fall into endless loop - * STORM-375: Smarter downloading of assignments by supervisors and workers - * STORM-328: More restrictive Config checks, strict range check within Utils.getInt() - * STORM-381: Replace broken jquery.tablesorter.min.js to latest - * STORM-312: add storm monitor tools to monitor throughtput interactively - * STORM-354: Testing: allow users to pass TEST-TIMEOUT-MS as param for complete-topology - * STORM-254: one Spout/Bolt can register metric twice with same name in different timeBucket - * STORM-403: heartbeats-to-nimbus in supervisor-test failed due to uninten... - * STORM-402: FileNotFoundException when using storm with apache tika - * STORM-364: The exception time display as default timezone. - * STORM-420: Missing quotes in storm-starter python code - * STORM-399: Kafka Spout defaulting to latest offset when current offset is older then 100k - * STORM-421: Memoize local hostname lookup in executor - * STORM-414: support logging level to multilang protocol spout and bolt - * STORM-321: Added a tool to see the current status of STORM JIRA and github pulls. - * STORM-415: validate-launched-once in supervisor-test can not handle multiple topologies - * STORM-155: Storm rebalancing code causes multiple topologies assigned to a single port - * STORM-419: Updated test so sort ordering is very explicit. - * STORM-406: Fix for reconnect logic in netty client. - * STORM-366: Add color span to most recent error and fix ui templates. - * STORM-369: topology summary page displays wrong order. - * STORM-239: Allow supervisor to operate in paths with spaces in them - * STORM-87: fail fast on ShellBolt exception - * STORM-417: Storm UI lost table sort style when tablesorter was updated - * STORM-396: Replace NullPointerException with IllegalArgumentExeption - * STORM-451: Latest storm does not build due to a pom.xml error in storm-hdfs pom.xml - * STORM-453: migrated to curator 2.5.0 - * STORM-458: sample spout uses incorrect name when connecting bolt - * STORM-455: Report error-level messages from ShellBolt children - * STORM-443: multilang log's loglevel protocol can cause hang - * STORM-449: Updated ShellBolt to not exit when shutting down. - * STORM-464: Simulated time advanced after test cluster exits causes intermittent test failures - * STORM-463: added static version of metrics helpers for Config - * STORM-376: Add compression to serialization - * STORM-437: Enforce utf-8 when multilang reads from stdin - * STORM-361: Add JSON-P support to Storm UI API - * STORM-373: Provide Additional String substitutions for *.worker.childopts - * STORM-274: Add support for command remoteconfvalue in storm.cmd - * STORM-132: sort supervisor by free slot in desending order - * STORM-472: Improve error message for non-completeable testing spouts - * STORM-401: handle InterruptedIOException properly. - * STORM-461: exit-process! does not always exit the process, but throws an exception instead - * STORM-475: Storm UI pages do not use UTF-8 - * STORM-336: Logback version should be upgraded - * STORM-386: nodejs multilang protocol implementation and examples - * STORM-500: Add Spinner when UI is loading stats from nimbus - * STORM-501: Missing StormSubmitter API - * STORM-493: Workers inherit storm.conf.file/storm.options properties of their supervisor - * STORM-498: make ZK connection timeout configurable in Kafka spout - * STORM-428: extracted ITuple interface - * STORM-508: Update DEVELOPER.md now that Storm has graduated from Incubator - * STORM-514: Update storm-starter README now that Storm has graduated from Incubator diff --git a/docs/_posts/2015-03-25-storm094-released.md b/docs/_posts/2015-03-25-storm094-released.md deleted file mode 100644 index 0119e3887e9..00000000000 --- a/docs/_posts/2015-03-25-storm094-released.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: post -title: Storm 0.9.4 released -author: P. Taylor Goetz ---- - -The Apache Storm community is pleased to announce that version 0.9.4 has been released and is available from [the downloads page](/downloads.html). - -This is a maintenance release that includes a number of important bug fixes that improve Storm's stability and fault tolerance. We encourage users of previous versions to upgrade to this latest release. - - -Thanks ------- -Special thanks are due to all those who have contributed to Apache Storm -- whether through direct code contributions, documentation, bug reports, or helping other users on the mailing lists. Your efforts are much appreciated. - - -Full Changelog ---------- - - * STORM-559: ZkHosts in README should use 2181 as port. - * STORM-682: supervisor should handle worker state corruption gracefully. - * STORM-693: when kafka bolt fails to write tuple, it should report error instead of silently acking. - * STORM-329: fix cascading Storm failure by improving reconnection strategy and buffering messages - * STORM-130: Supervisor getting killed due to java.io.FileNotFoundException: File '../stormconf.ser' does not exist. \ No newline at end of file diff --git a/docs/_posts/2015-06-04-storm095-released.md b/docs/_posts/2015-06-04-storm095-released.md deleted file mode 100644 index 1d018c03aa2..00000000000 --- a/docs/_posts/2015-06-04-storm095-released.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -layout: post -title: Storm 0.9.5 released -author: P. Taylor Goetz ---- - -The Apache Storm community is pleased to announce that version 0.9.5 has been released and is available from [the downloads page](/downloads.html). - -This is a maintenance release that includes a number of important bug fixes that improve Storm's stability and fault tolerance. We encourage users of previous versions to upgrade to this latest release. - - -Thanks ------- -Special thanks are due to all those who have contributed to Apache Storm -- whether through direct code contributions, documentation, bug reports, or helping other users on the mailing lists. Your efforts are much appreciated. - - -Full Changelog ---------- - - * STORM-790: Log "task is null" instead of letting worker die when task is null in transfer-fn - * STORM-796: Add support for "error" command in ShellSpout - * STORM-745: fix storm.cmd to evaluate 'shift' correctly with 'storm jar' - * STORM-130: Supervisor getting killed due to java.io.FileNotFoundException: File '../stormconf.ser' does not exist. diff --git a/docs/about.md b/docs/about.md deleted file mode 100644 index 4bd6d5efb53..00000000000 --- a/docs/about.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -layout: page -title: About -permalink: /about/ ---- - -Storm is a free and open source distributed realtime computation system. Storm makes it easy to reliably process unbounded streams of data, doing for realtime processing what Hadoop did for batch processing. Storm is simple, can be used with any programming language, and is a lot of fun to use! diff --git a/docs/about/deployment.md b/docs/about/deployment.md deleted file mode 100644 index 88ec55ff802..00000000000 --- a/docs/about/deployment.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -layout: about ---- - -Storm clusters are easy to deploy, requiring a minimum of setup and configuration to get up and running. Storm's out of the box configurations are suitable for production. Read more about how to deploy a Storm cluster [here](/documentation/Setting-up-a-Storm-cluster.html). - -If you're on EC2, the [storm-deploy](https://github.com/nathanmarz/storm-deploy) project can provision, configure, and install a Storm cluster from scratch at just the click of a button. - -Additionally, Storm is easy to operate once deployed. Storm has been designed to be [extremely robust](/about/fault-tolerant.html) – the cluster will just keep on running, month after month. diff --git a/docs/about/fault-tolerant.md b/docs/about/fault-tolerant.md deleted file mode 100644 index 057b372090f..00000000000 --- a/docs/about/fault-tolerant.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -layout: about ---- - -Storm is fault-tolerant: when workers die, Storm will automatically restart them. If a node dies, the worker will be restarted on another node. - -The Storm daemons, Nimbus and the Supervisors, are designed to be stateless and fail-fast. So if they die, they will restart like nothing happened. This means you can *kill -9* the Storm daemons without affecting the health of the cluster or your topologies. - -Read more about Storm's fault-tolerance [on the manual](/documentation/Fault-tolerance.html). \ No newline at end of file diff --git a/docs/about/free-and-open-source.md b/docs/about/free-and-open-source.md deleted file mode 100644 index 3a942d56952..00000000000 --- a/docs/about/free-and-open-source.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -layout: about ---- - -Apache Storm is a free and open source project licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) - - -Storm has a large and growing ecosystem of libraries and tools to use in conjunction with Storm including everything from: - -1. *Spouts*: These spouts integrate with queueing systems such as JMS, Kafka, Redis pub/sub, and more. -2. *storm-state*: storm-state makes it easy to manage large amounts of in-memory state in your computations in a reliable by using a distributed filesystem for persistence -3. *Database integrations*: There are helper bolts for integrating with various databases, such as MongoDB, RDBMS's, Cassandra, and more. -4. Other miscellaneous utilities - -The [Storm documentation](/documentation/Home.html) has links to notable Storm-related projects hosted outside of Apache. \ No newline at end of file diff --git a/docs/about/guarantees-data-processing.md b/docs/about/guarantees-data-processing.md deleted file mode 100644 index 3d6a0f36631..00000000000 --- a/docs/about/guarantees-data-processing.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: about ---- - -Storm guarantees every tuple will be fully processed. One of Storm's core mechanisms is the ability to track the lineage of a tuple as it makes its way through the topology in an extremely efficient way. Read more about how this works [here](/documentation/Guaranteeing-message-processing.html). - -Storm's basic abstractions provide an at-least-once processing guarantee, the same guarantee you get when using a queueing system. Messages are only replayed when there are failures. - -Using [Trident](/documentation/Trident-tutorial.html), a higher level abstraction over Storm's basic abstractions, you can achieve exactly-once processing semantics. - diff --git a/docs/about/integrates.md b/docs/about/integrates.md deleted file mode 100644 index 8071bdb1518..00000000000 --- a/docs/about/integrates.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -layout: about ---- - -Storm integrates with any queueing system and any database system. Storm's [spout](/apidocs/backtype/storm/spout/ISpout.html) abstraction makes it easy to integrate a new queuing system. Example queue integrations include: - -1. [Kestrel](https://github.com/nathanmarz/storm-kestrel) -2. [RabbitMQ / AMQP](https://github.com/Xorlev/storm-amqp-spout) -3. [Kafka](https://github.com/apache/storm/tree/master/external/storm-kafka) -4. [JMS](https://github.com/ptgoetz/storm-jms) -5. [Amazon Kinesis](https://github.com/awslabs/kinesis-storm-spout) - -Likewise, integrating Storm with database systems is easy. Simply open a connection to your database and read/write like you normally would. Storm will handle the parallelization, partitioning, and retrying on failures when necessary. \ No newline at end of file diff --git a/docs/about/multi-language.md b/docs/about/multi-language.md deleted file mode 100644 index 31ff7556657..00000000000 --- a/docs/about/multi-language.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -layout: about ---- - -Storm was designed from the ground up to be usable with any programming language. At the core of Storm is a [Thrift](http://thrift.apache.org/) [definition](https://github.com/apache/storm/blob/master/storm-core/src/storm.thrift) for defining and submitting topologies. Since Thrift can be used in any language, topologies can be defined and submitted from any language. - -Similarly, spouts and bolts can be defined in any language. Non-JVM spouts and bolts communicate to Storm over a [JSON-based protocol](/documentation/Multilang-protocol.html) over stdin/stdout. Adapters that implement this protocol exist for [Ruby](https://github.com/apache/storm/blob/master/storm-core/src/multilang/rb/storm.rb), [Python](https://github.com/apache/storm/blob/master/storm-core/src/multilang/py/storm.py), [Javascript](https://github.com/apache/storm/blob/master/storm-core/src/multilang/js/storm.js), [Perl](https://github.com/dan-blanchard/io-storm). - -*storm-starter* has an [example topology](https://github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/storm/starter/WordCountTopology.java) that implements one of the bolts in Python. diff --git a/docs/about/scalable.md b/docs/about/scalable.md deleted file mode 100644 index b487e528b6f..00000000000 --- a/docs/about/scalable.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: about ---- - -Storm topologies are inherently parallel and run across a cluster of machines. Different parts of the topology can be scaled individually by tweaking their parallelism. The "rebalance" command of the "storm" command line client can adjust the parallelism of running topologies on the fly. - -Storm's inherent parallelism means it can process very high throughputs of messages with very low latency. Storm was benchmarked at processing **one million 100 byte messages per second per node** on hardware with the following specs: - - * **Processor:** 2x Intel E5645@2.4Ghz - * **Memory:** 24 GB diff --git a/docs/about/simple-api.md b/docs/about/simple-api.md deleted file mode 100644 index 7fc8c10d429..00000000000 --- a/docs/about/simple-api.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -layout: about ---- - -Storm has a simple and easy to use API. When programming on Storm, you manipulate and transform streams of tuples, and a tuple is a named list of values. Tuples can contain objects of any type; if you want to use a type Storm doesn't know about it's [very easy](/documentation/Serialization.html) to register a serializer for that type. - -There are just three abstractions in Storm: spouts, bolts, and topologies. A **spout** is a source of streams in a computation. Typically a spout reads from a queueing broker such as Kestrel, RabbitMQ, or Kafka, but a spout can also generate its own stream or read from somewhere like the Twitter streaming API. Spout implementations already exist for most queueing systems. - -A **bolt** processes any number of input streams and produces any number of new output streams. Most of the logic of a computation goes into bolts, such as functions, filters, streaming joins, streaming aggregations, talking to databases, and so on. - -A **topology** is a network of spouts and bolts, with each edge in the network representing a bolt subscribing to the output stream of some other spout or bolt. A topology is an arbitrarily complex multi-stage stream computation. Topologies run indefinitely when deployed. - -Storm has a "local mode" where a Storm cluster is simulated in-process. This is useful for development and testing. The "storm" command line client is used when ready to submit a topology for execution on an actual cluster. - -The [storm-starter](https://github.com/apache/storm/tree/master/examples/storm-starter) project contains example topologies for learning the basics of Storm. Learn more about how to use Storm by reading the [tutorial](/documentation/Tutorial.html) and the [documentation](/documentation/Home.html). \ No newline at end of file diff --git a/docs/assets/css/bootstrap.css b/docs/assets/css/bootstrap.css index c6f3d210685..680e7687862 100644 --- a/docs/assets/css/bootstrap.css +++ b/docs/assets/css/bootstrap.css @@ -1,10 +1,9 @@ /*! - * Bootstrap v3.3.1 (http://getbootstrap.com) - * Copyright 2011-2014 Twitter, Inc. + * Bootstrap v3.3.5 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ - -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ html { font-family: sans-serif; -webkit-text-size-adjust: 100%; @@ -239,9 +238,6 @@ th { h3 { page-break-after: avoid; } - select { - background: #fff !important; - } .navbar { display: none; } @@ -268,7 +264,7 @@ th { font-family: 'Glyphicons Halflings'; src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); } .glyphicon { position: relative; @@ -883,6 +879,192 @@ th { .glyphicon-tree-deciduous:before { content: "\e200"; } +.glyphicon-cd:before { + content: "\e201"; +} +.glyphicon-save-file:before { + content: "\e202"; +} +.glyphicon-open-file:before { + content: "\e203"; +} +.glyphicon-level-up:before { + content: "\e204"; +} +.glyphicon-copy:before { + content: "\e205"; +} +.glyphicon-paste:before { + content: "\e206"; +} +.glyphicon-alert:before { + content: "\e209"; +} +.glyphicon-equalizer:before { + content: "\e210"; +} +.glyphicon-king:before { + content: "\e211"; +} +.glyphicon-queen:before { + content: "\e212"; +} +.glyphicon-pawn:before { + content: "\e213"; +} +.glyphicon-bishop:before { + content: "\e214"; +} +.glyphicon-knight:before { + content: "\e215"; +} +.glyphicon-baby-formula:before { + content: "\e216"; +} +.glyphicon-tent:before { + content: "\26fa"; +} +.glyphicon-blackboard:before { + content: "\e218"; +} +.glyphicon-bed:before { + content: "\e219"; +} +.glyphicon-apple:before { + content: "\f8ff"; +} +.glyphicon-erase:before { + content: "\e221"; +} +.glyphicon-hourglass:before { + content: "\231b"; +} +.glyphicon-lamp:before { + content: "\e223"; +} +.glyphicon-duplicate:before { + content: "\e224"; +} +.glyphicon-piggy-bank:before { + content: "\e225"; +} +.glyphicon-scissors:before { + content: "\e226"; +} +.glyphicon-bitcoin:before { + content: "\e227"; +} +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} +.glyphicon-yen:before { + content: "\00a5"; +} +.glyphicon-jpy:before { + content: "\00a5"; +} +.glyphicon-ruble:before { + content: "\20bd"; +} +.glyphicon-rub:before { + content: "\20bd"; +} +.glyphicon-scale:before { + content: "\e230"; +} +.glyphicon-ice-lolly:before { + content: "\e231"; +} +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; +} +.glyphicon-education:before { + content: "\e233"; +} +.glyphicon-option-horizontal:before { + content: "\e234"; +} +.glyphicon-option-vertical:before { + content: "\e235"; +} +.glyphicon-menu-hamburger:before { + content: "\e236"; +} +.glyphicon-modal-window:before { + content: "\e237"; +} +.glyphicon-oil:before { + content: "\e238"; +} +.glyphicon-grain:before { + content: "\e239"; +} +.glyphicon-sunglasses:before { + content: "\e240"; +} +.glyphicon-text-size:before { + content: "\e241"; +} +.glyphicon-text-color:before { + content: "\e242"; +} +.glyphicon-text-background:before { + content: "\e243"; +} +.glyphicon-object-align-top:before { + content: "\e244"; +} +.glyphicon-object-align-bottom:before { + content: "\e245"; +} +.glyphicon-object-align-horizontal:before { + content: "\e246"; +} +.glyphicon-object-align-left:before { + content: "\e247"; +} +.glyphicon-object-align-vertical:before { + content: "\e248"; +} +.glyphicon-object-align-right:before { + content: "\e249"; +} +.glyphicon-triangle-right:before { + content: "\e250"; +} +.glyphicon-triangle-left:before { + content: "\e251"; +} +.glyphicon-triangle-bottom:before { + content: "\e252"; +} +.glyphicon-triangle-top:before { + content: "\e253"; +} +.glyphicon-console:before { + content: "\e254"; +} +.glyphicon-superscript:before { + content: "\e255"; +} +.glyphicon-subscript:before { + content: "\e256"; +} +.glyphicon-menu-left:before { + content: "\e257"; +} +.glyphicon-menu-right:before { + content: "\e258"; +} +.glyphicon-menu-down:before { + content: "\e259"; +} +.glyphicon-menu-up:before { + content: "\e260"; +} * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -987,6 +1169,9 @@ hr { overflow: visible; clip: auto; } +[role="button"] { + cursor: pointer; +} h1, h2, h3, @@ -1155,62 +1340,72 @@ mark, .text-primary { color: #337ab7; } -a.text-primary:hover { +a.text-primary:hover, +a.text-primary:focus { color: #286090; } .text-success { color: #3c763d; } -a.text-success:hover { +a.text-success:hover, +a.text-success:focus { color: #2b542c; } .text-info { color: #31708f; } -a.text-info:hover { +a.text-info:hover, +a.text-info:focus { color: #245269; } .text-warning { color: #8a6d3b; } -a.text-warning:hover { +a.text-warning:hover, +a.text-warning:focus { color: #66512c; } .text-danger { color: #a94442; } -a.text-danger:hover { +a.text-danger:hover, +a.text-danger:focus { color: #843534; } .bg-primary { color: #fff; background-color: #337ab7; } -a.bg-primary:hover { +a.bg-primary:hover, +a.bg-primary:focus { background-color: #286090; } .bg-success { background-color: #dff0d8; } -a.bg-success:hover { +a.bg-success:hover, +a.bg-success:focus { background-color: #c1e2b3; } .bg-info { background-color: #d9edf7; } -a.bg-info:hover { +a.bg-info:hover, +a.bg-info:focus { background-color: #afd9ee; } .bg-warning { background-color: #fcf8e3; } -a.bg-warning:hover { +a.bg-warning:hover, +a.bg-warning:focus { background-color: #f7ecb5; } .bg-danger { background-color: #f2dede; } -a.bg-danger:hover { +a.bg-danger:hover, +a.bg-danger:focus { background-color: #e4b9b9; } .page-header { @@ -2123,7 +2318,7 @@ th { .table-bordered > thead > tr > td { border-bottom-width: 2px; } -.table-striped > tbody > tr:nth-child(odd) { +.table-striped > tbody > tr:nth-of-type(odd) { background-color: #f9f9f9; } .table-hover > tbody > tr:hover { @@ -2390,10 +2585,13 @@ output { .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { - cursor: not-allowed; background-color: #eee; opacity: 1; } +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} textarea.form-control { height: auto; } @@ -2401,22 +2599,30 @@ input[type="search"] { -webkit-appearance: none; } @media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"], - input[type="time"], - input[type="datetime-local"], - input[type="month"] { + input[type="date"].form-control, + input[type="time"].form-control, + input[type="datetime-local"].form-control, + input[type="month"].form-control { line-height: 34px; } input[type="date"].input-sm, input[type="time"].input-sm, input[type="datetime-local"].input-sm, - input[type="month"].input-sm { + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { line-height: 30px; } input[type="date"].input-lg, input[type="time"].input-lg, input[type="datetime-local"].input-lg, - input[type="month"].input-lg { + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { line-height: 46px; } } @@ -2452,6 +2658,7 @@ input[type="search"] { } .radio-inline, .checkbox-inline { + position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; @@ -2485,6 +2692,7 @@ fieldset[disabled] .checkbox label { cursor: not-allowed; } .form-control-static { + min-height: 34px; padding-top: 7px; padding-bottom: 7px; margin-bottom: 0; @@ -2494,44 +2702,80 @@ fieldset[disabled] .checkbox label { padding-right: 0; padding-left: 0; } -.input-sm, -.form-group-sm .form-control { +.input-sm { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } -select.input-sm, -select.form-group-sm .form-control { +select.input-sm { height: 30px; line-height: 30px; } textarea.input-sm, -textarea.form-group-sm .form-control, -select[multiple].input-sm, -select[multiple].form-group-sm .form-control { +select[multiple].input-sm { height: auto; } -.input-lg, -.form-group-lg .form-control { +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.form-group-sm select.form-control { + height: 30px; + line-height: 30px; +} +.form-group-sm textarea.form-control, +.form-group-sm select[multiple].form-control { + height: auto; +} +.form-group-sm .form-control-static { + height: 30px; + min-height: 32px; + padding: 6px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-lg { height: 46px; padding: 10px 16px; font-size: 18px; - line-height: 1.33; + line-height: 1.3333333; border-radius: 6px; } -select.input-lg, -select.form-group-lg .form-control { +select.input-lg { height: 46px; line-height: 46px; } textarea.input-lg, -textarea.form-group-lg .form-control, -select[multiple].input-lg, -select[multiple].form-group-lg .form-control { +select[multiple].input-lg { height: auto; } +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.form-group-lg select.form-control { + height: 46px; + line-height: 46px; +} +.form-group-lg textarea.form-control, +.form-group-lg select[multiple].form-control { + height: auto; +} +.form-group-lg .form-control-static { + height: 46px; + min-height: 38px; + padding: 11px 16px; + font-size: 18px; + line-height: 1.3333333; +} .has-feedback { position: relative; } @@ -2550,12 +2794,16 @@ select[multiple].form-group-lg .form-control { text-align: center; pointer-events: none; } -.input-lg + .form-control-feedback { +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { width: 46px; height: 46px; line-height: 46px; } -.input-sm + .form-control-feedback { +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { width: 30px; height: 30px; line-height: 30px; @@ -2740,12 +2988,14 @@ select[multiple].form-group-lg .form-control { } @media (min-width: 768px) { .form-horizontal .form-group-lg .control-label { - padding-top: 14.3px; + padding-top: 14.333333px; + font-size: 18px; } } @media (min-width: 768px) { .form-horizontal .form-group-sm .control-label { padding-top: 6px; + font-size: 12px; } } .btn { @@ -2795,21 +3045,32 @@ select[multiple].form-group-lg .form-control { .btn.disabled, .btn[disabled], fieldset[disabled] .btn { - pointer-events: none; cursor: not-allowed; filter: alpha(opacity=65); -webkit-box-shadow: none; box-shadow: none; opacity: .65; } +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} .btn-default { color: #333; background-color: #fff; border-color: #ccc; } -.btn-default:hover, .btn-default:focus, -.btn-default.focus, +.btn-default.focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { @@ -2817,6 +3078,19 @@ fieldset[disabled] .btn { background-color: #e6e6e6; border-color: #adadad; } +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { @@ -2852,9 +3126,17 @@ fieldset[disabled] .btn-default.active { background-color: #337ab7; border-color: #2e6da4; } -.btn-primary:hover, .btn-primary:focus, -.btn-primary.focus, +.btn-primary.focus { + color: #fff; + background-color: #286090; + border-color: #122b40; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { @@ -2862,6 +3144,19 @@ fieldset[disabled] .btn-default.active { background-color: #286090; border-color: #204d74; } +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { @@ -2897,9 +3192,17 @@ fieldset[disabled] .btn-primary.active { background-color: #5cb85c; border-color: #4cae4c; } -.btn-success:hover, .btn-success:focus, -.btn-success.focus, +.btn-success.focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { @@ -2907,6 +3210,19 @@ fieldset[disabled] .btn-primary.active { background-color: #449d44; border-color: #398439; } +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { @@ -2942,9 +3258,17 @@ fieldset[disabled] .btn-success.active { background-color: #5bc0de; border-color: #46b8da; } -.btn-info:hover, .btn-info:focus, -.btn-info.focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #1b6d85; +} +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { @@ -2952,6 +3276,19 @@ fieldset[disabled] .btn-success.active { background-color: #31b0d5; border-color: #269abc; } +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { @@ -2987,9 +3324,17 @@ fieldset[disabled] .btn-info.active { background-color: #f0ad4e; border-color: #eea236; } -.btn-warning:hover, .btn-warning:focus, -.btn-warning.focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #985f0d; +} +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { @@ -2997,6 +3342,19 @@ fieldset[disabled] .btn-info.active { background-color: #ec971f; border-color: #d58512; } +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { @@ -3032,9 +3390,17 @@ fieldset[disabled] .btn-warning.active { background-color: #d9534f; border-color: #d43f3a; } -.btn-danger:hover, .btn-danger:focus, -.btn-danger.focus, +.btn-danger.focus { + color: #fff; + background-color: #c9302c; + border-color: #761c19; +} +.btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { @@ -3042,6 +3408,19 @@ fieldset[disabled] .btn-warning.active { background-color: #c9302c; border-color: #ac2925; } +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { @@ -3109,7 +3488,7 @@ fieldset[disabled] .btn-link:focus { .btn-group-lg > .btn { padding: 10px 16px; font-size: 18px; - line-height: 1.33; + line-height: 1.3333333; border-radius: 6px; } .btn-sm, @@ -3149,11 +3528,9 @@ input[type="button"].btn-block { } .collapse { display: none; - visibility: hidden; } .collapse.in { display: block; - visibility: visible; } tr.collapse.in { display: table-row; @@ -3181,10 +3558,12 @@ tbody.collapse.in { height: 0; margin-left: 2px; vertical-align: middle; - border-top: 4px solid; + border-top: 4px dashed; + border-top: 4px solid \9; border-right: 4px solid transparent; border-left: 4px solid transparent; } +.dropup, .dropdown { position: relative; } @@ -3297,13 +3676,14 @@ tbody.collapse.in { .navbar-fixed-bottom .dropdown .caret { content: ""; border-top: 0; - border-bottom: 4px solid; + border-bottom: 4px dashed; + border-bottom: 4px solid \9; } .dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu { top: auto; bottom: 100%; - margin-bottom: 1px; + margin-bottom: 2px; } @media (min-width: 768px) { .navbar-right .dropdown-menu { @@ -3345,6 +3725,7 @@ tbody.collapse.in { .btn-toolbar { margin-left: -5px; } +.btn-toolbar .btn, .btn-toolbar .btn-group, .btn-toolbar .input-group { float: left; @@ -3375,12 +3756,12 @@ tbody.collapse.in { .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } -.btn-group > .btn-group:first-child > .btn:last-child, -.btn-group > .btn-group:first-child > .dropdown-toggle { +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { border-top-right-radius: 0; border-bottom-right-radius: 0; } -.btn-group > .btn-group:last-child > .btn:first-child { +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -3506,7 +3887,7 @@ tbody.collapse.in { height: 46px; padding: 10px 16px; font-size: 18px; - line-height: 1.33; + line-height: 1.3333333; border-radius: 6px; } select.input-group-lg > .form-control, @@ -3635,6 +4016,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group { + z-index: 2; margin-left: -1px; } .nav { @@ -3820,11 +4202,9 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .tab-content > .tab-pane { display: none; - visibility: hidden; } .tab-content > .active { display: block; - visibility: visible; } .nav-tabs .dropdown-menu { margin-top: -1px; @@ -3871,7 +4251,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { height: auto !important; padding-bottom: 0; overflow: visible !important; - visibility: visible !important; } .navbar-collapse.in { overflow-y: visible; @@ -4120,6 +4499,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { border-top-right-radius: 0; } .navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 0; @@ -4412,6 +4792,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { + z-index: 3; color: #23527c; background-color: #eee; border-color: #ddd; @@ -4443,6 +4824,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination-lg > li > span { padding: 10px 16px; font-size: 18px; + line-height: 1.3333333; } .pagination-lg > li:first-child > a, .pagination-lg > li:first-child > span { @@ -4458,6 +4840,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination-sm > li > span { padding: 5px 10px; font-size: 12px; + line-height: 1.5; } .pagination-sm > li:first-child > a, .pagination-sm > li:first-child > span { @@ -4584,7 +4967,7 @@ a.label:focus { color: #fff; text-align: center; white-space: nowrap; - vertical-align: baseline; + vertical-align: middle; background-color: #777; border-radius: 10px; } @@ -4595,7 +4978,8 @@ a.label:focus { position: relative; top: -1px; } -.btn-xs .badge { +.btn-xs .badge, +.btn-group-xs > .btn .badge { top: 0; padding: 1px 5px; } @@ -4620,7 +5004,8 @@ a.badge:focus { margin-left: 3px; } .jumbotron { - padding: 30px 15px; + padding-top: 30px; + padding-bottom: 30px; margin-bottom: 30px; color: inherit; background-color: #eee; @@ -4646,7 +5031,8 @@ a.badge:focus { } @media screen and (min-width: 768px) { .jumbotron { - padding: 48px 0; + padding-top: 48px; + padding-bottom: 48px; } .container .jumbotron, .container-fluid .jumbotron { @@ -4859,6 +5245,20 @@ a.thumbnail.active { .media:first-child { margin-top: 0; } +.media, +.media-body { + overflow: hidden; + zoom: 1; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-object.img-thumbnail { + max-width: none; +} .media-right, .media > .pull-right { padding-left: 10px; @@ -4908,18 +5308,26 @@ a.thumbnail.active { border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } -a.list-group-item { +a.list-group-item, +button.list-group-item { color: #555; } -a.list-group-item .list-group-item-heading { +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { color: #333; } a.list-group-item:hover, -a.list-group-item:focus { +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { color: #555; text-decoration: none; background-color: #f5f5f5; } +button.list-group-item { + width: 100%; + text-align: left; +} .list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus { @@ -4965,20 +5373,27 @@ a.list-group-item:focus { color: #3c763d; background-color: #dff0d8; } -a.list-group-item-success { +a.list-group-item-success, +button.list-group-item-success { color: #3c763d; } -a.list-group-item-success .list-group-item-heading { +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { color: inherit; } a.list-group-item-success:hover, -a.list-group-item-success:focus { +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { color: #3c763d; background-color: #d0e9c6; } a.list-group-item-success.active, +button.list-group-item-success.active, a.list-group-item-success.active:hover, -a.list-group-item-success.active:focus { +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { color: #fff; background-color: #3c763d; border-color: #3c763d; @@ -4987,20 +5402,27 @@ a.list-group-item-success.active:focus { color: #31708f; background-color: #d9edf7; } -a.list-group-item-info { +a.list-group-item-info, +button.list-group-item-info { color: #31708f; } -a.list-group-item-info .list-group-item-heading { +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { color: inherit; } a.list-group-item-info:hover, -a.list-group-item-info:focus { +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { color: #31708f; background-color: #c4e3f3; } a.list-group-item-info.active, +button.list-group-item-info.active, a.list-group-item-info.active:hover, -a.list-group-item-info.active:focus { +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { color: #fff; background-color: #31708f; border-color: #31708f; @@ -5009,20 +5431,27 @@ a.list-group-item-info.active:focus { color: #8a6d3b; background-color: #fcf8e3; } -a.list-group-item-warning { +a.list-group-item-warning, +button.list-group-item-warning { color: #8a6d3b; } -a.list-group-item-warning .list-group-item-heading { +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { color: inherit; } a.list-group-item-warning:hover, -a.list-group-item-warning:focus { +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { color: #8a6d3b; background-color: #faf2cc; } a.list-group-item-warning.active, +button.list-group-item-warning.active, a.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus { +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { color: #fff; background-color: #8a6d3b; border-color: #8a6d3b; @@ -5031,20 +5460,27 @@ a.list-group-item-warning.active:focus { color: #a94442; background-color: #f2dede; } -a.list-group-item-danger { +a.list-group-item-danger, +button.list-group-item-danger { color: #a94442; } -a.list-group-item-danger .list-group-item-heading { +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { color: inherit; } a.list-group-item-danger:hover, -a.list-group-item-danger:focus { +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { color: #a94442; background-color: #ebcccc; } a.list-group-item-danger.active, +button.list-group-item-danger.active, a.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus { +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { color: #fff; background-color: #a94442; border-color: #a94442; @@ -5083,7 +5519,11 @@ a.list-group-item-danger.active:focus { font-size: 16px; color: inherit; } -.panel-title > a { +.panel-title > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.panel-title > .small > a { color: inherit; } .panel-footer { @@ -5114,6 +5554,10 @@ a.list-group-item-danger.active:focus { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } +.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} .panel-heading + .list-group .list-group-item:first-child { border-top-width: 0; } @@ -5412,10 +5856,10 @@ a.list-group-item-danger.active:focus { height: 100%; border: 0; } -.embed-responsive.embed-responsive-16by9 { +.embed-responsive-16by9 { padding-bottom: 56.25%; } -.embed-responsive.embed-responsive-4by3 { +.embed-responsive-4by3 { padding-bottom: 75%; } .well { @@ -5474,7 +5918,7 @@ button.close { right: 0; bottom: 0; left: 0; - z-index: 1040; + z-index: 1050; display: none; overflow: hidden; -webkit-overflow-scrolling: touch; @@ -5517,10 +5961,12 @@ button.close { box-shadow: 0 3px 9px rgba(0, 0, 0, .5); } .modal-backdrop { - position: absolute; + position: fixed; top: 0; right: 0; + bottom: 0; left: 0; + z-index: 1040; background-color: #000; } .modal-backdrop.fade { @@ -5593,11 +6039,23 @@ button.close { display: block; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; + font-style: normal; font-weight: normal; - line-height: 1.4; - visibility: visible; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; filter: alpha(opacity=0); opacity: 0; + + line-break: auto; } .tooltip.in { filter: alpha(opacity=90); @@ -5624,7 +6082,6 @@ button.close { padding: 3px 8px; color: #fff; text-align: center; - text-decoration: none; background-color: #000; border-radius: 4px; } @@ -5701,9 +6158,18 @@ button.close { padding: 1px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; white-space: normal; background-color: #fff; -webkit-background-clip: padding-box; @@ -5713,6 +6179,8 @@ button.close { border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + + line-break: auto; } .popover.top { margin-top: -10px; @@ -5840,8 +6308,8 @@ button.close { -webkit-backface-visibility: hidden; backface-visibility: hidden; - -webkit-perspective: 1000; - perspective: 1000; + -webkit-perspective: 1000px; + perspective: 1000px; } .carousel-inner > .item.next, .carousel-inner > .item.active.right { @@ -5940,6 +6408,7 @@ button.close { top: 50%; z-index: 5; display: inline-block; + margin-top: -10px; } .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left { @@ -5955,8 +6424,8 @@ button.close { .carousel-control .icon-next { width: 20px; height: 20px; - margin-top: -10px; font-family: serif; + line-height: 1; } .carousel-control .icon-prev:before { content: '\2039'; @@ -6114,7 +6583,6 @@ button.close { } .hidden { display: none !important; - visibility: hidden !important; } .affix { position: fixed; @@ -6147,7 +6615,7 @@ button.close { display: block !important; } table.visible-xs { - display: table; + display: table !important; } tr.visible-xs { display: table-row !important; @@ -6177,7 +6645,7 @@ button.close { display: block !important; } table.visible-sm { - display: table; + display: table !important; } tr.visible-sm { display: table-row !important; @@ -6207,7 +6675,7 @@ button.close { display: block !important; } table.visible-md { - display: table; + display: table !important; } tr.visible-md { display: table-row !important; @@ -6237,7 +6705,7 @@ button.close { display: block !important; } table.visible-lg { - display: table; + display: table !important; } tr.visible-lg { display: table-row !important; @@ -6290,7 +6758,7 @@ button.close { display: block !important; } table.visible-print { - display: table; + display: table !important; } tr.visible-print { display: table-row !important; diff --git a/docs/assets/css/bootstrap.css.map b/docs/assets/css/bootstrap.css.map index a02f6ba0a96..9f60ed2b1bd 100644 --- a/docs/assets/css/bootstrap.css.map +++ b/docs/assets/css/bootstrap.css.map @@ -1 +1 @@ -{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"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AAA;EHsR9C;AGrRmC;EAAW,kBAAA;EHwR9C;AGvRmC;EAAW,kBAAA;EH0R9C;AGzRmC;EAAW,kBAAA;EH4R9C;AG3RmC;EAAW,kBAAA;EH8R9C;AG7RmC;EAAW,kBAAA;EHgS9C;AG/RmC;EAAW,kBAAA;EHkS9C;AGjSmC;EAAW,kBAAA;EHoS9C;AGnSmC;EAAW,kBAAA;EHsS9C;AGrSmC;EAAW,kBAAA;EHwS9C;AGvSmC;EAAW,kBAAA;EH0S9C;AGzSmC;EAAW,kBAAA;EH4S9C;AG3SmC;EAAW,kBAAA;EH8S9C;AG7SmC;EAAW,kBAAA;EHgT9C;AG/SmC;EAAW,kBAAA;EHkT9C;AGjTmC;EAAW,kBAAA;EHoT9C;AGnTmC;EAAW,kBAAA;EHsT9C;AGrTmC;EAAW,kBAAA;EHwT9C;AGvTmC;EAAW,kBAAA;EH0T9C;AGzTmC;EAAW,kBAAA;EH4T9C;AG3TmC;EAAW,kBAAA;EH8T9C;AG7TmC;EAAW,kBAAA;EHgU9C;AG/TmC;EAAW,kBAAA;EHkU9C;AGjUmC;EAAW,kBAAA;EHoU9C;AGnUmC;EAAW,kBAAA;EHsU9C;AGrUmC;EAAW,kBAAA;EHwU9C;AGvUmC;EAAW,kBAAA;EH0U9C;AGzUmC;EAAW,kBAAA;EH4U9C;AG3UmC;EAAW,kBAAA;EH8U9C;AG7UmC;EAAW,kBAAA;EHgV9C;AG/UmC;EAAW,kBAAA;EHkV9C;AGjVmC;EAAW,kBAAA;EHoV9C;AGnVmC;EAAW,kBAAA;EHsV9C;AGrVmC;EAAW,kBAAA;EHwV9C;AGvVmC;EAAW,kBAAA;EH0V9C;AGzVmC;EAAW,kBAAA;EH4V9C;AG3VmC;EAAW,kBAAA;EH8V9C;AG7VmC;EAAW,kBAAA;EHgW9C;AG/VmC;EAAW,kBAAA;EHkW9C;AGjWmC;EAAW,kBAAA;EHoW9C;AGnWmC;EAAW,kBAAA;EHsW9C;AGrWmC;EAAW,kBAAA;EHwW9C;AGvWmC;EAAW,kBAAA;EH0W9C;AGzWmC;EAAW,kBAAA;EH4W9C;AG3WmC;EAAW,kBAAA;EH8W9C;AG7WmC;EAAW,kBAAA;EHgX9C;AG/WmC;EAAW,kBAAA;EHkX9C;AGjXmC;EAAW,kBAAA;EHoX9C;AGnXmC;EAAW,kBAAA;EHsX9C;AGrXmC;EAAW,kBAAA;EHwX9C;AGvXmC;EAAW,kBAAA;EH0X9C;AGzXmC;EAAW,kBAAA;EH4X9C;AG3XmC;EAAW,kBAAA;EH8X9C;AG7XmC;EAAW,kBAAA;EHgY9C;AG/XmC;EAAW,kBAAA;EHkY9C;AGjYmC;EAAW,kBAAA;EHoY9C;AGnYmC;EAAW,kBAAA;EHsY9C;AGrYmC;EAAW,kBAAA;EHwY9C;AGvYmC;EAAW,kBAAA;EH0Y9C;AGzYmC;EAAW,kBAAA;EH4Y9C;AG3YmC;EAAW,kBAAA;EH8Y9C;AG7YmC;EAAW,kBAAA;EHgZ9C;AG/YmC;EAAW,kBAAA;EHkZ9C;AGjZmC;EAAW,kBAAA;EHoZ9C;AGnZmC;EAAW,kBAAA;EHsZ9C;AGrZmC;EAAW,kBAAA;EHwZ9C;AGvZmC;EAAW,kBAAA;EH0Z9C;AGzZmC;EAAW,kBAAA;EH4Z9C;AG3ZmC;EAAW,kBAAA;EH8Z9C;AG7ZmC;EAAW,kBAAA;EHga9C;AG/ZmC;EAAW,kBAAA;EHka9C;AGjamC;EAAW,kBAAA;EHoa9C;AGnamC;EAAW,kBAAA;EHsa9C;AGramC;EAAW,kBAAA;EHwa9C;AGvamC;EAAW,kBAAA;EH0a9C;AGzamC;EAAW,kBAAA;EH4a9C;AG3amC;EAAW,kBAAA;EH8a9C;AG7amC;EAAW,kBAAA;EHgb9C;AG/amC;EAAW,kBAAA;EHkb9C;AGjbmC;EAAW,kBAAA;EHob9C;AGnbmC;EAAW,kBAAA;EHsb9C;AGrbmC;EAAW,kBAAA;EHwb9C;AGvbmC;EAAW,kBAAA;EH0b9C;AGzbmC;EAAW,kBAAA;EH4b9C;AG3bmC;EAAW,kBAAA;EH8b9C;AG7bmC;EAAW,kBAAA;EHgc9C;AG/bmC;EAAW,kBAAA;EHkc9C;AGjcmC;EAAW,kBAAA;EHoc9C;AGncmC;EAAW,kBAAA;EHsc9C;AGrcmC;EAAW,kBAAA;EHwc9C;AGvcmC;EAAW,kBAAA;EH0c9C;AGzcmC;EAAW,kBAAA;EH4c9C;AG3cmC;EAAW,kBAAA;EH8c9C;AG7cmC;EAAW,kBAAA;EHgd9C;AG/cmC;EAAW,kBAAA;EHkd9C;AGjdmC;EAAW,kBAAA;EHod9C;AGndmC;EAAW,kBAAA;EHsd9C;AGrdmC;EAAW,kBAAA;EHwd9C;AGvdmC;EAAW,kBAAA;EH0d9C;AGzdmC;EAAW,kBAAA;EH4d9C;AG3dmC;EAAW,kBAAA;EH8d9C;AG7dmC;EAAW,kBAAA;EHge9C;AG/dmC;EAAW,kBAAA;EHke9C;AGjemC;EAAW,kBAAA;EHoe9C;AGnemC;EAAW,kBAAA;EHse9C;AGremC;EAAW,kBAAA;EHwe9C;AGvemC;EAAW,kBAAA;EH0e9C;AGzemC;EAAW,kBAAA;EH4e9C;AG3emC;EAAW,kBAAA;EH8e9C;AG7emC;EAAW,kBAAA;EHgf9C;AG/emC;EAAW,kBAAA;EHkf9C;AGjfmC;EAAW,kBAAA;EHof9C;AGnfmC;EAAW,kBAAA;EHsf9C;AGrfmC;EAAW,kBAAA;EHwf9C;AGvfmC;EAAW,kBAAA;EH0f9C;AGzfmC;EAAW,kBAAA;EH4f9C;AG3fmC;EAAW,kBAAA;EH8f9C;AG7fmC;EAAW,kBAAA;EHggB9C;AG/fmC;EAAW,kBAAA;EHkgB9C;AGjgBmC;EAAW,kBAAA;EHogB9C;AGngBmC;EAAW,kBAAA;EHsgB9C;AGrgBmC;EAAW,kBAAA;EHwgB9C;AGvgBmC;EAAW,kBAAA;EH0gB9C;AGzgBmC;EAAW,kBAAA;EH4gB9C;AG3gBmC;EAAW,kBAAA;EH8gB9C;AG7gBmC;EAAW,kBAAA;EHghB9C;AG/gBmC;EAAW,kBAAA;EHkhB9C;AGjhBmC;EAAW,kBAAA;EHohB9C;AGnhBmC;EAAW,kBAAA;EHshB9C;AGrhBmC;EAAW,kBAAA;EHwhB9C;AGvhBmC;EAAW,kBAAA;EH0hB9C;AGzhBmC;EAAW,kBAAA;EH4hB9C;AG3hBmC;EAAW,kBAAA;EH8hB9C;AG7hBmC;EAAW,kBAAA;EHgiB9C;AG/hBmC;EAAW,kBAAA;EHkiB9C;AGjiBmC;EAAW,kBAAA;EHoiB9C;AGniBmC;EAAW,kBAAA;EHsiB9C;AGriBmC;EAAW,kBAAA;EHwiB9C;AGviBmC;EAAW,kBAAA;EH0iB9C;AGziBmC;EAAW,kBAAA;EH4iB9C;AG3iBmC;EAAW,kBAAA;EH8iB9C;AG7iBmC;EAAW,kBAAA;EHgjB9C;AG/iBmC;EAAW,kBA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EACE,mBAAA;Efi7CH;Ael7CC;EACE,2BAAA;Efo7CH;Aer7CC;EACE,2BAAA;Efu7CH;Aex7CC;EACE,kBAAA;Ef07CH;Ae37CC;EACE,2BAAA;Ef67CH;Ae97CC;EACE,2BAAA;Efg8CH;Aej8CC;EACE,kBAAA;Efm8CH;Aep8CC;EACE,2BAAA;Efs8CH;Aev8CC;EACE,2BAAA;Efy8CH;Ae18CC;EACE,kBAAA;Ef48CH;Ae78CC;EACE,2BAAA;Ef+8CH;Aeh9CC;EACE,0BAAA;Efk9CH;Aen9CC;EACE,iBAAA;Efq9CH;Aaz9CD;EE9BI;IACE,aAAA;If0/CH;Een/CD;IACE,aAAA;Ifq/CD;Eet/CD;IACE,qBAAA;Ifw/CD;Eez/CD;IACE,qBAAA;If2/CD;Ee5/CD;IACE,YAAA;If8/CD;Ee//CD;IACE,qBAAA;IfigDD;EelgDD;IACE,qBAAA;IfogDD;EergDD;IACE,YAAA;IfugDD;EexgDD;IACE,qBAAA;If0gDD;Ee3gDD;IACE,qBAAA;If6gDD;Ee9gDD;IACE,YAAA;IfghDD;EejhDD;IACE,qBAAA;IfmhDD;EephDD;IACE,oBAAA;IfshDD;EexgDD;IACE,aAAA;If0gDD;Ee3gDD;IACE,qBAAA;If6gDD;Ee9gDD;IACE,qBAAA;IfghDD;EejhDD;IACE,YAAA;IfmhDD;EephDD;IACE,qBAAA;IfshDD;EevhDD;IACE,qBAAA;IfyhDD;Ee1hDD;IACE,YAAA;If4hDD;Ee7hDD;IACE,qBAAA;If+hDD;EehiDD;IACE,qBAAA;IfkiDD;EeniDD;IACE,YAAA;IfqiDD;EetiDD;IACE,qBAAA;IfwiDD;EeziDD;IACE,oBAAA;If2iDD;EeviDD;IACE,aAAA;IfyiDD;EezjDD;IACE,YAAA;If2jDD;Ee5jDD;IACE,oBAAA;If8jDD;Ee/jDD;IACE,oBAAA;IfikDD;EelkDD;IACE,WAAA;IfokDD;EerkDD;IACE,oBAAA;IfukDD;EexkDD;IACE,oBAAA;If0kDD;Ee3kDD;IACE,WAAA;If6kDD;Ee9kDD;IACE,oBAAA;IfglDD;EejlDD;IACE,oBAAA;IfmlDD;EeplDD;IACE,WAAA;IfslDD;EevlDD;IACE,oBAAA;IfylDD;Ee1lDD;IACE,mBAAA;If4lDD;EexlDD;IACE,YAAA;If0lDD;Ee5kDD;IACE,mBAAA;If8kDD;Ee/kDD;IACE,2BAAA;IfilDD;EellDD;IACE,2BAAA;IfolDD;EerlDD;IACE,kBAAA;IfulDD;EexlDD;IACE,2BAAA;If0lDD;Ee3lDD;IACE,2BAAA;If6lDD;Ee9lDD;IACE,kBAAA;IfgmDD;EejmDD;IACE,2BAAA;IfmmDD;EepmDD;IACE,2BAAA;IfsmDD;EevmDD;IACE,kBAAA;IfymDD;Ee1mDD;IACE,2BAAA;If4mDD;Ee7mDD;IACE,0BAAA;If+mDD;EehnDD;IACE,iBAAA;IfknDD;EACF;Aa9mDD;EEvCI;IACE,aAAA;IfwpDH;EejpDD;IACE,aAAA;IfmpDD;EeppDD;IACE,qBAAA;IfspDD;EevpDD;IACE,qBAAA;IfypDD;Ee1pDD;IACE,YAAA;If4pDD;Ee7pDD;IACE,qBAAA;If+pDD;EehqDD;IACE,qBAAA;IfkqDD;EenqDD;IACE,YAAA;IfqqDD;EetqDD;IACE,qBAAA;IfwqDD;EezqDD;IACE,qBAAA;If2qDD;Ee5qDD;IACE,YAAA;If8qDD;Ee/qDD;IACE,qBAAA;IfirDD;EelrDD;IACE,oBAAA;IforDD;EetqDD;IACE,aAAA;IfwqDD;EezqDD;IACE,qBAAA;If2qDD;Ee5qDD;IACE,qBAAA;If8qDD;Ee/qDD;IACE,YAAA;IfirDD;EelrDD;IACE,qBAAA;IforDD;EerrDD;IACE,qBAAA;IfurDD;EexrDD;IACE,YAAA;If0rDD;Ee3rDD;IACE,qBAAA;If6rDD;Ee9rDD;IACE,qBAAA;IfgsDD;EejsDD;IACE,YAAA;IfmsDD;EepsDD;IACE,qBAAA;IfssDD;EevsDD;IACE,oBAAA;IfysDD;EersDD;IACE,aAAA;IfusDD;EevtDD;IACE,YAAA;IfytDD;Ee1tDD;IACE,oBAAA;If4tDD;Ee7tDD;IACE,oBAAA;If+tDD;EehuDD;IACE,WAAA;IfkuDD;EenuDD;IACE,oBAAA;IfquDD;EetuDD;IACE,oBAAA;IfwuDD;EezuDD;IACE,WAAA;If2uDD;Ee5uDD;IACE,oBAAA;If8uDD;Ee/uDD;IACE,oBAAA;IfivDD;EelvDD;IACE,WAAA;IfovDD;EervDD;IACE,oBAAA;IfuvDD;EexvDD;IACE,mBAAA;If0vDD;EetvDD;IACE,YAAA;IfwvDD;Ee1uDD;IACE,mBAAA;If4uDD;Ee7uDD;IACE,2BAAA;If+uDD;EehvDD;IACE,2BAAA;IfkvDD;EenvDD;IACE,kBAAA;IfqvDD;EetvDD;IACE,2BAAA;IfwvDD;EezvDD;IACE,2BAAA;If2vDD;Ee5vDD;IACE,kBAAA;If8vDD;Ee/vDD;IACE,2BAAA;IfiwDD;EelwDD;IACE,2BAAA;IfowDD;EerwDD;IACE,kBAAA;IfuwDD;EexwDD;IACE,2BAAA;If0wDD;Ee3wDD;IACE,0BAAA;If6wDD;Ee9wDD;IACE,iBAAA;IfgxDD;EACF;AarwDD;EE9CI;IACE,aAAA;IfszDH;Ee/yDD;IACE,aAAA;IfizDD;EelzDD;IACE,qBAAA;IfozDD;EerzDD;IACE,qBAAA;IfuzDD;EexzDD;IACE,YAAA;If0zDD;Ee3zDD;IACE,qBAAA;If6zDD;Ee9zDD;IACE,qBAAA;Ifg0DD;Eej0DD;IACE,YAAA;Ifm0DD;Eep0DD;IACE,qBAAA;Ifs0DD;Eev0DD;IACE,qBAAA;Ify0DD;Ee10DD;IACE,YAAA;If40DD;Ee70DD;IACE,qBAAA;If+0DD;Eeh1DD;IACE,oBAAA;Ifk1DD;Eep0DD;IACE,aAAA;Ifs0DD;Eev0DD;IACE,qBAAA;Ify0DD;Ee10DD;IACE,qBAAA;If40DD;Ee70DD;IACE,YAAA;If+0DD;Eeh1DD;IACE,qBAAA;Ifk1DD;Een1DD;IACE,qBAAA;Ifq1DD;Eet1DD;IACE,YAAA;Ifw1DD;Eez1DD;IACE,qBAAA;If21DD;Ee51DD;IACE,qBAAA;If81DD;Ee/1DD;IACE,YAAA;Ifi2DD;Eel2DD;IACE,qBAAA;Ifo2DD;Eer2DD;IACE,oBAAA;Ifu2DD;Een2DD;IACE,aAAA;Ifq2DD;Eer3DD;IACE,YAAA;Ifu3DD;Eex3DD;IACE,oBAAA;If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EACA,uBAAA;ErB+tFD;AqB7tFC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB+tFP;AqB7tFC;;;EAGE,wBAAA;ErB+tFH;AqB1tFG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBwuFT;AoBhtFD;ECnBI,gBAAA;EACA,2BAAA;ErBsuFH;AoBjtFD;ECxDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB4wFD;AqB1wFC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB4wFP;AqB1wFC;;;EAGE,wBAAA;ErB4wFH;AqBvwFG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBqxFT;AoB1vFD;ECtBI,gBAAA;EACA,2BAAA;ErBmxFH;AoB1vFD;EC5DE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErByzFD;AqBvzFC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErByzFP;AqBvzFC;;;EAGE,wBAAA;ErByzFH;AqBpzFG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBk0FT;AoBnyFD;EC1BI,gBAAA;EACA,2BAAA;ErBg0FH;AoBnyFD;EChEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBs2FD;AqBp2FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBs2FP;AqBp2FC;;;EAGE,wBAAA;ErBs2FH;AqBj2FG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErB+2FT;AoB50FD;EC9BI,gBAAA;EACA,2BAAA;ErB62FH;AoB50FD;ECpEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBm5FD;AqBj5FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBm5FP;AqBj5FC;;;EAGE,wBAAA;ErBm5FH;AqB94FG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErB45FT;AoBr3FD;EClCI,gBAAA;EACA,2BAAA;ErB05FH;AoBr3FD;ECxEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBg8FD;AqB97FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;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","file":"bootstrap.css","sourcesContent":["/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n -moz-box-sizing: content-box;\n -webkit-box-sizing: content-box;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important;\n box-shadow: none !important;\n text-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n select {\n background: #fff !important;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('../fonts/glyphicons-halflings-regular.eot');\n src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\2a\";\n}\n.glyphicon-plus:before {\n content: \"\\2b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #ffffff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n background-color: #fcf8e3;\n padding: .2em;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #ffffff;\n background-color: #333333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n word-break: break-all;\n word-wrap: break-word;\n color: #333333;\n background-color: #f5f5f5;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n.row {\n margin-left: -15px;\n margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #dddddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #dddddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #dddddd;\n}\n.table .table {\n background-color: #ffffff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-child(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #dddddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n min-width: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #ffffff;\n background-image: none;\n border: 1px solid #cccccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999999;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n background-color: #eeeeee;\n opacity: 1;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"],\n input[type=\"time\"],\n input[type=\"datetime-local\"],\n input[type=\"month\"] {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-left: 0;\n padding-right: 0;\n}\n.input-sm,\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm,\nselect.form-group-sm .form-control {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\ntextarea.form-group-sm .form-control,\nselect[multiple].input-sm,\nselect[multiple].form-group-sm .form-control {\n height: auto;\n}\n.input-lg,\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.33;\n border-radius: 6px;\n}\nselect.input-lg,\nselect.form-group-lg .form-control {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\ntextarea.form-group-lg .form-control,\nselect[multiple].input-lg,\nselect[multiple].form-group-lg .form-control {\n height: auto;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n border-color: #3c763d;\n background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n border-color: #8a6d3b;\n background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n border-color: #a94442;\n background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n margin-top: 0;\n margin-bottom: 0;\n padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-left: -15px;\n margin-right: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n text-align: right;\n margin-bottom: 0;\n padding-top: 7px;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 14.3px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n white-space: nowrap;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n outline: 0;\n background-image: none;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n pointer-events: none;\n opacity: 0.65;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default {\n color: #333333;\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default:hover,\n.btn-default:focus,\n.btn-default.focus,\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default .badge {\n color: #ffffff;\n background-color: #333333;\n}\n.btn-primary {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:hover,\n.btn-primary:focus,\n.btn-primary.focus,\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #ffffff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.btn-success {\n color: #ffffff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:hover,\n.btn-success:focus,\n.btn-success.focus,\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #ffffff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #ffffff;\n}\n.btn-info {\n color: #ffffff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:hover,\n.btn-info:focus,\n.btn-info.focus,\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #ffffff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #ffffff;\n}\n.btn-warning {\n color: #ffffff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:hover,\n.btn-warning:focus,\n.btn-warning.focus,\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #ffffff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #ffffff;\n}\n.btn-danger {\n color: #ffffff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:hover,\n.btn-danger:focus,\n.btn-danger.focus,\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #ffffff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #ffffff;\n}\n.btn-link {\n color: #337ab7;\n font-weight: normal;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.33;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n visibility: hidden;\n}\n.collapse.in {\n display: block;\n visibility: visible;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n list-style: none;\n font-size: 14px;\n text-align: left;\n background-color: #ffffff;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #ffffff;\n text-decoration: none;\n outline: 0;\n background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n cursor: not-allowed;\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n left: auto;\n right: 0;\n}\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n border-top: 0;\n border-bottom: 4px solid;\n content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 1px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n left: auto;\n right: 0;\n }\n .navbar-right .dropdown-menu-left {\n left: 0;\n right: auto;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child > .btn:last-child,\n.btn-group > .btn-group:first-child > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child > .btn:first-child {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-bottom-left-radius: 4px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.33;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n margin-left: -1px;\n}\n.nav {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n background-color: transparent;\n cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #dddddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #dddddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-bottom-color: transparent;\n cursor: default;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #ffffff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n visibility: hidden;\n}\n.tab-content > .active {\n display: block;\n visibility: visible;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n overflow-x: visible;\n padding-right: 15px;\n padding-left: 15px;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n visibility: visible !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-left: 0;\n padding-right: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n height: 50px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: 15px;\n padding: 9px 10px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n margin-left: -15px;\n margin-right: -15px;\n padding: 10px 15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-left: 15px;\n margin-right: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #dddddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #dddddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n background-color: #e7e7e7;\n color: #555555;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777777;\n}\n.navbar-default .navbar-link:hover {\n color: #333333;\n}\n.navbar-default .btn-link {\n color: #777777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #cccccc;\n}\n.navbar-inverse {\n background-color: #222222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #ffffff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n background-color: #080808;\n color: #ffffff;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #ffffff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #ffffff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n content: \"/\\00a0\";\n padding: 0 5px;\n color: #cccccc;\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n line-height: 1.42857143;\n text-decoration: none;\n color: #337ab7;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-bottom-left-radius: 4px;\n border-top-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-bottom-right-radius: 4px;\n border-top-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n color: #23527c;\n background-color: #eeeeee;\n border-color: #dddddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n background-color: #ffffff;\n border-color: #dddddd;\n cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-bottom-left-radius: 6px;\n border-top-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-bottom-right-radius: 6px;\n border-top-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-bottom-left-radius: 3px;\n border-top-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-bottom-right-radius: 3px;\n border-top-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n list-style: none;\n text-align: center;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n background-color: #ffffff;\n cursor: not-allowed;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #ffffff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n color: #ffffff;\n line-height: 1;\n vertical-align: baseline;\n white-space: nowrap;\n text-align: center;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding: 30px 15px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding: 48px 0;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-left: 60px;\n padding-right: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-left: auto;\n margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n background-color: #dff0d8;\n border-color: #d6e9c6;\n color: #3c763d;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n background-color: #d9edf7;\n border-color: #bce8f1;\n color: #31708f;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n background-color: #fcf8e3;\n border-color: #faebcc;\n color: #8a6d3b;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n background-color: #f2dede;\n border-color: #ebccd1;\n color: #a94442;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n overflow: hidden;\n height: 20px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #ffffff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n margin-bottom: 20px;\n padding-left: 0;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n}\n.list-group-item:first-child {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item {\n color: #555555;\n}\na.list-group-item .list-group-item-heading {\n color: #333333;\n}\na.list-group-item:hover,\na.list-group-item:focus {\n text-decoration: none;\n color: #555555;\n background-color: #f5f5f5;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n background-color: #eeeeee;\n color: #777777;\n cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\na.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\na.list-group-item-success.active:hover,\na.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\na.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\na.list-group-item-info.active:hover,\na.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\na.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\na.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #ffffff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #dddddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-left: 15px;\n padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-left-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #dddddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n border: 0;\n margin-bottom: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #dddddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #dddddd;\n}\n.panel-default {\n border-color: #dddddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #dddddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #dddddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #dddddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n}\n.embed-responsive.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000000;\n text-shadow: 0 1px 0 #ffffff;\n opacity: 0.2;\n filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n color: #000000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #ffffff;\n border: 1px solid #999999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n background-clip: padding-box;\n outline: 0;\n}\n.modal-backdrop {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n background-color: #000000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n min-height: 16.42857143px;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n visibility: visible;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 12px;\n font-weight: normal;\n line-height: 1.4;\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.tooltip.in {\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.tooltip.top {\n margin-top: -3px;\n padding: 5px 0;\n}\n.tooltip.right {\n margin-left: 3px;\n padding: 0 5px;\n}\n.tooltip.bottom {\n margin-top: 3px;\n padding: 5px 0;\n}\n.tooltip.left {\n margin-left: -3px;\n padding: 0 5px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #ffffff;\n text-align: center;\n text-decoration: none;\n background-color: #000000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-left .tooltip-arrow {\n bottom: 0;\n right: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n font-weight: normal;\n line-height: 1.42857143;\n text-align: left;\n background-color: #ffffff;\n background-clip: padding-box;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n white-space: normal;\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n margin: 0;\n padding: 8px 14px;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n border-width: 10px;\n content: \"\";\n}\n.popover.top > .arrow {\n left: 50%;\n margin-left: -11px;\n border-bottom-width: 0;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n bottom: -11px;\n}\n.popover.top > .arrow:after {\n content: \" \";\n bottom: 1px;\n margin-left: -10px;\n border-bottom-width: 0;\n border-top-color: #ffffff;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-left-width: 0;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n content: \" \";\n left: 1px;\n bottom: -10px;\n border-left-width: 0;\n border-right-color: #ffffff;\n}\n.popover.bottom > .arrow {\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n top: -11px;\n}\n.popover.bottom > .arrow:after {\n content: \" \";\n top: 1px;\n margin-left: -10px;\n border-top-width: 0;\n border-bottom-color: #ffffff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: #ffffff;\n bottom: -10px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n}\n.carousel-inner > .item {\n display: none;\n position: relative;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n transition: transform 0.6s ease-in-out;\n backface-visibility: hidden;\n perspective: 1000;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 15%;\n opacity: 0.5;\n filter: alpha(opacity=50);\n font-size: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n left: auto;\n right: 0;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n outline: 0;\n color: #ffffff;\n text-decoration: none;\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n margin-top: -10px;\n font-family: serif;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid #ffffff;\n border-radius: 10px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: #ffffff;\n}\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -15px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -15px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -15px;\n }\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-footer:before,\n.modal-footer:after {\n content: \" \";\n display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n visibility: hidden !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS text size adjust after orientation change, without disabling\n// user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability when focused and also mouse hovered in all browsers.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome\n// (include `-moz` to future-proof).\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n -moz-box-sizing: content-box;\n -webkit-box-sizing: content-box; // 2\n box-sizing: content-box;\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n box-shadow: none !important;\n text-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n //\n // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245\n // Once fixed, we can just straight up remove this.\n select {\n background: #fff !important;\n }\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\2a\"; } }\n.glyphicon-plus { &:before { content: \"\\2b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content/\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // See https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @grid-float-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-child(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &[disabled],\n &[readonly],\n fieldset[disabled] & {\n cursor: @cursor-disabled;\n background-color: @input-bg-disabled;\n opacity: 1; // iOS fix for unreadable disabled content\n }\n\n // Reset height for `textarea`s\n textarea& {\n height: auto;\n }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n\n\n// Special styles for iOS temporal inputs\n//\n// In Mobile Safari, setting `display: block` on temporal inputs causes the\n// text within the input to become vertically misaligned. As a workaround, we\n// set a pixel line-height that matches the given height of the input, but only\n// for Safari.\n\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"],\n input[type=\"time\"],\n input[type=\"datetime-local\"],\n input[type=\"month\"] {\n line-height: @input-height-base;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm {\n line-height: @input-height-small;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg {\n line-height: @input-height-large;\n }\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n margin-bottom: 15px;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n\n label {\n min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n// Some special care is needed because
    ","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:k.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("