├── .gitignore ├── .mailmap ├── .travis.yml ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── bin ├── chronos-sync.rb └── start.sh ├── build-release.sh ├── build.xml ├── changelog.md ├── docs ├── .gitignore ├── Gemfile ├── README.md ├── _config.yml ├── _layouts │ ├── default.html │ ├── docs.html │ └── narrow.html ├── _sass │ ├── _base.scss │ ├── _buttons.scss │ └── _highlight.scss ├── css │ └── style.scss ├── docs │ ├── api.md │ ├── authentication.md │ ├── configuration.md │ ├── contributing.md │ ├── debugging.md │ ├── faq.md │ ├── getting-started.md │ ├── job-management.md │ └── webui.md ├── img │ ├── .gitkeep │ └── emr_use_case.png ├── index.md ├── resources.md └── support.md ├── integration-tests ├── .dockerignore ├── Dockerfile ├── check-results.rb └── jobs │ ├── dependent │ └── hourly-dependent-sleep1m.yaml │ └── scheduled │ ├── daily-sleep1m.yaml │ ├── hourly-sleep1m.yaml │ └── weekly-sleep1m.yaml ├── pom.xml └── src ├── main ├── resources │ ├── jetty-logging.properties │ ├── logging.properties │ └── ui │ │ ├── .babelrc │ │ ├── .bowerrc │ │ ├── .editorconfig │ │ ├── .eslintrc │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .jshintrc │ │ ├── .npmignore │ │ ├── assets │ │ ├── css │ │ │ ├── bootstrap-theme.css │ │ │ ├── bootstrap.min.css │ │ │ ├── font-awesome.min.css │ │ │ ├── jsoneditor.min.css │ │ │ └── react-select.min.css │ │ └── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── RobotoMono-Bold.ttf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── fontawesome-webfont.woff2 │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── bin │ │ └── server.js │ │ ├── components │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── JobEditor.js │ │ ├── JobSummaryView.js │ │ ├── JsonEditor.js │ │ ├── Main.js │ │ └── Root.js │ │ ├── index.html │ │ ├── models │ │ ├── JobForm.js │ │ ├── JobSummaryModel.js │ │ └── JsonModel.js │ │ ├── package.json │ │ ├── server.js │ │ ├── stores │ │ ├── JobSummaryStore.js │ │ └── JsonStore.js │ │ ├── webpack-release.config.js │ │ └── webpack.config.js └── scala │ └── org │ └── apache │ └── mesos │ └── chronos │ ├── notification │ ├── HttpClient.scala │ ├── JobNotificationObserver.scala │ ├── MailClient.scala │ ├── MattermostClient.scala │ ├── NotificationClient.scala │ ├── RavenClient.scala │ └── SlackClient.scala │ ├── scheduler │ ├── Main.scala │ ├── api │ │ ├── ApiResult.scala │ │ ├── ApiResultSerializer.scala │ │ ├── ChronosRestModule.scala │ │ ├── CorsFilter.scala │ │ ├── DependentJobResource.scala │ │ ├── GraphManagementResource.scala │ │ ├── HistogramSerializer.scala │ │ ├── Iso8601JobResource.scala │ │ ├── JobManagementResource.scala │ │ ├── JobStatWrapperSerializer.scala │ │ ├── JobSummaryWrapperSerializer.scala │ │ ├── LeaderRestModule.scala │ │ ├── PathConstants.scala │ │ ├── RedirectFilter.scala │ │ ├── StatsResource.scala │ │ ├── TaskManagementResource.scala │ │ └── WebJarServlet.scala │ ├── config │ │ ├── CassandraConfiguration.scala │ │ ├── GraphiteConfiguration.scala │ │ ├── JobMetricsModule.scala │ │ ├── JobStatsModule.scala │ │ ├── MainModule.scala │ │ ├── MediaType.scala │ │ ├── SchedulerConfiguration.scala │ │ └── ZookeeperModule.scala │ ├── graph │ │ └── JobGraph.scala │ ├── jobs │ │ ├── Containers.scala │ │ ├── EnvironmentVariable.scala │ │ ├── Fetch.scala │ │ ├── Iso8601Expressions.scala │ │ ├── JobMetrics.scala │ │ ├── JobSchedule.scala │ │ ├── JobScheduler.scala │ │ ├── JobStatWrapper.scala │ │ ├── JobSummaryWrapper.scala │ │ ├── JobUtils.scala │ │ ├── Jobs.scala │ │ ├── JobsObserver.scala │ │ ├── Label.scala │ │ ├── MetricReporterService.scala │ │ ├── Parameter.scala │ │ ├── ScheduledTask.scala │ │ ├── TaskManager.scala │ │ ├── TaskStat.scala │ │ ├── TaskUtils.scala │ │ ├── ZookeeperService.scala │ │ ├── constraints │ │ │ ├── Constraint.scala │ │ │ ├── EqualsConstraint.scala │ │ │ ├── LikeConstraint.scala │ │ │ └── UnlikeConstraint.scala │ │ ├── graph │ │ │ └── Exporter.scala │ │ └── stats │ │ │ └── JobStats.scala │ ├── mesos │ │ ├── ConstraintChecker.scala │ │ ├── MesosDriverFactory.scala │ │ ├── MesosJobFramework.scala │ │ ├── MesosOfferReviver.scala │ │ ├── MesosOfferReviverActor.scala │ │ ├── MesosOfferReviverDelegate.scala │ │ ├── MesosTaskBuilder.scala │ │ └── SchedulerDriverBuilder.scala │ └── state │ │ ├── MesosStatePersistenceStore.scala │ │ └── PersistenceStore.scala │ └── utils │ ├── JobDeserializer.scala │ ├── JobSerializer.scala │ ├── Supervisor.scala │ └── Timestamp.scala └── test ├── resources └── logging.properties └── scala └── org └── apache └── mesos └── chronos ├── ChronosTestHelper.scala └── scheduler ├── api └── SerDeTest.scala ├── jobs ├── Iso8601ExpressionParserSpec.scala ├── JobSchedulerElectionSpec.scala ├── JobSchedulerSpec.scala ├── JobUtilsSpec.scala ├── MockJobUtils.scala ├── TaskManagerSpec.scala ├── TaskUtilsSpec.scala ├── constraints │ ├── ConstraintSpecHelper.scala │ ├── EqualsConstraintSpec.scala │ ├── LikeConstraintSpec.scala │ └── UnlikeConstraintSpec.scala ├── graph │ └── JobGraphSpec.scala └── stats │ └── JobStatsSpec.scala ├── mesos ├── ConstraintCheckerSpec.scala ├── MesosDriverFactorySpec.scala ├── MesosJobFrameworkSpec.scala ├── MesosOfferReviverActorSpec.scala ├── MesosOfferReviverDelegateSpec.scala └── MesosTaskBuilderSpec.scala └── state └── PersistenceStoreSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .*sw? 2 | *.iml 3 | *.log 4 | .idea/* 5 | target 6 | src/main/resources/**/temp 7 | src/main/resources/**/build 8 | .DS_Store 9 | .rvmrc 10 | node_modules 11 | chronos.arx 12 | chronos.tbz 13 | .classpath.txt 14 | dependency-reduced-pom.xml 15 | .arcconfig 16 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Andy Kramolisch 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: scala 3 | env: 4 | - NODE_VERSION=7.2.0 5 | install: 6 | - nvm install $NODE_VERSION 7 | script: 8 | - nvm use $NODE_VERSION 9 | - mvn clean test package 10 | scala: 11 | - "2.11.2" 12 | jdk: 13 | - oraclejdk8 14 | cache: 15 | directories: 16 | - $HOME/.m2/repository 17 | - src/main/resources/ui/node_modules 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | ARG http_proxy 3 | ENV http_proxy ${http_proxy} 4 | 5 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E56151BF \ 6 | && echo "deb http://repos.mesosphere.com/debian jessie-unstable main" | tee /etc/apt/sources.list.d/mesosphere.list \ 7 | && echo "deb http://repos.mesosphere.com/debian jessie-testing main" | tee -a /etc/apt/sources.list.d/mesosphere.list \ 8 | && echo "deb http://repos.mesosphere.com/debian jessie main" | tee -a /etc/apt/sources.list.d/mesosphere.list \ 9 | && apt-get update \ 10 | && apt-get install --no-install-recommends -y --force-yes mesos=1.0.1-2.0.93.debian81 \ 11 | && apt-get clean \ 12 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 13 | 14 | ADD ./tmp/chronos.jar /chronos/chronos.jar 15 | ADD bin/start.sh /chronos/bin/start.sh 16 | ENTRYPOINT ["/chronos/bin/start.sh"] 17 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Airbnb 2 | Copyright 2015 Mesosphere 3 | 4 | This file has been modified from its original form by Mesosphere, Inc. (“Mesosphere”). All modifications made to this file by Mesosphere (the “Modifications”) are © 2015 Mesosphere. The Modifications are licensed to you under the Apache License, Version 2.0. 5 | 6 | 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 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chronos [![Build Status](https://travis-ci.org/mesos/chronos.svg?branch=master)](https://travis-ci.org/mesos/chronos) 2 | Chronos is a replacement for `cron`. It is a distributed and fault-tolerant scheduler that runs on top of [Apache Mesos][mesos] that can be used for job orchestration. It supports custom Mesos executors as well 3 | as the default command executor. Thus by default, Chronos executes `sh` 4 | (on most systems bash) scripts. 5 | 6 | Chronos can be used to interact with systems such as Hadoop (incl. EMR), even if the Mesos agents on which execution happens do not have Hadoop installed. Included wrapper scripts allow transfering files and executing them on a remote machine in the background and using asynchronous callbacks to notify Chronos of job completion or failures. Chronos is also natively able to schedule jobs that run inside Docker containers. 7 | 8 | Chronos has a number of advantages over regular cron. 9 | It allows you to schedule your jobs using [ISO8601][ISO8601] repeating interval notation, which enables more flexibility in job scheduling. Chronos also supports the definition of jobs triggered by the completion of other jobs. It supports arbitrarily long dependency chains. 10 | 11 | *The easiest way to use Chronos is to use [DC/OS](https://dcos.io/get-started/) and install chronos via the universe.* 12 | 13 | 14 | ## Features 15 | 16 | * Web UI 17 | * ISO8601 Repeating Interval Notation 18 | * Handles dependencies 19 | * Job Stats (e.g. 50th, 75th, 95th and 99th percentile timing, failure/success) 20 | * Job History (e.g. job duration, start time, end time, failure/success) 21 | * Fault Tolerance (leader/follower) 22 | * Configurable Retries 23 | * Multiple Workers (i.e. Mesos agents) 24 | * Native Docker support 25 | 26 | ## Documentation and Support 27 | 28 | Chronos documentation is available on the [Chronos GitHub pages site](https://mesos.github.io/chronos/). 29 | 30 | Documentation for installing and configuring the full Mesosphere stack including Mesos and Chronos is available on the [Mesosphere website](https://docs.mesosphere.com). 31 | 32 | For questions and discussions around Chronos, please use the Google Group "chronos-scheduler": 33 | [Chronos Scheduler Group](https://groups.google.com/forum/#!forum/chronos-scheduler). 34 | 35 | If you'd like to take part in design research and test new features in Chronos before they're released, please add your name to Mesosphere's [UX Research](http://uxresearch.mesosphere.com) list. 36 | 37 | ## Packaging 38 | 39 | Mesosphere publishes Docker images for Chronos to Dockerhub, at . 40 | 41 | ## Contributing 42 | 43 | Instructions on how to contribute to Chronos are available on the [Contributing](http://mesos.github.io/chronos/docs/contributing.html) docs page. 44 | 45 | ## License 46 | 47 | The use and distribution terms for this software are covered by the 48 | Apache 2.0 License (http://www.apache.org/licenses/LICENSE-2.0.html) 49 | which can be found in the file LICENSE at the root of this distribution. 50 | By using this software in any fashion, you are agreeing to be bound by 51 | the terms of this license. 52 | You must not remove this notice, or any other, from this software. 53 | 54 | ## Contributors 55 | 56 | * Florian Leibert ([@flo](http://twitter.com/flo)) 57 | * Andy Kramolisch ([@andykram](https://github.com/andykram)) 58 | * Harry Shoff ([@hshoff](https://twitter.com/hshoff)) 59 | * Elizabeth Lingg 60 | 61 | ## Reporting Bugs 62 | 63 | Please see the [support page](http://mesos.github.io/chronos/support.html) for information on how to report bugs. 64 | 65 | [ISO8601]: http://en.wikipedia.org/wiki/ISO_8601 "ISO8601 Standard" 66 | [mesos]: https://mesos.apache.org/ "Apache Mesos" 67 | -------------------------------------------------------------------------------- /bin/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export LIBPROCESS_PORT="${PORT1}" 4 | exec java $JVM_OPTS -jar /chronos/chronos.jar $@ --http_port $PORT0 5 | -------------------------------------------------------------------------------- /build-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | branch=`git rev-parse --abbrev-ref HEAD` 5 | if [ "$branch" = "master" ]; then 6 | set +e # ignore error of following command 7 | tag=`git describe --exact-match --tags HEAD` 8 | exit_code=$? 9 | set -e 10 | if [ $exit_code = 0 ]; then 11 | image_tag=`git describe --tags` 12 | else 13 | image_tag="latest" 14 | fi 15 | else 16 | image_tag=$branch 17 | fi 18 | version=`git describe --tags` 19 | 20 | echo "Branch: $branch" 21 | echo "Tag: $tag" 22 | echo "Image tag: $image_tag" 23 | echo "Version: $version" 24 | 25 | mkdir -p tmp 26 | 27 | # build jar 28 | docker run -e http_proxy=$http_proxy -v `pwd`:/mnt/build --entrypoint=/bin/sh maven:3-jdk-8 -c "\ 29 | curl -sL https://deb.nodesource.com/setup_7.x | bash - \ 30 | && apt-get update && apt-get install -y --no-install-recommends nodejs \ 31 | && ln -sf /usr/bin/nodejs /usr/bin/node \ 32 | && cp -r /mnt/build /chronos \ 33 | && cd /chronos \ 34 | && mvn clean \ 35 | && mvn versions:set -DnewVersion=$version \ 36 | && mvn package \ 37 | && cp target/chronos-$version.jar /mnt/build/tmp/chronos.jar \ 38 | " 39 | 40 | # build image 41 | docker build --build-arg http_proxy=$http_proxy -t mesosphere/chronos:$image_tag . 42 | 43 | if [ ! -z ${DOCKER_HUB_USERNAME+x} -a ! -z ${DOCKER_HUB_PASSWORD+x} ]; then 44 | # login to dockerhub 45 | docker login -u "${DOCKER_HUB_USERNAME}" -p "${DOCKER_HUB_PASSWORD}" 46 | 47 | # push image 48 | docker push mesosphere/chronos:$image_tag 49 | fi 50 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ${sourceDir} Hasn't been updated, skipping asset compilation. 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | [Follow this link to view the release notes on GitHub.](https://github.com/mesos/chronos/releases) 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Jekyll 4 | gem "jekyll" 5 | gem "jekyll-coffeescript" 6 | gem "jekyll-sass-converter" 7 | 8 | # Converters 9 | gem "kramdown" 10 | gem "maruku" 11 | gem "rdiscount" 12 | gem "redcarpet" 13 | gem "RedCloth" 14 | 15 | # Liquid 16 | gem "liquid" 17 | 18 | # Highlighters 19 | gem "pygments.rb" 20 | 21 | # Plugins 22 | #gem "jemoji" 23 | #gem "jekyll-mentions" 24 | gem "jekyll-redirect-from" 25 | gem "jekyll-sitemap" 26 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Chronos Documentation Website 2 | 3 | ## Run it locally 4 | 5 | Ensure you have installed everything listed in the dependencies section before 6 | following the instructions. 7 | 8 | ### Dependencies 9 | 10 | * [Bundler](http://bundler.io/) 11 | * [Node.js](http://nodejs.org/) (for compiling assets) 12 | * Python 13 | * Ruby 14 | * [RubyGems](https://rubygems.org/) 15 | 16 | ### Instructions 17 | 18 | 1. Install packages needed to generate the site 19 | 20 | * On Linux: 21 | 22 | $ apt-get install ruby-dev make autoconf nodejs nodejs-legacy python-dev 23 | * On Mac OS X: 24 | 25 | $ brew install node 26 | 27 | 2. Clone the Chronos repository 28 | 29 | 3. Change into the "docs" directory where docs live 30 | 31 | $ cd docs 32 | 33 | 4. Install Bundler 34 | 35 | $ gem install bundler 36 | 37 | 5. Install the bundle's dependencies 38 | 39 | $ bundle install 40 | 41 | 6. Start the web server 42 | 43 | $ bundle exec jekyll serve --watch 44 | 45 | 7. Visit the site at 46 | [http://localhost:4000/chronos/](http://localhost:4000/chronos/) 47 | 48 | ## Deploying the site 49 | 50 | 1. Clone a separate copy of the Chronos repo as a sibling of your normal 51 | Chronos project directory and name it "chronos-gh-pages". 52 | 53 | $ git clone git@github.com:mesos/chronos.git chronos-gh-pages 54 | 55 | 2. Check out the "gh-pages" branch. 56 | 57 | $ cd /path/to/chronos-gh-pages 58 | $ git checkout gh-pages 59 | 60 | 3. Copy the contents of the "docs" directory in master to the root of your 61 | chronos-gh-pages directory. 62 | 63 | $ cd /path/to/chronos 64 | $ cp -r docs/** ../chronos-gh-pages 65 | 66 | 4. Change to the chronos-gh-pages directory, commit, and push the changes 67 | 68 | $ cd /path/to/chronos-gh-pages 69 | $ git commit . -m "Syncing docs with master branch" 70 | $ git push 71 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | - 3 | scope: 4 | path: "" 5 | values: 6 | layout: default 7 | - 8 | scope: 9 | path: "docs" 10 | values: 11 | layout: docs 12 | tab: docs 13 | 14 | sass: 15 | sass_dir: _sass 16 | style: :compressed 17 | 18 | baseurl: "/chronos" 19 | highlighter: rouge 20 | lsi: false 21 | markdown: kramdown 22 | redcarpet: 23 | extensions: [with_toc_data, tables] 24 | safe: true 25 | source: . 26 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chronos: {{ page.title }} 7 | 8 | 9 | 10 | 19 | 20 | 21 |
22 |
23 |
24 | 53 | {{ content }} 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/_layouts/docs.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 |
7 |
Welcome
8 | 25 |
26 |
Developing
27 | 34 |
35 |
Reference
36 | 68 |
69 |
70 | {{ content }} 71 |
72 |
73 | -------------------------------------------------------------------------------- /docs/_layouts/narrow.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 |
7 | {{ content }} 8 |
9 |
10 | -------------------------------------------------------------------------------- /docs/_sass/_base.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Montserrat'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Montserrat-Regular'), url(https://themes.googleusercontent.com/static/fonts/montserrat/v4/zhcz-_WihjSQC0oHJ9TCYL3hpw3pgy2gAi-Ip7WPMi0.woff) format('woff'); 6 | } 7 | 8 | h1, .h1, 9 | h2, .h2, 10 | h3, .h3, 11 | h4, .h4 { 12 | font-family: Montserrat, "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | } 14 | 15 | body { 16 | background-color: #f6f6f6; 17 | color: #323539; 18 | line-height: 1.7; 19 | padding-top: 20px; 20 | } 21 | 22 | footer { 23 | margin-bottom: 20px; 24 | margin-top: 20px; 25 | } 26 | 27 | hr { 28 | border-color: #ddd; 29 | } 30 | 31 | pre { 32 | background-color: #fff; 33 | } 34 | 35 | .jumbotron { 36 | background-color: #323539; 37 | color: #f6f6f6; 38 | } 39 | 40 | .jumbotron .btn-link { 41 | color: #00b482; 42 | } 43 | 44 | .navbar { 45 | border: none; 46 | } 47 | 48 | .navbar-inverse { 49 | background-color: #2a2d2f; 50 | border-color: transparent; 51 | } 52 | 53 | .navbar-inverse .navbar-brand, 54 | .navbar-inverse .nav > li > a { 55 | color: #fff; 56 | } 57 | 58 | .navbar-inverse .navbar-nav > li > a:hover { 59 | background-color: #3e4146; 60 | } 61 | 62 | .navbar-inverse .navbar-nav > .active > a, 63 | .navbar-inverse .navbar-nav > .active > a:hover, 64 | .navbar-inverse .navbar-nav > .active > a:focus { 65 | border-bottom: 3px solid #00b482; 66 | padding-bottom: 12px; 67 | } 68 | -------------------------------------------------------------------------------- /docs/_sass/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn-primary { 2 | background-color: #00b482; 3 | border-color: #00b482; 4 | } 5 | 6 | .btn-primary:hover, 7 | .btn-primary:focus, 8 | .btn-primary:active, 9 | .btn-primary.active, 10 | .open .dropdown-toggle.btn-primary { 11 | background-color: #00da9d; 12 | border-color: #007756; 13 | } 14 | -------------------------------------------------------------------------------- /docs/_sass/_highlight.scss: -------------------------------------------------------------------------------- 1 | .highlight { background: #ffffff; } 2 | .highlight .c { color: #999988; font-style: italic } /* Comment */ 3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .highlight .k { font-weight: bold } /* Keyword */ 5 | .highlight .o { font-weight: bold } /* Operator */ 6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 14 | .highlight .gh { color: #999999 } /* Generic.Heading */ 15 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 16 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 17 | .highlight .go { color: #888888 } /* Generic.Output */ 18 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 19 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 20 | .highlight .gu { color: #aaaaaa } /* Generic.Subheading */ 21 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 22 | .highlight .kc { font-weight: bold } /* Keyword.Constant */ 23 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */ 24 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */ 25 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */ 26 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 27 | .highlight .m { color: #009999 } /* Literal.Number */ 28 | .highlight .s { color: #d14 } /* Literal.String */ 29 | .highlight .na { color: #008080 } /* Name.Attribute */ 30 | .highlight .nb { color: #0086B3 } /* Name.Builtin */ 31 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ 32 | .highlight .no { color: #008080 } /* Name.Constant */ 33 | .highlight .ni { color: #800080 } /* Name.Entity */ 34 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ 35 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ 36 | .highlight .nn { color: #555555 } /* Name.Namespace */ 37 | .highlight .nt { color: #000080 } /* Name.Tag */ 38 | .highlight .nv { color: #008080 } /* Name.Variable */ 39 | .highlight .ow { font-weight: bold } /* Operator.Word */ 40 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 41 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 42 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 43 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 44 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 45 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */ 46 | .highlight .sc { color: #d14 } /* Literal.String.Char */ 47 | .highlight .sd { color: #d14 } /* Literal.String.Doc */ 48 | .highlight .s2 { color: #d14 } /* Literal.String.Double */ 49 | .highlight .se { color: #d14 } /* Literal.String.Escape */ 50 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */ 51 | .highlight .si { color: #d14 } /* Literal.String.Interpol */ 52 | .highlight .sx { color: #d14 } /* Literal.String.Other */ 53 | .highlight .sr { color: #009926 } /* Literal.String.Regex */ 54 | .highlight .s1 { color: #d14 } /* Literal.String.Single */ 55 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */ 56 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ 57 | .highlight .vc { color: #008080 } /* Name.Variable.Class */ 58 | .highlight .vg { color: #008080 } /* Name.Variable.Global */ 59 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */ 60 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 61 | -------------------------------------------------------------------------------- /docs/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "base"; 5 | @import "buttons"; 6 | @import "highlight"; 7 | -------------------------------------------------------------------------------- /docs/docs/authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mesos Framework Authentication 3 | --- 4 | 5 | 6 | # Mesos Framework Authentication 7 | 8 | To enable framework authentication in Chronos: 9 | 10 | * Run Chronos with `--mesos_authentication_principal` set to a Mesos-authorized principal. For Mesos' built-in CRAM-MD5 authentication, you must also provide `--mesos_authentication_secret_file` pointing to a file containing your authentication secret. 11 | 12 | The secret file cannot have a trailing newline. To not add a newline simply run: 13 | 14 | ```bash 15 | $ echo -n "secret" > /path/to/secret/file 16 | ``` 17 | 18 | * If using the built-in CRAM-MD5 authentication mechanism, run `mesos-master` with the credentials flag and the path to the file with authorized users and their secrets: `--credentials=/path/to/credential/file` 19 | 20 | Note that this `--credentials` file is for all frameworks and agents registering with Mesos. In enterprise installations, the cluster admin will have already configured credentials in Mesos, so the user launching Chronos just needs to specify the principal+secret given to them by the cluster/security admin. 21 | 22 | Each line in the file should be a principal and corresponding secret separated by a single space: 23 | 24 | ```bash 25 | $ cat /path/to/credential/file 26 | principal secret 27 | principal2 secret2 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/docs/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributor Guidelines 3 | --- 4 | 5 | 6 | # Contributor Guidelines 7 | 8 | ## Submitting Changes to Chronos 9 | 10 | - A GitHub pull request is the preferred way of submitting patch sets. 11 | 12 | - Any changes in the public API or behavior must be reflected in the project 13 | documentation. 14 | 15 | - Pull requests should include appropriate additions to the unit test suite. 16 | 17 | - If the change is a bugfix, then the added tests must fail without the patch 18 | as a safeguard against future regressions. 19 | 20 | - Changes should not result in a drop in code coverage. Coverage may be 21 | checked by running `mvn scoverage:check` 22 | 23 | ## Contributing Documentation 24 | 25 | We heartily welcome contributions to Chronos's documentation. Documentation should be submitted as a pull request againstthe `master` branch and published to our GitHub pages site by a Chronos committer using the instructions in [docs/README.md](https://github.com/mesos/chronos/tree/master/docs). 26 | 27 | For development, you can use [dcos-vagrant](https://github.com/dcos/dcos-vagrant) to test Chronos. On macOS, you can install libmesos with `brew install mesos`, and start Chronos against a DC/OS vagrant installation like so: 28 | 29 | $ java -Djava.library.path=/usr/local/lib -jar target/chronos-3.0.0-SNAPSHOT.jar --zk_hosts 192.168.65.90:2181 --master zk://192.168.65.90:2181/mesos 30 | -------------------------------------------------------------------------------- /docs/docs/debugging.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debugging 3 | --- 4 | 5 | ### Debugging Chronos 6 | 7 | Chronos uses log4j to control log output. To override the standard log4j configuration, 8 | create a [log4j configuration file](http://logging.apache.org/log4j/1.2/manual.html) and 9 | add `-Dlog4j.configuration=file:` to the Chronos startup command. 10 | 11 | ### Debugging Individual Jobs 12 | Individual jobs log with their task id on the mesos agents. 13 | Look in the standard out log for your job name and the string "ready for launch", or else "job ct:" and your job name. 14 | The job is done when the line in the log says: 15 | 16 | `Task with id 'value: TASK_ID' **FINISHED**` 17 | 18 | To find debug logs on the mesos agent, look in `/tmp/mesos/slaves` on the slave instance (unless you've specifically supplied a different log folder for mesos). For example: 19 | 20 | `/tmp/mesos/agents/` 21 | 22 | In that dir, the current agent run is timestamped so look for the most recent. 23 | Under that is a list of frameworks; you're interested in the Chronos framework. 24 | For example: 25 | 26 | `/tmp/mesos/agents//frameworks/` 27 | -------------------------------------------------------------------------------- /docs/docs/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Frequently Asked Questions 3 | --- 4 | 5 | 6 | # Frequently Asked Questions 7 | 8 | 9 | * [How do I find which Chronos node to talk to?](#which-node) 10 | * [How does Chronos use ZooKeeper?](#chronos-zookeeper) 11 | * [How does Chronos use Cassandra?](#chronos-cassandra) 12 | * [[osx] making Mesos fails on `warning: 'JNI_CreateJavaVM' is deprecated`](#osx-mesos) 13 | * [Running Chronos fails with the error: java.lang.UnsatisfiedLinkError: org.apache.mesos.state.AbstractState.__fetch(Ljava/lang/String;)J](#running-chronos) 14 | * [My Web UI is not showing up!](#web-ui) 15 | * [When running jobs locally I get an error like `Failed to execute 'chown -R'`](#running-jobs-locally) 16 | * [I found a bug!](#bug) 17 | 18 | ### How do I find which Chronos node to talk to? 19 | 20 | Chronos is designed (not required) to run with multiple nodes of which one is elected master. 21 | If you use the cURL command line tool, you can use the `-L` flag and hit any Chronos node and you will get a 22 | 307 REDIRECT to the leader. 23 | 24 | 25 | ### How does Chronos use ZooKeeper? 26 | 27 | Chronos registers itself with [ZooKeeper][ZooKeeper] at the location `/chronos/state`. This value can be changed via the configuration file. 28 | 29 | ### How does Chronos use Cassandra? 30 | 31 | Chronos can optionally use [Cassandra] for job history, reporting and statistics. By default, Chronos attempts to connect to the `metrics` keyspace. 32 | To use this feature, you must at a minimum: 33 | 34 | 1. Create a keyspace (named `metrics` and configurable with `--cassandra_keyspace`) 35 | ```sql 36 | CREATE KEYSPACE IF NOT EXISTS metrics 37 | WITH REPLICATION = { 38 | 'class' : 'SimpleStrategy', 'replication_factor' : 3 39 | }; 40 | ``` 41 | 1. Pass the `--cassandra_contact_points` flag to Chronos with a comma-separated list of Cassandra contact points 42 | 43 | ### [osx] Making Mesos fails on deprecated header warning 44 | 45 | Error message such as: 46 | 47 | conftest.cpp:7: warning: 'JNI_CreateJavaVM' is deprecated (declared at /System/Library/Frameworks/JavaVM.framework/Headers/jni.h:1937) 48 | 49 | This error is the result of OSX shipping with an outdated version of the JDK and associated libraries. To resolve this issue, do the following. 50 | 51 | 1. [Download](http://www.oracle.com/technetwork/java/javase/downloads/index.html) and install JDK7. 52 | 2. Set JDK7 as active: 53 | `export JAVA_HOME=$(/usr/libexec/java_home -v 1.7)` 54 | **Note:** Stick this in your `~/.*rc` to always use 1.7 55 | 3. Find your JNI headers, these should be in `$JAVA_HOME/include` and `$JAVA_HOME/include/darwin`. 56 | 4. Configure mesos with `JAVA_CPPFLAGS` set to the JNI path. 57 | 58 | **Example Assumptions:** 59 | 60 | * `$JAVA_HOME` in this example is `/Library/Java/JavaVirtualMachines/jdk1.7.0_12.jdk/Contents/Home` 61 | * The current working directory is `mesos/build` as advised by the [mesos README](https://github.com/apache/mesos/blob/trunk/README#L13) 62 | 63 | **Example:** 64 | 65 | JAVA_CPPFLAGS='-I/Library/Java/JavaVirtualMachines/jdk1.7.0_12.jdk/Contents/Home/include/ -I/Library/Java/JavaVirtualMachines/jdk1.7.0_12.jdk/Contents/Home/include/darwin/' ../configure 66 | 67 | ### Running Chronos fails with the error: `java.lang.UnsatisfiedLinkError: org.apache.mesos.state.AbstractState.__fetch(Ljava/lang/String;)J` 68 | 69 | This means you're using a mesos-jar file that is incompatible with the version of Mesos you're running. 70 | If you want to run chronos with a different version of mesos than in the pom.xml file, override the version by issuing `mvn package -Dmesos.version=0.14.0-rc4`. 71 | Please note, this must be a jar file version that's available from one of the repositories listed in the pom.xml file. 72 | 73 | 74 | ### My Web UI is not showing up! 75 | 76 | For asset bundling, you need node installed. If you're seeing a 403 when trying to access the web-ui, it's likely that node was not present during the `mvn package` step. 77 | 78 | See [docs/webui.md](http://mesos.github.io/chronos/docs/webui.html). 79 | 80 | ### When running jobs locally I get an error like `Failed to execute 'chown -R'` 81 | 82 | If you get an error such as: 83 | 84 | Failed to execute 'chown -R 0:0 '/tmp/mesos/agents/executors/...' ... Undefined error: 0 85 | Failed to launch executor` 86 | 87 | You can try starting your mesos agents with switch users disabled. To do this, start your slaves in the following manner: 88 | 89 | MESOS_SWITCH_USER=0 bin/mesos-agent.sh --master=zk://localhost:2181/mesos --resources="cpus:8,mem:68551;disk:803394" 90 | 91 | ### I found a bug! 92 | 93 | Please see the [support page](http://mesos.github.io/chronos/support.html) for information on how to report bugs. 94 | 95 | [ZooKeeper]: https://zookeeper.apache.org/ 96 | [Cassandra]: http://cassandra.apache.org 97 | -------------------------------------------------------------------------------- /docs/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting Up and Running Chronos 3 | --- 4 | 5 | # Setting up and Running Chronos 6 | 7 | ## Quickstart 8 | 9 | The [dcos-vagrant](https://github.com/dcos/dcos-vagrant) provides an easy way to try DC/OS and Chronos within a virtual machine using Vagrant. 10 | 11 | ## Requirements 12 | 13 | These requirements are just to run Chronos. You will need additional packages to build Chronos from source (see the [Building from Source section](#build-from-source) below). 14 | 15 | * [Apache Mesos][Mesos] 1.0.0+ 16 | * [Apache ZooKeeper][ZooKeeper] 17 | * JDK 1.8+ 18 | 19 | 20 | ## Install from Packages 21 | 22 | Mesosphere provides Docker images for Chronos, available from Docker hub at . 23 | 24 | ## Building from Source 25 | 26 | Follow these steps to build Chronos from source. This configuration assumes you already have Mesos installed on the same host (see Mesosphere link above to get a Mesos package). 27 | 28 | ## Requirements 29 | 30 | These requirements are to build and run Chronos. 31 | 32 | * [Apache Mesos][Mesos] 1.0.0+ 33 | * [Apache ZooKeeper][ZooKeeper] 34 | * JDK 1.8+ 35 | * [Maven 3+](https://maven.apache.org/download.cgi) 36 | * NodeJS 7+ 37 | 38 | 39 | ### Build Chronos 40 | 41 | Install [Node](http://nodejs.org/) first. On OSX, try `brew install node`. 42 | 43 | Start up Zookeeper, Mesos master, and Mesos agent(s). Then try 44 | 45 | export MESOS_NATIVE_LIBRARY=/usr/local/lib/libmesos.so 46 | git clone https://github.com/mesos/chronos.git 47 | cd chronos 48 | mvn package 49 | java -cp target/chronos*.jar org.apache.mesos.chronos.scheduler.Main --master zk://localhost:2181/mesos --zk_hosts localhost:2181 50 | 51 | ### Environment Variables Mesos Looks For 52 | 53 | * `MESOS_NATIVE_LIBRARY`: Absolute path to the native mesos library. This is usually `/usr/local/lib/libmesos.so` on Linux and `/usr/local/lib/libmesos.dylib` on OSX. 54 | 55 | If you're using the installer script this should be setup for you. 56 | 57 |
58 | 59 | ## Running Chronos 60 | 61 | The basic syntax for launching chronos is: 62 | 63 | java -jar chronos.jar --master zk://127.0.0.1:2181/mesos --zk_hosts 127.0.0.1:2181 64 | 65 | Please note that you need to have both Mesos and Zookeeper running for this to work! 66 | 67 | For more information on configuration options, please see [Configuration]({{ site.baseurl }}/docs/configuration.html). 68 | 69 | [Mesos]: https://mesos.apache.org/ "Apache Mesos" 70 | [Zookeeper]: https://zookeeper.apache.org/ "Apache ZooKeeper" 71 | -------------------------------------------------------------------------------- /docs/docs/job-management.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Job Management 3 | --- 4 | 5 | ## Job Management 6 | 7 | For larger installations, the web UI may be insufficient for managing jobs. At 8 | Airbnb, there are well over 700 production Chronos jobs. Rather than using the 9 | web UI for making edits, we created a script called `chronos-sync.rb` which can 10 | be used to synchronize configuration from disk to Chronos. For example, you 11 | may have a Git repository that contains all of the Chronos job configurations, 12 | and then you could run an hourly Chronos job that checks out the repository and 13 | runs `chronos-sync.rb`. 14 | 15 | You can initialize the configuration data by running: 16 | ``` 17 | $ bin/chronos-sync.rb -u http://chronos/ -p /path/to/jobs/config -c 18 | ``` 19 | 20 | After that, you can run the normal sync like this: 21 | 22 | ``` 23 | $ bin/chronos-sync.rb -u http://chronos/ -p /path/to/jobs/config 24 | ``` 25 | 26 | You can also forcefully update the configuration in Chronos from disk by 27 | passing the `-f` or `--force` parameter. In the example above, 28 | `/path/to/jobs/config` is the path where you would like the configuration data 29 | to live. 30 | 31 | Note: `chronos-sync.rb` does not delete jobs by default. You can pass the `--delete-missing` flag to `chronos-sync.rb` to remove jobs. Alternatively, you can manually remove it using the API or web UI. 32 | -------------------------------------------------------------------------------- /docs/docs/webui.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Web UI 3 | --- 4 | 5 | 6 | # Chronos Web UI 7 | 8 | ### Compiling Assets 9 | 10 | **Node.js is required to build assets** 11 | 12 | Assets are automatically compiled when running `mvn package`. If you change assets, and want them updated in your jar, you must either `rm -rf src/main/resources/ui/build` or `mvn clean`. 13 | 14 | ### Build Requirements 15 | 16 | When building and optimizing the assets, make sure you have at least 1GB of RAM available. 17 | -------------------------------------------------------------------------------- /docs/img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/docs/img/.gitkeep -------------------------------------------------------------------------------- /docs/img/emr_use_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/docs/img/emr_use_case.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fault tolerant job scheduler for Mesos 3 | --- 4 | 5 |
6 |

Chronos

7 |

8 | A fault tolerant job scheduler for Mesos which handles dependencies and ISO8601 based schedules 9 |

10 |
11 | 12 | ## Overview 13 | 14 | Chronos is a replacement for `cron`. It is a distributed and fault-tolerant scheduler that runs on top of [Apache Mesos][mesos] that can be used for job orchestration. It supports custom Mesos executors as well 15 | as the default command executor. Thus by default, Chronos executes `sh` 16 | (on most systems bash) scripts. 17 | 18 | Chronos can be used to interact with systems such as Hadoop (incl. EMR), even if the Mesos agents on which execution happens do not have Hadoop installed. Chronos is also natively able to schedule jobs that run inside Docker containers. 19 | 20 | Chronos has a number of advantages over regular cron. 21 | It allows you to schedule your jobs using [ISO8601][ISO8601] repeating interval notation, which enables more flexibility in job scheduling. Chronos also supports the definition of jobs triggered by the completion of other jobs. It supports arbitrarily long dependency chains. 22 | 23 | ## Chronos: How does it work? 24 | 25 | Chronos is a Mesos scheduler for running schedule and dependency based jobs. Scheduled jobs are configured with ISO8601-based schedules with repeating intervals. Typically, a job is scheduled to run indefinitely, such as once per day or per hour. Dependent jobs may have multiple parents, and will be triggered once all parents have been successfully invoked at least once since the last invocation of the dependent job. 26 | 27 | Internally, the Chronos scheduler main loop is quite simple. The pattern is as follows: 28 | 29 | 1. Chronos reads all job state from the state store (ZooKeeper) 30 | 1. Jobs are registered within the scheduler and loaded into the job graph for tracking dependencies. 31 | 1. Jobs are separated into a list of those which should be run at the current time (based on the clock of the host machine), and those which should not. 32 | 1. Jobs in the list of jobs to run are queued, and will be launched as soon as a sufficient offer becomes available. 33 | 1. Chronos will sleep until the next job is scheduled to run, and begin again from step 1. 34 | 35 | Furthermore, a dependent job will be queued for execution once all parents have successfully completed at least once since the last time it ran. After the dependent job runs, the cycle resets. 36 | 37 | This code lives within the `mainLoop()` method, [and can be found here][mainLoop]. 38 | 39 | Additionally, Chronos has a number of advanced features to help you build whatever it is you may be trying to. It can: 40 | 41 | - Write job metrics to Cassandra for further analysis, validation, and party favours 42 | - Send notifications to various endpoints such as email, Slack, and others 43 | - Export metrics to graphite and elsewhere 44 | 45 | Chronos cannot: 46 | 47 | - Magically solve all distributed computing problems for you 48 | - Guarantee precise scheduling 49 | - Guarantee clock synchronization 50 | - Guarantee that jobs actually run 51 | 52 | For the items listed above, you must figure this out yourself. 53 | 54 | ## Sample Architecture 55 | 56 | ![architecture]({{site.baseurl}}/img/emr_use_case.png "sample architecture") 57 | 58 | 59 | ## Chronos UI 60 | 61 | Chronos comes with a UI which can be used to add, delete, list, modify and run jobs. It can also show a graph of job dependencies. 62 | The screenshot should give you a good idea of what Chronos can do. 63 | 64 | Additionally, Chronos can show statistics on past job execution. This may include aggregate statistics such as number of 65 | successful and failed executions. Per job execution statistics (i.e. duration and status) are also available, if a 66 | [Cassandra cluster](https://github.com/mesosphere/cassandra-mesos/) is attached to Chronos. Please see the [Configuration 67 | ]({{ site.baseurl }}/docs/configuration.html) section 68 | on how to do this. 69 | 70 | ## Installation 71 | 72 | Chronos can be installed on DC/OS using the following command: 73 | 74 | $ dcos package install chronos 75 | 76 | Additionally, Mesosphere publishes public Docker images for Chronos. Images are available at . To run Chronos with Docker, you must have 2 ports available: one for the HTTP API, and one for libprocess. You must export these ports as environment variables for Chronos to start. For example: 77 | 78 | $ docker run --net=host -e PORT0=8080 -e PORT1=8081 mesosphere/chronos:v3.0.0 --zk_hosts 192.168.65.90:2181 --master zk://192.168.65.90:2181/mesos 79 | 80 | 81 | [ISO8601]: http://en.wikipedia.org/wiki/ISO_8601 "ISO8601 Standard" 82 | [mesos]: https://mesos.apache.org/ "Apache Mesos" 83 | [mainLoop]: https://github.com/mesos/chronos/blob/be96c4540b331b08d9742442e82c4516b4eaee85/src/main/scala/org/apache/mesos/chronos/scheduler/jobs/JobScheduler.scala#L469-L498 84 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: narrow 3 | tab: resources 4 | title: Chronos Resources 5 | --- 6 | 7 | # Chronos Resources 8 | 9 | ## Blog Posts 10 | 11 | * [Integration testing with Mesos, Chronos and Docker](https://mesosphere.com/blog/2015/03/26/integration-testing-with-mesos-chronos-docker/) 12 | 13 | ## Videos 14 | 15 | * [Containerize Your Batch Jobs with Mesosphere and Docker](https://mesosphere.com/blog/2014/12/03/docker-on-mesos-with-chronos/) 16 | 17 | * Replacing Cron & Building Scalable Data Pipelines: [YouTube](http://www.youtube.com/watch?v=FLqURrtS8IA) 18 | 19 | ## Libraries 20 | 21 | * [Go Chronos Client](https://github.com/yieldbot/chronos-client) 22 | * [Python Chronos Client](https://github.com/asher/chronos-python) 23 | 24 | ## Tools 25 | 26 | * [Chronos Shuttle](https://github.com/yieldbot/chronos-shuttle) An opinionated CLI for Chronos 27 | -------------------------------------------------------------------------------- /docs/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: narrow 3 | tab: support 4 | title: Getting Support for Chronos 5 | --- 6 | 7 | # Getting Support for Chronos 8 | 9 | For questions and discussions around Chronos, please use the Google Group "chronos-scheduler": 10 | [Chronos Scheduler Group](https://groups.google.com/forum/#!forum/chronos-scheduler). 11 | 12 | Also join us on IRC in #mesos on freenode. 13 | 14 | ## Reporting Bugs 15 | 16 | Bugs can be reported by creating a [new GitHub issue](https://github.com/mesos/chronos/issues/new). 17 | 18 | To make all of our lives easier we ask that all bug reports 19 | include at least the following information: 20 | 21 | The output of: 22 | 23 | mvn -X clean package 24 | 25 | and 26 | 27 | java -version 28 | 29 | If the error is in running tests, then please include the output of 30 | running all the tests. 31 | 32 | # Mac/FreeBSD 33 | tail +1 target/surefire-reports/*.txt 34 | # GNU Coreutils 35 | tail -n +1 target/surefire-reports/*.txt 36 | 37 | If the error is in the installer, please include all 38 | the output from running it with debug enabled: 39 | 40 | bash -x bin/installer.bash 41 | 42 | If the bug is in building Mesos from scratch, please [submit those bugs directly to the Apache Mesos JIRA](https://issues.apache.org/jira/browse/MESOS). 43 | 44 | If the bug occurs while running Chronos, please include the following 45 | information: 46 | 47 | * The command used to launch Chronos, for example: 48 | 49 | java -cp target/chronos.jar org.apache.mesos.chronos.scheduler.Main 50 | 51 | * The version of Mesos you are running. 52 | 53 | * The output of 54 | 55 | java -version -------------------------------------------------------------------------------- /integration-tests/.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | Dockerfile 3 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y --force-yes ruby ruby-dev build-essential \ 5 | && gem install --no-ri --no-rdoc cassandra-driver \ 6 | && apt-get purge -y --auto-remove build-essential \ 7 | && apt-get clean \ 8 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 9 | 10 | ADD https://raw.githubusercontent.com/mesos/chronos/master/bin/chronos-sync.rb /chronos/chronos-sync.rb 11 | COPY . /chronos 12 | -------------------------------------------------------------------------------- /integration-tests/check-results.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'cassandra' 4 | require 'securerandom' 5 | 6 | CASSANDRA_HOSTS = ['node-0.cassandra.mesos', 'node-1.cassandra.mesos', 'node-2.cassandra.mesos'] 7 | CASSANDRA_PORT = 9042 8 | 9 | cluster = Cassandra.cluster(hosts: CASSANDRA_HOSTS, port: CASSANDRA_PORT) 10 | 11 | session = cluster.connect 12 | 13 | exit_code = 0 14 | 15 | future = session.execute_async( 16 | 'SELECT job_name, ts, task_state FROM metrics.chronos WHERE ts >= ? ALLOW FILTERING', 17 | arguments: [(DateTime.now - 7).to_time] 18 | ) 19 | result = [] 20 | future.on_success do |rows| 21 | rows.each do |row| 22 | result.push({ 23 | :job_name => row['job_name'], 24 | :ts => row['ts'], 25 | :task_state => row['task_state'], 26 | }) 27 | end 28 | end 29 | future.join 30 | 31 | grouped = result.group_by {|r| r[:job_name]} 32 | 33 | def check_count_equals(count, expected, name, state) 34 | if count != expected 35 | puts "State count for name=#{name} and state=#{state} didn't match expected value (got #{count}, expected #{expected}" 36 | return true 37 | end 38 | false 39 | end 40 | 41 | def check_count_at_least(count, expected, name, state) 42 | if count < expected 43 | puts "State count for name=#{name} and state=#{state} didn't match expected >= value (got #{count}, expected #{expected}" 44 | return true 45 | end 46 | false 47 | end 48 | 49 | def get_expected(name) 50 | if name.include?('hourly') 51 | 24*7 52 | elsif name.include?('daily') 53 | 7 54 | elsif name.include?('weekly') 55 | 1 56 | else 57 | 0 58 | end 59 | end 60 | 61 | had_error = false 62 | grouped.each do |name, result| 63 | states = result.group_by {|r| r[:task_state]} 64 | counts = states.map{|k, v| {:state => k, :count =>v.size}} 65 | puts "Summary for #{name}:" 66 | puts counts 67 | expected = get_expected(name) 68 | next if expected == 0 69 | counts.each do |v| 70 | if v[:state] == 'TASK_FINISHED' 71 | if check_count_equals(v[:count], expected, name, v[:state]) 72 | had_error = true 73 | end 74 | elsif v[:state] == 'TASK_RUNNING' 75 | if check_count_at_least(v[:count], expected, name, v[:state]) 76 | had_error = true 77 | end 78 | end 79 | end 80 | end 81 | 82 | if had_error 83 | exit_code = 1 84 | end 85 | 86 | session.close 87 | 88 | exit exit_code 89 | -------------------------------------------------------------------------------- /integration-tests/jobs/dependent/hourly-dependent-sleep1m.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: hourly-dependent-sleep1m 3 | command: sleep 1m 4 | parents: 5 | - hourly-sleep1m 6 | owner: nobody 7 | cpus: 0.01 8 | mem: 32 9 | disk: 0 10 | runAsUser: root 11 | -------------------------------------------------------------------------------- /integration-tests/jobs/scheduled/daily-sleep1m.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: daily-sleep1m 3 | command: sleep 1m 4 | schedule: R/2016-12-05T22:00:00.000Z/P1D 5 | owner: nobody 6 | cpus: 0.01 7 | mem: 32 8 | disk: 0 9 | runAsUser: root 10 | -------------------------------------------------------------------------------- /integration-tests/jobs/scheduled/hourly-sleep1m.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: hourly-sleep1m 3 | command: sleep 1m 4 | schedule: R/2016-12-05T22:00:00.000Z/PT1H 5 | owner: nobody 6 | cpus: 0.01 7 | mem: 32 8 | disk: 0 9 | runAsUser: root 10 | -------------------------------------------------------------------------------- /integration-tests/jobs/scheduled/weekly-sleep1m.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: weekly-sleep1m 3 | command: sleep 1m 4 | schedule: R/2016-12-05T22:00:00.000Z/P1W 5 | owner: nobody 6 | cpus: 0.01 7 | mem: 32 8 | disk: 0 9 | runAsUser: root 10 | -------------------------------------------------------------------------------- /src/main/resources/jetty-logging.properties: -------------------------------------------------------------------------------- 1 | # Setup logging implementation 2 | org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog 3 | org.eclipse.jetty.LEVEL=INFO 4 | # Make websocket more verbose for testing 5 | org.eclipse.jetty.websocket.LEVEL=INFO 6 | -------------------------------------------------------------------------------- /src/main/resources/logging.properties: -------------------------------------------------------------------------------- 1 | handlers=java.util.logging.ConsoleHandler 2 | java.util.logging.ConsoleHandler.level=INFO 3 | java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter 4 | root.level=INFO 5 | -------------------------------------------------------------------------------- /src/main/resources/ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", "react", "stage-1" 4 | ], 5 | "plugins": ["transform-react-jsx","transform-decorators-legacy"] 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/ui/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "components" 3 | } 4 | -------------------------------------------------------------------------------- /src/main/resources/ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | -------------------------------------------------------------------------------- /src/main/resources/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "quotes": [2, "single"], 13 | "strict": [2, "never"], 14 | "react/jsx-uses-react": 2, 15 | "react/jsx-uses-vars": 2, 16 | "react/react-in-jsx-scope": 2 17 | }, 18 | "plugins": [ 19 | "react" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/ui/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /src/main/resources/ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | dist 5 | bundle.js 6 | -------------------------------------------------------------------------------- /src/main/resources/ui/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "newcap": false 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/ui/.npmignore: -------------------------------------------------------------------------------- 1 | intermediate/ 2 | publish/ 3 | -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/RobotoMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/RobotoMono-Bold.ttf -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesos/chronos/c1348ddfd80eb8f888418805005066c230fc6ad2/src/main/resources/ui/assets/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/main/resources/ui/bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | 5 | var babelrc = JSON.parse(fs.readFileSync('./.babelrc')); 6 | require('babel-register')(babelrc); 7 | 8 | global.__CLIENT__ = false; 9 | global.__SERVER__ = true; 10 | 11 | require('../server'); 12 | -------------------------------------------------------------------------------- /src/main/resources/ui/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Footer = () => ( 4 | 7 | ) 8 | 9 | export default Footer 10 | -------------------------------------------------------------------------------- /src/main/resources/ui/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Header = ({ title }) => ( 4 | 11 | ) 12 | 13 | export default Header 14 | -------------------------------------------------------------------------------- /src/main/resources/ui/components/Main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import JobSummaryView from './JobSummaryView' 3 | import JobEditor from './JobEditor' 4 | import {observer} from 'mobx-react' 5 | 6 | @observer 7 | export default class Main extends React.Component { 8 | derp(event) { 9 | console.log(event) 10 | } 11 | render() { 12 | const jobSummaryStore = this.props.jobSummaryStore 13 | return ( 14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
SUCCESS
22 |
{jobSummaryStore.successCount}
23 |
FAILURE
24 |
{jobSummaryStore.failureCount}
25 |
FRESH
26 |
{jobSummaryStore.freshCount}
27 |
RUNNING
28 |
{jobSummaryStore.runningCount}
29 |
QUEUED
30 |
{jobSummaryStore.queuedCount}
31 |
IDLE
32 |
{jobSummaryStore.idleCount}
33 |
34 |
35 | 36 |
37 |
38 | ) 39 | } 40 | 41 | getVisibleJobs() { 42 | return this.props.jobSummaryStore.jobSummarys 43 | } 44 | } 45 | 46 | Main.propTypes = { 47 | jobSummaryStore: React.PropTypes.object.isRequired, 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/ui/components/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from './Header' 3 | import Main from './Main' 4 | import Footer from './Footer' 5 | import {render} from 'react-dom' 6 | import {JobSummaryStore} from '../stores/JobSummaryStore' 7 | 8 | var observableJobSummaryStore = new JobSummaryStore() 9 | 10 | class Root extends React.Component { 11 | render () { 12 | const jobSummaryStore = observableJobSummaryStore 13 | return ( 14 |
15 |
16 |
17 |
18 |
19 | ) 20 | } 21 | } 22 | 23 | render(, document.getElementById('root')); 24 | -------------------------------------------------------------------------------- /src/main/resources/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | CHRONOS 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/ui/models/JobSummaryModel.js: -------------------------------------------------------------------------------- 1 | import {observable, computed} from 'mobx'; 2 | 3 | export default class JobSummaryModel { 4 | name; 5 | @observable status; 6 | @observable state; 7 | @observable schedule; 8 | @observable parents; 9 | @observable disabled; 10 | 11 | constructor(store, name, status, state, schedule, parents, disabled) { 12 | this.store = store 13 | this.name = name 14 | this.status = status 15 | this.state = state 16 | this.schedule = schedule 17 | this.parents = parents 18 | this.disabled = disabled 19 | } 20 | 21 | @computed get nextExpected() { 22 | if (!this.schedule) { 23 | return '—' 24 | } else { 25 | var scheduledDate = Date.parse(this.schedule.split('/')[1]) 26 | var dateDiff = (scheduledDate - new Date().getTime()) / 1000. 27 | if (dateDiff <= 1) { 28 | return 'OVERDUE' 29 | } else if (dateDiff <= 3600) { 30 | var minutes = Math.ceil(dateDiff / 60.0) 31 | if (minutes == 1) { 32 | return 'in <' + minutes + ' minute' 33 | } else { 34 | return 'in ~' + minutes + ' minutes' 35 | } 36 | } else if (dateDiff <= 86400) { 37 | var hours = Math.ceil(dateDiff / 3600.0) 38 | return 'in ~' + hours + ' hours' 39 | } else { 40 | var days = Math.ceil(dateDiff / 86400.0) 41 | return 'in ~' + days + ' days' 42 | } 43 | } 44 | } 45 | 46 | destroy() { 47 | this.store.removeJobSummary(this) 48 | } 49 | 50 | updateFromJson(json) { 51 | this.name = json.name 52 | this.status = json.status 53 | this.state = json.state 54 | this.schedule = json.schedule 55 | this.parents = json.parents 56 | this.disabled = json.disabled 57 | } 58 | 59 | static fromJS(store, json) { 60 | return new JobSummaryModel( 61 | store, 62 | json.name, 63 | json.state, 64 | json.status, 65 | json.schedule, 66 | json.parents, 67 | json.disabled 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/ui/models/JsonModel.js: -------------------------------------------------------------------------------- 1 | import {observable, computed} from 'mobx'; 2 | 3 | export default class JsonModel { 4 | name 5 | @observable json 6 | 7 | constructor(store, name, json) { 8 | this.store = store 9 | this.name = name 10 | 11 | // remove specific fields 12 | delete json['lastError'] 13 | delete json['lastSuccess'] 14 | delete json['errorsSinceLastSuccess'] 15 | delete json['successCount'] 16 | delete json['errorCount'] 17 | 18 | this.json = json 19 | } 20 | 21 | static fromJS(store, json) { 22 | return new JsonModel( 23 | store, 24 | json.name, 25 | json 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chronos-ui", 3 | "version": "1.0.0", 4 | "description": "Chronos UI", 5 | "author": "Brenden Matthews ", 6 | "license": "Apache-2.0", 7 | "bugs": { 8 | "url": "https://github.com/mesos/chronos/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mesos/chronos.git" 13 | }, 14 | "homepage": "https://github.com/mesos/chronos", 15 | "scripts": { 16 | "start": "node bin/server.js", 17 | "dev": "webpack --progress --colors", 18 | "build": "webpack --progress --colors --optimize-minimize --optimize-occurrence-order --optimize-dedupe --config webpack-release.config.js --bail", 19 | "lint": "eslint --fix ./" 20 | }, 21 | "devDependencies": { 22 | "babel-cli": "^6.18.0", 23 | "babel-core": "^6.18.2", 24 | "babel-eslint": "^7.1.1", 25 | "babel-loader": "^6.2.7", 26 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 27 | "babel-plugin-transform-react-jsx": "^6.8.0", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "babel-preset-stage-1": "^6.16.0", 31 | "babel-register": "^6.18.0", 32 | "body-parser": "^1.15.2", 33 | "bootstrap": "^3.3.7", 34 | "brace": "^0.9.1", 35 | "eslint": "^3.10.2", 36 | "eslint-plugin-react": "^6.7.1", 37 | "express": "^4.14.0", 38 | "jquery": "^3.1.1", 39 | "mobx": "^2.6.2", 40 | "mobx-react": "^3.5.9", 41 | "mobx-react-form": "^1.13.28", 42 | "react": "^15.4.0", 43 | "react-ace": "^4.1.0", 44 | "react-dom": "^15.4.0", 45 | "react-select": "^1.0.0-rc.2", 46 | "validatorjs": "^3.8.0", 47 | "webpack": "^1.13.3", 48 | "webpack-dev-middleware": "^1.8.4", 49 | "webpack-dev-server": "^1.16.2", 50 | "webpack-hot-middleware": "^2.13.2" 51 | }, 52 | "dependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/ui/stores/JobSummaryStore.js: -------------------------------------------------------------------------------- 1 | import {observable, computed, autorun} from 'mobx'; 2 | import JobSummaryModel from '../models/JobSummaryModel' 3 | import $ from 'jquery' 4 | 5 | export class JobSummaryStore { 6 | @observable jobSummarys = new Array 7 | @observable isLoading = true 8 | 9 | constructor() { 10 | this.loadJobSummarys(); 11 | } 12 | 13 | @computed get successCount() { 14 | return this.jobSummarys.reduce( 15 | (sum, job) => sum + (job.status === 'success' ? 1 : 0), 16 | 0 17 | ) 18 | } 19 | 20 | @computed get failureCount() { 21 | return this.jobSummarys.reduce( 22 | (sum, job) => sum + (job.status === 'failure' ? 1 : 0), 23 | 0 24 | ) 25 | } 26 | 27 | @computed get freshCount() { 28 | return this.jobSummarys.reduce( 29 | (sum, job) => sum + (job.status === 'fresh' ? 1 : 0), 30 | 0 31 | ) 32 | } 33 | 34 | @computed get queuedCount() { 35 | return this.jobSummarys.reduce( 36 | (sum, job) => sum + (job.state === 'queued' ? 1 : 0), 37 | 0 38 | ) 39 | } 40 | 41 | @computed get idleCount() { 42 | return this.jobSummarys.reduce( 43 | (sum, job) => sum + (job.state === 'idle' ? 1 : 0), 44 | 0 45 | ) 46 | } 47 | 48 | @computed get runningCount() { 49 | return this.jobSummarys.reduce( 50 | (sum, job) => sum + (job.state.match(/\d+ running/) ? 1 : 0), 51 | 0 52 | ) 53 | } 54 | 55 | @computed get jobNames() { 56 | var names = [] 57 | this.jobSummarys.forEach(j => { 58 | names.push(j.name) 59 | }) 60 | return names 61 | } 62 | 63 | /** 64 | * Update a jobSummary with information from the server. Guarantees a jobSummary 65 | * only exists once. Might either construct a new jobSummary, update an existing one, 66 | * or remove a jobSummary if it has been deleted on the server. 67 | */ 68 | updateJobSummaryFromServer(json) { 69 | var jobSummary = this.jobSummarys.find(jobSummary => jobSummary.name === json.name); 70 | if (!jobSummary) { 71 | jobSummary = JobSummaryModel.fromJS(this, json); 72 | this.jobSummarys.push(jobSummary); 73 | } 74 | jobSummary.updateFromJson(json); 75 | } 76 | 77 | /** 78 | * Fetches all jobSummary's from the server 79 | */ 80 | loadJobSummarys() { 81 | this.isLoading = true; 82 | var otherThis = this; 83 | $.getJSON('v1/scheduler/jobs/summary').done(function(resp) { 84 | var serverJobNames = new Set(); 85 | resp.jobs.forEach(json => { 86 | serverJobNames.add(json.name) 87 | otherThis.updateJobSummaryFromServer(json) 88 | }); 89 | 90 | // Check for jobs which exist here, but not on the server 91 | otherThis.jobSummarys.filter(function(j) { 92 | return !serverJobNames.has(j.name) 93 | }).forEach(j => j.destroy()) 94 | 95 | otherThis.isLoading = false; 96 | }).fail(function() { 97 | otherThis.isLoading = false; 98 | }); 99 | setTimeout(function() { 100 | otherThis.loadJobSummarys(); 101 | }, 2000); 102 | } 103 | 104 | /** 105 | * A jobSummary was somehow deleted, clean it from the client memory 106 | */ 107 | removeJobSummary(jobSummary) { 108 | this.jobSummarys.splice(this.jobSummarys.indexOf(jobSummary), 1); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/resources/ui/stores/JsonStore.js: -------------------------------------------------------------------------------- 1 | import {observable} from 'mobx' 2 | import JsonModel from '../models/JsonModel' 3 | 4 | export class JsonStore { 5 | @observable isLoading = false 6 | @observable job = null 7 | @observable submitError = "" 8 | @observable submitStatus = "" 9 | @observable value = "" 10 | 11 | loadJob(jobName) { 12 | this.isLoading = true 13 | var otherThis = this 14 | $.getJSON('v1/scheduler/job/' + encodeURIComponent(jobName)).done(function(resp) { 15 | var serverJobNames = new Set() 16 | otherThis.job = JsonModel.fromJS(this, resp) 17 | otherThis.isLoading = false 18 | }).fail(function() { 19 | otherThis.isLoading = false 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/ui/webpack-release.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | './components/Root.js' 8 | ], 9 | output: { 10 | path: path.join(__dirname, 'build/assets/js'), 11 | filename: 'bundle.js', 12 | publicPath: '/assets/', 13 | }, 14 | plugins: [ 15 | new webpack.optimize.OccurenceOrderPlugin(), 16 | new webpack.NoErrorsPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV) }, 19 | __CLIENT__: JSON.stringify(true), 20 | __SERVER__: JSON.stringify(false), 21 | }), 22 | new webpack.ProvidePlugin({ 23 | jQuery: 'jquery', 24 | $: 'jquery', 25 | jquery: 'jquery' 26 | }), 27 | ], 28 | resolve: { 29 | extensions: ['', '.js', '.jsx'] 30 | }, 31 | module: { 32 | loaders: [{ 33 | test: /\.jsx?$/, 34 | loader: 'babel', 35 | exclude: /(node_modules)/, 36 | }] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/main/resources/ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'webpack-hot-middleware/client?reload=true', 8 | './components/Root.js' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'assets/js'), 12 | filename: 'bundle.js', 13 | publicPath: '/assets/js', 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin(), 19 | new webpack.DefinePlugin({ 20 | 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV) }, 21 | __CLIENT__: JSON.stringify(true), 22 | __SERVER__: JSON.stringify(false), 23 | }), 24 | new webpack.ProvidePlugin({ 25 | jQuery: 'jquery', 26 | $: 'jquery', 27 | jquery: 'jquery' 28 | }), 29 | ], 30 | resolve: { 31 | extensions: ['', '.js', '.jsx'] 32 | }, 33 | module: { 34 | loaders: [{ 35 | test: /\.jsx?$/, 36 | loader: 'babel', 37 | exclude: /(node_modules)/, 38 | }, 39 | ] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/notification/HttpClient.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.notification 2 | 3 | import java.io.{DataOutputStream, StringWriter} 4 | import java.net.{HttpURLConnection, URL} 5 | import java.util.logging.Logger 6 | 7 | import com.fasterxml.jackson.core.JsonFactory 8 | import org.apache.commons.codec.binary.Base64 9 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 10 | 11 | class HttpClient(val endpointUrl: String, 12 | val credentials: Option[String]) extends NotificationClient { 13 | 14 | private[this] val log = Logger.getLogger(getClass.getName) 15 | 16 | def sendNotification(job: BaseJob, to: String, subject: String, message: Option[String]) { 17 | 18 | val jsonBuffer = new StringWriter 19 | val factory = new JsonFactory() 20 | val generator = factory.createGenerator(jsonBuffer) 21 | 22 | // Create the payload 23 | generator.writeStartObject() 24 | 25 | if (subject != null && subject.nonEmpty) { 26 | generator.writeStringField("subject", subject) 27 | } 28 | if (message.nonEmpty && message.get.nonEmpty) { 29 | generator.writeStringField("message", message.get) 30 | } 31 | if (to != null && to.nonEmpty) { 32 | generator.writeStringField("to", to) 33 | } 34 | generator.writeStringField("job", job.name.toString()) 35 | generator.writeStringField("command", job.command.toString()) 36 | generator.writeStringField("cpus", job.cpus.toString()) 37 | generator.writeStringField("softError", job.softError.toString()) 38 | generator.writeStringField("errorCount", job.errorCount.toString()) 39 | generator.writeStringField("errorsSinceLastSuccess", job.errorsSinceLastSuccess.toString()) 40 | generator.writeStringField("executor", job.executor.toString()) 41 | generator.writeStringField("executorFlags", job.executorFlags.toString()) 42 | generator.writeStringField("lastError", job.lastError.toString()) 43 | generator.writeStringField("lastSuccess", job.lastSuccess.toString()) 44 | generator.writeStringField("mem", job.mem.toString()) 45 | generator.writeStringField("retries", job.retries.toString()) 46 | generator.writeStringField("successCount", job.successCount.toString()) 47 | val uris = job.fetch.map { 48 | _.uri 49 | } ++ job.uris 50 | generator.writeStringField("uris", uris.mkString(",")) 51 | 52 | 53 | generator.writeEndObject() 54 | generator.flush() 55 | 56 | val payload = jsonBuffer.toString 57 | val auth = if (credentials.nonEmpty && credentials.get.nonEmpty) { 58 | "Basic " + new String(Base64.encodeBase64(credentials.get.getBytes())); 59 | } else { 60 | "" 61 | } 62 | 63 | 64 | var connection: HttpURLConnection = null 65 | try { 66 | val url = new URL(endpointUrl) 67 | connection = url.openConnection.asInstanceOf[HttpURLConnection] 68 | connection.setDoInput(true) 69 | connection.setDoOutput(true) 70 | connection.setUseCaches(false) 71 | connection.setRequestMethod("POST") 72 | connection.setRequestProperty("Content-Type", "application/json"); 73 | 74 | if (auth.nonEmpty) { 75 | connection.setRequestProperty("Authorization", auth); 76 | } 77 | 78 | val outputStream = new DataOutputStream(connection.getOutputStream) 79 | outputStream.writeBytes(payload) 80 | outputStream.flush() 81 | outputStream.close() 82 | 83 | log.info("Sent message to http endpoint. Response code:" + 84 | connection.getResponseCode + 85 | " - " + 86 | connection.getResponseMessage) 87 | } finally { 88 | if (connection != null) { 89 | connection.disconnect() 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/notification/JobNotificationObserver.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.notification 2 | 3 | import java.util.logging.Logger 4 | 5 | import akka.actor.ActorRef 6 | import com.google.inject.Inject 7 | import org.apache.mesos.chronos.scheduler.jobs._ 8 | import org.joda.time.{DateTime, DateTimeZone} 9 | 10 | class JobNotificationObserver @Inject()(val notificationClients: List[ActorRef] = List(), 11 | val clusterName: Option[String] = None) { 12 | val clusterPrefix = clusterName.map(name => s"[$name]").getOrElse("") 13 | private[this] val log = Logger.getLogger(getClass.getName) 14 | 15 | def asObserver: JobsObserver.Observer = JobsObserver.withName({ 16 | case JobRemoved(job) => sendNotification(job, "%s [Chronos] Your job '%s' was deleted!".format(clusterPrefix, job.name), None) 17 | case JobDisabled(job, cause) => sendNotification( 18 | job, 19 | "%s [Chronos] job '%s' disabled".format(clusterPrefix, job.name), 20 | Some(cause)) 21 | 22 | case JobRetriesExhausted(job, taskStatus, attempts) => 23 | val msg = "\n'%s'. Retries attempted: %d.\nTask id: %s\n" 24 | .format(DateTime.now(DateTimeZone.UTC), job.retries, taskStatus.getTaskId.getValue) 25 | sendNotification(job, "%s [Chronos] job '%s' failed!".format(clusterPrefix, job.name), 26 | Some(TaskUtils.appendSchedulerMessage(msg, taskStatus))) 27 | }, getClass.getSimpleName) 28 | 29 | def sendNotification(job: BaseJob, subject: String, message: Option[String] = None) { 30 | for (client <- notificationClients) { 31 | val subowners = job.owner.split("\\s*,\\s*") 32 | for (subowner <- subowners) { 33 | log.info("Sending mail notification to:%s for job %s using client: %s".format(subowner, job.name, client)) 34 | client ! (job, subowner, subject, message) 35 | } 36 | } 37 | 38 | log.info(subject) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/notification/MailClient.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.notification 2 | 3 | import java.util.logging.Logger 4 | 5 | import org.apache.commons.mail.{DefaultAuthenticator, SimpleEmail} 6 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 7 | 8 | /** 9 | * A very simple mail client that works out of the box with providers such as Amazon SES. 10 | * TODO(FL): Test with other providers. 11 | * 12 | * @author Florian Leibert (flo@leibert.de) 13 | */ 14 | class MailClient( 15 | val mailServerString: String, 16 | val fromUser: String, 17 | val mailUser: Option[String], 18 | val password: Option[String], 19 | val ssl: Boolean) 20 | extends NotificationClient { 21 | 22 | private[this] val log = Logger.getLogger(getClass.getName) 23 | private[this] val split = """(.*):([0-9]*)""".r 24 | private[this] val split(mailHost, mailPortStr) = mailServerString 25 | private[this] val mailPort: Int = mailPortStr.toInt 26 | 27 | def sendNotification(job: BaseJob, to: String, subject: String, message: Option[String]) { 28 | val email = new SimpleEmail 29 | email.setHostName(mailHost) 30 | 31 | if (mailUser.isDefined && password.nonEmpty) { 32 | email.setAuthenticator(new DefaultAuthenticator(mailUser.get, password.get)) 33 | } 34 | 35 | email.addTo(to) 36 | email.setFrom(fromUser) 37 | 38 | email.setSubject(subject) 39 | 40 | if (message.nonEmpty && message.get.nonEmpty) { 41 | email.setMsg(message.get) 42 | } 43 | 44 | email.setSSLOnConnect(ssl) 45 | email.setSmtpPort(mailPort) 46 | val response = email.send 47 | log.info("Sent email to '%s' with subject: '%s', got response '%s'".format(to, subject, response)) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/notification/MattermostClient.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.notification 2 | 3 | import java.io.{DataOutputStream, StringWriter} 4 | import java.net.{HttpURLConnection, URL} 5 | import java.util.logging.Logger 6 | 7 | import com.fasterxml.jackson.core.JsonFactory 8 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 9 | 10 | class MattermostClient(val webhookUrl: String) extends NotificationClient { 11 | 12 | private[this] val log = Logger.getLogger(getClass.getName) 13 | 14 | def sendNotification(job: BaseJob, to: String, subject: String, message: Option[String]) { 15 | 16 | val jsonBuffer = new StringWriter 17 | val factory = new JsonFactory() 18 | val generator = factory.createGenerator(jsonBuffer) 19 | 20 | // Create the payload 21 | generator.writeStartObject() 22 | 23 | if (message.nonEmpty && message.get.nonEmpty) { 24 | if (subject != null && subject.nonEmpty) { 25 | generator.writeStringField("text", "%s: %s".format(subject, message.get)) 26 | } else { 27 | generator.writeStringField("text", "%s".format(message.get)) 28 | } 29 | } 30 | 31 | generator.writeEndObject() 32 | generator.flush() 33 | 34 | val payload = jsonBuffer.toString 35 | 36 | var connection: HttpURLConnection = null 37 | try { 38 | val url = new URL(webhookUrl) 39 | connection = url.openConnection.asInstanceOf[HttpURLConnection] 40 | connection.setDoInput(true) 41 | connection.setDoOutput(true) 42 | connection.setUseCaches(false) 43 | connection.setRequestMethod("POST") 44 | connection.setRequestProperty("Content-Type", "application/json"); 45 | 46 | val outputStream = new DataOutputStream(connection.getOutputStream) 47 | outputStream.writeBytes(payload) 48 | outputStream.flush() 49 | outputStream.close() 50 | 51 | log.info("Sent message to Mattermost! Response code:" + 52 | connection.getResponseCode + 53 | " - " + 54 | connection.getResponseMessage) 55 | } finally { 56 | if (connection != null) { 57 | connection.disconnect() 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/notification/NotificationClient.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.notification 2 | 3 | import java.util.logging.{Level, Logger} 4 | 5 | import akka.actor.{Actor, Terminated} 6 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 7 | 8 | /** 9 | * The form and design of the ability to send a notification to a specific source 10 | * 11 | * @author Greg Bowyer (gbowyer@fastmail.co.uk) 12 | */ 13 | trait NotificationClient extends Actor { 14 | 15 | private[this] val log = Logger.getLogger(getClass.getName) 16 | 17 | /** 18 | * Send the notification 19 | * 20 | * @param job the job that is being notified on 21 | * @param to the recipient of the notification 22 | * @param subject the subject line to use in notification 23 | * @param message the message that offers additional information about the notification 24 | */ 25 | def sendNotification(job: BaseJob, to: String, subject: String, message: Option[String]) 26 | 27 | def receive = { 28 | case (job: BaseJob, to: String, subject: String, message: Option[String]@unchecked) => 29 | try { 30 | sendNotification(job, to, subject, message) 31 | } catch { 32 | case t: Exception => log.log(Level.WARNING, "Caught a Exception while trying to send mail.", t) 33 | } 34 | case Terminated(_) => 35 | log.warning("Actor has exited, no longer sending out email notifications!") 36 | case _ => log.warning("Couldn't understand message.") 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/notification/RavenClient.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.notification 2 | 3 | import java.util.logging.Logger 4 | 5 | import com.getsentry.raven.RavenFactory 6 | import com.getsentry.raven.event.{Event, EventBuilder} 7 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 8 | 9 | /** 10 | * Notification client that uses sentry / raven to transmit its messages 11 | * 12 | * @author Greg Bowyer (gbowyer@fastmail.co.uk) 13 | */ 14 | class RavenClient(val dsn: String) extends NotificationClient { 15 | 16 | private[this] val log = Logger.getLogger(getClass.getName) 17 | private[this] val raven = RavenFactory.ravenInstance(dsn) 18 | 19 | def sendNotification(job: BaseJob, to: String, subject: String, message: Option[String]) { 20 | val ravenMessage = subject + "\n\n" + message.getOrElse("") 21 | val uris = job.fetch.map { 22 | _.uri 23 | } ++ job.uris 24 | val event = new EventBuilder() 25 | .withMessage(ravenMessage) 26 | .withLevel(Event.Level.ERROR) 27 | .withTag("owner", to) 28 | .withTag("job", job.name) 29 | .withTag("command", job.command) 30 | .withExtra("cpus", job.cpus) 31 | .withExtra("softError", job.softError) 32 | .withExtra("errorCount", job.errorCount) 33 | .withExtra("errorsSinceLastSuccess", job.errorsSinceLastSuccess) 34 | .withExtra("executor", job.executor) 35 | .withExtra("executorFlags", job.executorFlags) 36 | .withExtra("lastError", job.lastError) 37 | .withExtra("lastSuccess", job.lastSuccess) 38 | .withExtra("mem", job.mem) 39 | .withExtra("retries", job.retries) 40 | .withExtra("successCount", job.successCount) 41 | .withExtra("uris", uris.mkString(",")) 42 | .build() 43 | 44 | raven.sendEvent(event) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/notification/SlackClient.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.notification 2 | 3 | import java.io.{DataOutputStream, StringWriter} 4 | import java.net.{HttpURLConnection, URL} 5 | import java.util.logging.Logger 6 | 7 | import com.fasterxml.jackson.core.JsonFactory 8 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 9 | 10 | class SlackClient(val webhookUrl: String) extends NotificationClient { 11 | 12 | private[this] val log = Logger.getLogger(getClass.getName) 13 | 14 | def sendNotification(job: BaseJob, to: String, subject: String, message: Option[String]) { 15 | 16 | val jsonBuffer = new StringWriter 17 | val factory = new JsonFactory() 18 | val generator = factory.createGenerator(jsonBuffer) 19 | 20 | // Create the payload 21 | generator.writeStartObject() 22 | 23 | if (message.nonEmpty && message.get.nonEmpty) { 24 | if (subject != null && subject.nonEmpty) { 25 | generator.writeStringField("text", "%s: %s".format(subject, message.get)) 26 | } else { 27 | generator.writeStringField("text", "%s".format(message.get)) 28 | } 29 | } 30 | 31 | generator.writeEndObject() 32 | generator.flush() 33 | 34 | val payload = jsonBuffer.toString 35 | 36 | var connection: HttpURLConnection = null 37 | try { 38 | val url = new URL(webhookUrl) 39 | connection = url.openConnection.asInstanceOf[HttpURLConnection] 40 | connection.setDoInput(true) 41 | connection.setDoOutput(true) 42 | connection.setUseCaches(false) 43 | connection.setRequestMethod("POST") 44 | 45 | val outputStream = new DataOutputStream(connection.getOutputStream) 46 | outputStream.writeBytes(payload) 47 | outputStream.flush() 48 | outputStream.close() 49 | 50 | log.info("Sent message to Slack! Response code:" + 51 | connection.getResponseCode + 52 | " - " + 53 | connection.getResponseMessage) 54 | } finally { 55 | if (connection != null) { 56 | connection.disconnect() 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/Main.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean 4 | import java.util.logging.{Level, Logger} 5 | 6 | import com.google.inject.AbstractModule 7 | import mesosphere.chaos.http.{HttpConf, HttpModule, HttpService} 8 | import mesosphere.chaos.metrics.MetricsModule 9 | import mesosphere.chaos.{App, AppConfiguration} 10 | import org.apache.mesos.chronos.scheduler.api._ 11 | import org.apache.mesos.chronos.scheduler.config._ 12 | import org.apache.mesos.chronos.scheduler.jobs.{JobScheduler, MetricReporterService, ZookeeperService} 13 | import org.rogach.scallop.ScallopConf 14 | 15 | 16 | /** 17 | * Main entry point to chronos using the Chaos framework. 18 | * 19 | * @author Florian Leibert (flo@leibert.de) 20 | */ 21 | object Main extends App { 22 | lazy val conf = new ScallopConf(args) 23 | with HttpConf with AppConfiguration with SchedulerConfiguration 24 | with GraphiteConfiguration with CassandraConfiguration 25 | val isLeader = new AtomicBoolean(false) 26 | private[this] val log = Logger.getLogger(getClass.getName) 27 | conf.verify 28 | 29 | log.info("---------------------") 30 | log.info("Initializing chronos.") 31 | log.info("---------------------") 32 | 33 | def modules(): Seq[AbstractModule] = { 34 | Seq( 35 | new HttpModule(conf), 36 | new ChronosRestModule, 37 | new MetricsModule, 38 | new MainModule(conf), 39 | new ZookeeperModule(conf), 40 | new JobMetricsModule(conf), 41 | new JobStatsModule(conf) 42 | ) 43 | } 44 | 45 | try { 46 | run( 47 | classOf[ZookeeperService], 48 | classOf[HttpService], 49 | classOf[JobScheduler], 50 | classOf[MetricReporterService] 51 | ) 52 | } catch { 53 | case t: Throwable => 54 | log.log(Level.SEVERE, s"Chronos has exited because of an unexpected error: ${t.getMessage}", t) 55 | System.exit(1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/ApiResult.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import javax.ws.rs.core.Response 4 | 5 | class ApiResult( 6 | val message: String, 7 | val status: String = Response.Status.BAD_REQUEST.toString 8 | ) { 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/ApiResultSerializer.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator 4 | import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider} 5 | 6 | class ApiResultSerializer extends JsonSerializer[ApiResult] { 7 | def serialize(apiResult: ApiResult, json: JsonGenerator, provider: SerializerProvider) { 8 | json.writeStartObject() 9 | 10 | json.writeFieldName("status") 11 | json.writeString(apiResult.status) 12 | 13 | json.writeFieldName("message") 14 | json.writeString(apiResult.message) 15 | 16 | json.writeEndObject() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/ChronosRestModule.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import javax.inject.Named 4 | import javax.validation.Validation 5 | 6 | import com.codahale.metrics.servlets.MetricsServlet 7 | import com.fasterxml.jackson.databind.ObjectMapper 8 | import com.fasterxml.jackson.databind.module.SimpleModule 9 | import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider 10 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 11 | import com.google.inject.name.Names 12 | import com.google.inject.servlet.ServletModule 13 | import com.google.inject.{Provides, Scopes, Singleton} 14 | import com.sun.jersey.guice.spi.container.servlet.GuiceContainer 15 | import mesosphere.chaos.http.{LogConfigServlet, PingServlet} 16 | import mesosphere.chaos.validation.{ConstraintViolationExceptionMapper, JacksonMessageBodyProvider} 17 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 18 | import org.apache.mesos.chronos.utils.{JobDeserializer, JobSerializer} 19 | 20 | /** 21 | * @author Tobi Knaup 22 | */ 23 | 24 | class ChronosRestModule extends ServletModule { 25 | 26 | val guiceContainerUrl = "/v1/scheduler/*" 27 | 28 | // Override these in a subclass to mount resources at a different path 29 | val pingUrl = "/ping" 30 | val metricsUrl = "/metrics" 31 | val loggingUrl = "/logging" 32 | 33 | @Provides 34 | @Singleton 35 | def provideJacksonJsonProvider( 36 | @Named("restMapper") mapper: ObjectMapper): JacksonJsonProvider = { 37 | val mod = new SimpleModule("JobModule") 38 | mod.addSerializer(classOf[BaseJob], new JobSerializer) 39 | mod.addDeserializer(classOf[BaseJob], new JobDeserializer) 40 | mapper.registerModule(DefaultScalaModule) 41 | mapper.registerModule(mod) 42 | new JacksonMessageBodyProvider(mapper, 43 | Validation.buildDefaultValidatorFactory().getValidator) 44 | } 45 | 46 | protected override def configureServlets() { 47 | bind(classOf[ObjectMapper]) 48 | .annotatedWith(Names.named("restMapper")) 49 | .toInstance(new ObjectMapper()) 50 | 51 | bind(classOf[PingServlet]).in(Scopes.SINGLETON) 52 | bind(classOf[MetricsServlet]).in(Scopes.SINGLETON) 53 | bind(classOf[LogConfigServlet]).in(Scopes.SINGLETON) 54 | bind(classOf[ConstraintViolationExceptionMapper]).in(Scopes.SINGLETON) 55 | bind(classOf[LeaderResource]).in(Scopes.SINGLETON) 56 | 57 | serve(pingUrl).`with`(classOf[PingServlet]) 58 | serve(metricsUrl).`with`(classOf[MetricsServlet]) 59 | serve(loggingUrl).`with`(classOf[LogConfigServlet]) 60 | serve(guiceContainerUrl).`with`(classOf[GuiceContainer]) 61 | 62 | bind(classOf[WebJarServlet]).in(Scopes.SINGLETON) 63 | serve("/", "/assets/*").`with`(classOf[WebJarServlet]) 64 | 65 | bind(classOf[Iso8601JobResource]).in(Scopes.SINGLETON) 66 | bind(classOf[DependentJobResource]).in(Scopes.SINGLETON) 67 | bind(classOf[JobManagementResource]).in(Scopes.SINGLETON) 68 | bind(classOf[TaskManagementResource]).in(Scopes.SINGLETON) 69 | bind(classOf[GraphManagementResource]).in(Scopes.SINGLETON) 70 | bind(classOf[StatsResource]).in(Scopes.SINGLETON) 71 | bind(classOf[RedirectFilter]).in(Scopes.SINGLETON) 72 | bind(classOf[CorsFilter]).in(Scopes.SINGLETON) 73 | //This filter will redirect to the master if running in HA mode. 74 | filter("/*").through(classOf[CorsFilter]) 75 | filter("/*").through(classOf[RedirectFilter]) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/CorsFilter.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import java.util.logging.Logger 4 | import javax.servlet._ 5 | import javax.servlet.http.HttpServletResponse 6 | 7 | class CorsFilter extends Filter { 8 | private val log: Logger = Logger.getLogger(getClass.getName) 9 | 10 | def init(filterConfig: FilterConfig) {} 11 | 12 | def doFilter(rawRequest: ServletRequest, rawResponse: ServletResponse, chain: FilterChain) { 13 | rawResponse match { 14 | case response: HttpServletResponse => 15 | log.fine("Adding cors header to api response") 16 | response.setHeader("Access-Control-Allow-Origin", "*") 17 | chain.doFilter(rawRequest, rawResponse) 18 | case _ => 19 | } 20 | } 21 | 22 | def destroy() {} 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/DependentJobResource.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import java.util.logging.{Level, Logger} 4 | import javax.ws.rs.core.Response 5 | import javax.ws.rs.core.Response.Status 6 | import javax.ws.rs.{POST, Path, Produces} 7 | 8 | import com.codahale.metrics.annotation.Timed 9 | import com.google.common.base.Charsets 10 | import com.google.inject.Inject 11 | import org.apache.commons.lang3.exception.ExceptionUtils 12 | import org.apache.mesos.chronos.scheduler.graph.JobGraph 13 | import org.apache.mesos.chronos.scheduler.jobs._ 14 | 15 | /** 16 | * The REST API for adding job-dependencies to the scheduler. 17 | * 18 | * @author Florian Leibert (flo@leibert.de) 19 | */ 20 | @Path(PathConstants.dependentJobPath) 21 | @Produces(Array("application/json")) 22 | class DependentJobResource @Inject()( 23 | val jobScheduler: JobScheduler, 24 | val jobGraph: JobGraph) { 25 | 26 | private[this] val log = Logger.getLogger(getClass.getName) 27 | 28 | @POST 29 | @Timed 30 | def post(newJob: DependencyBasedJob): Response = { 31 | handleRequest(newJob) 32 | } 33 | 34 | def handleRequest(newJob: DependencyBasedJob): Response = { 35 | try { 36 | val oldJobOpt = jobGraph.lookupVertex(newJob.name) 37 | if (oldJobOpt.isEmpty) { 38 | log.info("Received request for job:" + newJob.toString) 39 | 40 | require(JobUtils.isValidJobName(newJob.name), 41 | "the job's name is invalid. Allowed names: '%s'".format(JobUtils.jobNamePattern.toString())) 42 | if (newJob.parents.isEmpty) throw new Exception("Error, parent does not exist") 43 | 44 | jobScheduler.loadJob(newJob) 45 | Response.noContent().build() 46 | } else { 47 | require(oldJobOpt.isDefined, "JobSchedule '%s' not found".format(newJob.name)) 48 | 49 | val oldJob = oldJobOpt.get 50 | 51 | //TODO(FL): Ensure we're using job-ids rather than relying on jobs names for identification. 52 | assert(newJob.name == oldJob.name, "Renaming jobs is currently not supported!") 53 | 54 | require(newJob.parents.nonEmpty, "Error, parent does not exist") 55 | 56 | log.info("Received replace request for job:" + newJob.toString) 57 | require(jobGraph.lookupVertex(newJob.name).isDefined, "JobSchedule '%s' not found".format(newJob.name)) 58 | //TODO(FL): Put all the logic for registering, deregistering and replacing dependency based jobs into one place. 59 | val parents = jobGraph.parentJobs(newJob) 60 | oldJob match { 61 | case j: DependencyBasedJob => 62 | val newParentNames = parents.map(_.name) 63 | val oldParentNames = jobGraph.parentJobs(j).map(_.name) 64 | 65 | if (newParentNames != oldParentNames) { 66 | oldParentNames.foreach(jobGraph.removeDependency(_, oldJob.name)) 67 | newParentNames.foreach(jobGraph.addDependency(_, newJob.name)) 68 | } 69 | case j: ScheduleBasedJob => 70 | log.info("Removing schedule for job: %s".format(j)) 71 | parents.foreach(p => jobGraph.addDependency(p.name, newJob.name)) 72 | } 73 | 74 | jobScheduler.updateJob(oldJob, newJob) 75 | 76 | log.info("JobSchedule parent: [ %s ], name: %s, command: %s".format(newJob.parents.mkString(","), newJob.name, newJob.command)) 77 | log.info("Replaced job: '%s', oldJob: '%s', newJob: '%s'".format( 78 | newJob.name, 79 | new String(JobUtils.toBytes(oldJob), Charsets.UTF_8), 80 | new String(JobUtils.toBytes(newJob), Charsets.UTF_8))) 81 | 82 | Response.noContent().build() 83 | } 84 | } catch { 85 | case ex: IllegalArgumentException => 86 | log.log(Level.INFO, "Bad Request", ex) 87 | Response 88 | .status(Status.BAD_REQUEST) 89 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex))) 90 | .build 91 | case ex: Exception => 92 | log.log(Level.WARNING, "Exception while serving request", ex) 93 | Response 94 | .serverError() 95 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex), 96 | status = Status.INTERNAL_SERVER_ERROR.toString)) 97 | .build 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/GraphManagementResource.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import java.io.StringWriter 4 | import java.util.logging.{Level, Logger} 5 | import javax.ws.rs._ 6 | import javax.ws.rs.core.Response.Status 7 | import javax.ws.rs.core.{MediaType, Response} 8 | 9 | import com.codahale.metrics.annotation.Timed 10 | import com.google.inject.Inject 11 | import org.apache.commons.lang3.exception.ExceptionUtils 12 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 13 | import org.apache.mesos.chronos.scheduler.graph.JobGraph 14 | import org.apache.mesos.chronos.scheduler.jobs.graph.Exporter 15 | import org.apache.mesos.chronos.scheduler.jobs.stats.JobStats 16 | 17 | /** 18 | * The REST API for managing jobs. 19 | * 20 | * @author Florian Leibert (flo@leibert.de) 21 | */ 22 | //TODO(FL): Create a case class that removes epsilon from the dependent. 23 | @Path(PathConstants.graphBasePath) 24 | @Consumes(Array(MediaType.APPLICATION_JSON)) 25 | class GraphManagementResource @Inject()( 26 | val jobStats: JobStats, 27 | val jobGraph: JobGraph, 28 | val configuration: SchedulerConfiguration) { 29 | 30 | private val log = Logger.getLogger(getClass.getName) 31 | 32 | @Produces(Array(MediaType.TEXT_PLAIN)) 33 | @Path(PathConstants.jobGraphDotPath) 34 | @GET 35 | @Timed 36 | def dotGraph(): Response = { 37 | try { 38 | Response.ok(jobGraph.makeDotFile()).build 39 | } catch { 40 | case ex: IllegalArgumentException => 41 | log.log(Level.INFO, "Bad Request", ex) 42 | Response 43 | .status(Status.BAD_REQUEST) 44 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex))) 45 | .build 46 | case ex: Exception => 47 | log.log(Level.WARNING, "Exception while serving request", ex) 48 | Response 49 | .serverError() 50 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex), 51 | status = Status.INTERNAL_SERVER_ERROR.toString)) 52 | .build 53 | } 54 | } 55 | 56 | @Produces(Array(MediaType.TEXT_PLAIN)) 57 | @Path(PathConstants.jobGraphCsvPath) 58 | @GET 59 | @Timed 60 | def jsonGraph(): Response = { 61 | try { 62 | val buffer = new StringWriter 63 | Exporter.export(buffer, jobGraph, jobStats) 64 | Response.ok(buffer.toString).build 65 | } catch { 66 | case ex: IllegalArgumentException => 67 | log.log(Level.INFO, "Bad Request", ex) 68 | Response 69 | .status(Status.BAD_REQUEST) 70 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex))) 71 | .build 72 | case ex: Exception => 73 | log.log(Level.WARNING, "Exception while serving request", ex) 74 | Response 75 | .serverError() 76 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex), 77 | status = Status.INTERNAL_SERVER_ERROR.toString)) 78 | .build 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/HistogramSerializer.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import com.codahale.metrics.{Histogram, Snapshot} 4 | import com.fasterxml.jackson.core.JsonGenerator 5 | import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider} 6 | 7 | object HistogramSerializerUtil { 8 | def serialize(hist: Histogram, json: JsonGenerator, provider: SerializerProvider) { 9 | val snapshot: Snapshot = hist.getSnapshot 10 | json.writeStartObject() 11 | 12 | json.writeFieldName("75thPercentile") 13 | json.writeNumber(snapshot.get75thPercentile()) 14 | 15 | json.writeFieldName("95thPercentile") 16 | json.writeNumber(snapshot.get95thPercentile()) 17 | 18 | json.writeFieldName("98thPercentile") 19 | json.writeNumber(snapshot.get98thPercentile()) 20 | 21 | json.writeFieldName("99thPercentile") 22 | json.writeNumber(snapshot.get99thPercentile()) 23 | 24 | json.writeFieldName("median") 25 | json.writeNumber(snapshot.getMedian) 26 | 27 | json.writeFieldName("mean") 28 | json.writeNumber(snapshot.getValue(0.5d)) 29 | 30 | json.writeFieldName("count") 31 | json.writeNumber(snapshot.size()) 32 | 33 | json.writeEndObject() 34 | } 35 | } 36 | 37 | /** 38 | * Author: @andykram 39 | */ 40 | class HistogramSerializer extends JsonSerializer[Histogram] { 41 | def serialize(hist: Histogram, json: JsonGenerator, provider: SerializerProvider) { 42 | HistogramSerializerUtil.serialize(hist, json, provider) 43 | json.close() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/JobStatWrapperSerializer.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator 4 | import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider} 5 | import org.apache.mesos.chronos.scheduler.jobs.JobStatWrapper 6 | import org.joda.time.DateTime 7 | import org.joda.time.format.{DateTimeFormat, PeriodFormatterBuilder} 8 | 9 | class JobStatWrapperSerializer extends JsonSerializer[JobStatWrapper] { 10 | def serialize(jobStat: JobStatWrapper, json: JsonGenerator, provider: SerializerProvider) { 11 | json.writeStartObject() 12 | 13 | json.writeFieldName("histogram") 14 | HistogramSerializerUtil.serialize(jobStat.hist, json, provider) 15 | 16 | val taskStats = jobStat.taskStats 17 | json.writeFieldName("taskStatHistory") 18 | json.writeStartArray() 19 | for (taskStat <- taskStats) { 20 | json.writeStartObject() 21 | 22 | json.writeFieldName("taskId") 23 | json.writeString(taskStat.taskId) 24 | 25 | json.writeFieldName("jobName") 26 | json.writeString(taskStat.jobName) 27 | 28 | json.writeFieldName("slaveId") 29 | json.writeString(taskStat.taskSlaveId) 30 | 31 | val fmt = DateTimeFormat.forPattern("MM/dd/yy HH:mm:ss") 32 | 33 | def writeTime(timeOpt: Option[DateTime]): Unit = 34 | timeOpt.fold(json.writeString("N/A"))(dT => json.writeString(fmt.print(dT))) 35 | 36 | //always show start time 37 | json.writeFieldName("startTime") 38 | writeTime(taskStat.taskStartTs) 39 | //show either end time or currently running 40 | json.writeFieldName("endTime") 41 | writeTime(taskStat.taskEndTs) 42 | 43 | taskStat.taskDuration.foreach { 44 | dur => 45 | val pFmt = new PeriodFormatterBuilder() 46 | .appendDays().appendSuffix("d") 47 | .appendHours().appendSuffix("h") 48 | .appendMinutes().appendSuffix("m") 49 | .printZeroIfSupported() 50 | .appendSeconds().appendSuffix("s") 51 | .toFormatter 52 | 53 | json.writeFieldName("duration") 54 | json.writeString(pFmt.print(dur.toPeriod)) 55 | 56 | } 57 | 58 | json.writeFieldName("status") 59 | json.writeString(taskStat.taskStatus.toString) 60 | 61 | //only write elements processed, ignore numAdditionalElementsProcessed 62 | taskStat.numElementsProcessed.foreach { 63 | num => 64 | json.writeFieldName("numElementsProcessed") 65 | json.writeNumber(num) 66 | } 67 | 68 | json.writeEndObject() 69 | } 70 | json.writeEndArray() 71 | 72 | json.writeEndObject() 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/JobSummaryWrapperSerializer.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator 4 | import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider} 5 | import org.apache.mesos.chronos.scheduler.jobs.JobSummaryWrapper 6 | 7 | class JobSummaryWrapperSerializer extends JsonSerializer[JobSummaryWrapper] { 8 | def serialize(jobSummary: JobSummaryWrapper, json: JsonGenerator, provider: SerializerProvider) { 9 | json.writeStartObject() 10 | 11 | json.writeFieldName("jobs") 12 | 13 | json.writeStartArray() 14 | for (job <- jobSummary.jobs) { 15 | json.writeStartObject() 16 | 17 | json.writeFieldName("name") 18 | json.writeString(job.name) 19 | 20 | json.writeFieldName("status") 21 | json.writeString(job.status) 22 | 23 | json.writeFieldName("state") 24 | json.writeString(job.state) 25 | 26 | json.writeFieldName("schedule") 27 | json.writeString(job.schedule) 28 | 29 | json.writeFieldName("parents") 30 | json.writeStartArray() 31 | for (parent <- job.parents) { 32 | json.writeString(parent) 33 | } 34 | json.writeEndArray() 35 | 36 | json.writeFieldName("disabled") 37 | json.writeBoolean(job.disabled) 38 | 39 | json.writeEndObject() 40 | } 41 | json.writeEndArray() 42 | 43 | json.writeEndObject() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/LeaderRestModule.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import javax.ws.rs.core.{MediaType, Response} 4 | import javax.ws.rs.{GET, Path, Produces} 5 | 6 | import com.google.inject.Inject 7 | import org.apache.mesos.chronos.scheduler.jobs.JobScheduler 8 | 9 | 10 | @Path(PathConstants.getLeaderPattern) 11 | @Produces(Array(MediaType.APPLICATION_JSON)) 12 | class LeaderResource @Inject()( 13 | val jobScheduler: JobScheduler 14 | ) { 15 | @GET 16 | def getLeader(): Response = { 17 | Response.ok(Map("leader" -> jobScheduler.getLeader)).build 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/PathConstants.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | /** 4 | * @author Florian Leibert (flo@leibert.de) 5 | */ 6 | object PathConstants { 7 | final val iso8601JobPath = "/iso8601" 8 | final val dependentJobPath = "/dependency" 9 | 10 | final val jobBasePath = "/" 11 | final val jobPatternPath = "job/{jobName}" 12 | final val allJobsPath = "jobs" 13 | final val jobSummaryPath = "jobs/summary" 14 | final val jobSearchPath = "jobs/search" 15 | final val allStatsPath = "stats/{percentile}" 16 | final val jobStatsPatternPath = "job/stat/{jobName}" 17 | final val jobTaskProgressPath = "job/{jobName}/task/{taskId}/progress" 18 | final val jobSuccessPath = "job/success/{jobName}" 19 | final val graphBasePath = "/graph" 20 | final val jobGraphDotPath = "dot" 21 | final val jobGraphCsvPath = "csv" 22 | final val killTaskPattern = "kill/{jobName}" 23 | final val getLeaderPattern = "/leader" 24 | 25 | final val isMasterPath = "isMaster" 26 | final val taskBasePath = "/task" 27 | final val uriTemplate = "http://%s%s" 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/RedirectFilter.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import java.io.{InputStream, OutputStream} 4 | import java.net.{HttpURLConnection, URL} 5 | import java.util.logging.{Level, Logger} 6 | import javax.servlet._ 7 | import javax.servlet.http.{HttpServletRequest, HttpServletResponse} 8 | 9 | import com.google.inject.Inject 10 | import mesosphere.chaos.http.HttpConf 11 | import org.apache.commons.lang3.exception.ExceptionUtils 12 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 13 | import org.apache.mesos.chronos.scheduler.jobs.JobScheduler 14 | 15 | import scala.collection.JavaConverters._ 16 | import scala.language.postfixOps 17 | 18 | /** 19 | * Simple filter that redirects to the leader if applicable. 20 | * 21 | * @author Florian Leibert (flo@leibert.de) 22 | */ 23 | class RedirectFilter @Inject()( 24 | val jobScheduler: JobScheduler, 25 | val config: SchedulerConfiguration with HttpConf) 26 | extends Filter { 27 | val log: Logger = Logger.getLogger(getClass.getName) 28 | 29 | def init(filterConfig: FilterConfig) {} 30 | 31 | def doFilter(rawRequest: ServletRequest, 32 | rawResponse: ServletResponse, 33 | chain: FilterChain) { 34 | rawRequest match { 35 | case request: HttpServletRequest => 36 | val leaderData = jobScheduler.getLeader 37 | val response = rawResponse.asInstanceOf[HttpServletResponse] 38 | val currentId = "%s:%d".format(config.hostname(), config.httpPort()) 39 | 40 | if (jobScheduler.isLeader || currentId == leaderData) { 41 | chain.doFilter(request, response) 42 | } else { 43 | var proxyStatus: Int = 200 44 | try { 45 | log.info("Proxying request to %s .".format(leaderData)) 46 | 47 | val method = request.getMethod 48 | 49 | val proxy = 50 | buildUrl(leaderData, request) 51 | .openConnection() 52 | .asInstanceOf[HttpURLConnection] 53 | 54 | val names = request.getHeaderNames 55 | // getHeaderNames() and getHeaders() are known to return null, see: 56 | // http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getHeaders(java.lang.String) 57 | if (names != null) { 58 | for (name <- names.asScala) { 59 | val values = request.getHeaders(name) 60 | if (values != null) { 61 | proxy.setRequestProperty(name, values.asScala.mkString(",")) 62 | } 63 | } 64 | } 65 | 66 | proxy.setRequestMethod(method) 67 | 68 | method match { 69 | case "GET" | "HEAD" | "DELETE" => 70 | proxy.setDoOutput(false) 71 | case _ => 72 | proxy.setDoOutput(true) 73 | val proxyOutputStream = proxy.getOutputStream 74 | copy(request.getInputStream, proxyOutputStream) 75 | proxyOutputStream.close() 76 | } 77 | 78 | proxyStatus = proxy.getResponseCode 79 | response.setStatus(proxyStatus) 80 | 81 | val fields = proxy.getHeaderFields 82 | // getHeaderNames() and getHeaders() are known to return null, see: 83 | // http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getHeaders(java.lang.String) 84 | if (fields != null) { 85 | for ((name, values) <- fields.asScala) { 86 | if (name != null && values != null) { 87 | for (value <- values.asScala) { 88 | response.setHeader(name, value) 89 | } 90 | } 91 | } 92 | } 93 | 94 | val responseOutputStream = response.getOutputStream 95 | copy(proxy.getInputStream, response.getOutputStream) 96 | proxy.getInputStream.close() 97 | responseOutputStream.close() 98 | } catch { 99 | case t: Exception => 100 | if ((200 to 299) contains proxyStatus) { 101 | log.log(Level.WARNING, "Exception while proxying!", t) 102 | response.sendError( 103 | 500, 104 | "Error proxying request to leader (maybe the leadership just changed?)\n\nError:\n" + ExceptionUtils 105 | .getStackTrace(t)) 106 | } 107 | } 108 | } 109 | case _ => 110 | } 111 | } 112 | 113 | def copy(input: InputStream, output: OutputStream) = { 114 | val bytes = new Array[Byte](1024) 115 | Iterator 116 | .continually(input.read(bytes)) 117 | .takeWhile(-1 !=) 118 | .foreach(read => output.write(bytes, 0, read)) 119 | } 120 | 121 | def buildUrl(leaderData: String, request: HttpServletRequest) = { 122 | if (request.getQueryString != null) { 123 | new URL( 124 | "http://%s%s?%s" 125 | .format(leaderData, request.getRequestURI, request.getQueryString)) 126 | } else { 127 | new URL("http://%s%s".format(leaderData, request.getRequestURI)) 128 | } 129 | } 130 | 131 | def destroy() { 132 | //NO-OP 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/StatsResource.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import java.util.logging.{Level, Logger} 4 | import javax.ws.rs._ 5 | import javax.ws.rs.core.Response.Status 6 | import javax.ws.rs.core.{MediaType, Response} 7 | 8 | import com.codahale.metrics.annotation.Timed 9 | import com.fasterxml.jackson.databind.ObjectMapper 10 | import com.google.inject.Inject 11 | import org.apache.commons.lang3.exception.ExceptionUtils 12 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 13 | import org.apache.mesos.chronos.scheduler.graph.JobGraph 14 | import org.apache.mesos.chronos.scheduler.jobs._ 15 | 16 | import scala.collection.JavaConversions._ 17 | import scala.collection.mutable.ListBuffer 18 | 19 | /** 20 | * The REST API to the PerformanceResource component of the API. 21 | * 22 | * @author Matt Redmond (matt.redmond@airbnb.com) 23 | * Returns a list of jobs, sorted by percentile run times. 24 | */ 25 | 26 | @Path(PathConstants.allStatsPath) 27 | @Produces(Array(MediaType.APPLICATION_JSON)) 28 | class StatsResource @Inject()( 29 | val jobScheduler: JobScheduler, 30 | val jobGraph: JobGraph, 31 | val configuration: SchedulerConfiguration, 32 | val jobMetrics: JobMetrics) { 33 | 34 | private[this] val log = Logger.getLogger(getClass.getName) 35 | 36 | @Timed 37 | @GET 38 | // Valid arguments are 39 | // /scheduler/stats/99thPercentile 40 | // /scheduler/stats/98thPercentile 41 | // /scheduler/stats/95thPercentile 42 | // /scheduler/stats/75thPercentile 43 | // /scheduler/stats/median 44 | // /scheduler/stats/mean 45 | def getPerf(@PathParam("percentile") percentile: String): Response = { 46 | try { 47 | var output = ListBuffer[Map[String, Any]]() 48 | var jobs = ListBuffer[(String, Double)]() 49 | 50 | val mapper = new ObjectMapper() 51 | for (jobNameString <- jobGraph.dag.vertexSet()) { 52 | val node = mapper.readTree(jobMetrics.getJsonStats(jobNameString)) 53 | if (node.has(percentile) && node.get(percentile) != null) { 54 | val time = node.get(percentile).asDouble() 55 | jobs.append((jobNameString, time)) 56 | } 57 | } 58 | jobs = jobs.sortBy(_._2).reverse 59 | for ((jobNameString, time) <- jobs) { 60 | val myMap = Map("jobNameLabel" -> jobNameString, "time" -> time / 1000.0) 61 | output.append(myMap) 62 | } 63 | Response.ok(output).build 64 | } catch { 65 | case ex: IllegalArgumentException => 66 | log.log(Level.INFO, "Bad Request", ex) 67 | Response 68 | .status(Status.BAD_REQUEST) 69 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex))) 70 | .build 71 | case ex: Exception => 72 | log.log(Level.WARNING, "Exception while serving request", ex) 73 | Response 74 | .serverError() 75 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex), 76 | status = Status.INTERNAL_SERVER_ERROR.toString)) 77 | .build 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/TaskManagementResource.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import java.util.logging.{Level, Logger} 4 | import javax.ws.rs._ 5 | import javax.ws.rs.core.Response 6 | import javax.ws.rs.core.Response.Status 7 | 8 | import com.google.inject.Inject 9 | import org.apache.commons.lang3.exception.ExceptionUtils 10 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 11 | import org.apache.mesos.chronos.scheduler.graph.JobGraph 12 | import org.apache.mesos.chronos.scheduler.jobs._ 13 | import org.apache.mesos.chronos.scheduler.state.PersistenceStore 14 | 15 | /** 16 | * The REST API for managing tasks such as updating the status of an asynchronous task. 17 | * 18 | * @author Florian Leibert (flo@leibert.de) 19 | */ 20 | //TODO(FL): Create a case class that removes epsilon from the dependent. 21 | @Path(PathConstants.taskBasePath) 22 | @Produces(Array("application/json")) 23 | class TaskManagementResource @Inject()( 24 | val persistenceStore: PersistenceStore, 25 | val jobScheduler: JobScheduler, 26 | val jobGraph: JobGraph, 27 | val taskManager: TaskManager, 28 | val configuration: SchedulerConfiguration) { 29 | 30 | private[this] val log = Logger.getLogger(getClass.getName) 31 | 32 | @DELETE 33 | @Path(PathConstants.killTaskPattern) 34 | def killTasksForJob(@PathParam("jobName") jobName: String): Response = { 35 | log.info("Task purge request received") 36 | try { 37 | require(jobGraph.lookupVertex(jobName).isDefined, 38 | "JobSchedule '%s' not found".format(jobName)) 39 | val job = jobGraph.getJobForName(jobName).get 40 | taskManager.cancelMesosTasks(job) 41 | Response.noContent().build() 42 | } catch { 43 | case ex: IllegalArgumentException => 44 | log.log(Level.INFO, "Bad Request", ex) 45 | Response 46 | .status(Status.BAD_REQUEST) 47 | .entity(new ApiResult(ExceptionUtils.getStackTrace(ex))) 48 | .build 49 | case ex: Exception => 50 | log.log(Level.WARNING, "Exception while serving request", ex) 51 | Response 52 | .serverError() 53 | .entity( 54 | new ApiResult(ExceptionUtils.getStackTrace(ex), 55 | status = Status.INTERNAL_SERVER_ERROR.toString)) 56 | .build 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/api/WebJarServlet.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.api 2 | 3 | import java.io.{InputStream, OutputStream} 4 | import java.net.URI 5 | import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} 6 | 7 | import org.slf4j.LoggerFactory 8 | 9 | import scala.annotation.tailrec 10 | import scala.util.{Failure, Success, Try} 11 | 12 | class WebJarServlet extends HttpServlet { 13 | private[this] val log = LoggerFactory.getLogger(getClass) 14 | private val BufferSize = 8192 15 | 16 | override def doGet(req: HttpServletRequest, resp: HttpServletResponse): Unit = { 17 | 18 | def withResource[T](path: String)(fn: InputStream => T): Option[T] = { 19 | Option(getClass.getResourceAsStream(path)).flatMap { stream => 20 | Try(stream.available()) match { 21 | case Success(_) => Some(fn(stream)) 22 | case Failure(_) => None 23 | } 24 | } 25 | } 26 | 27 | def transfer( 28 | in: InputStream, 29 | out: OutputStream, 30 | close: Boolean = true, 31 | continue: => Boolean = true): Unit = { 32 | try { 33 | val buffer = new Array[Byte](BufferSize) 34 | 35 | @tailrec def read(): Unit = { 36 | val byteCount = in.read(buffer) 37 | if (byteCount >= 0 && continue) { 38 | out.write(buffer, 0, byteCount) 39 | out.flush() 40 | read() 41 | } 42 | } 43 | 44 | read() 45 | } finally { 46 | if (close) Try(in.close()) 47 | } 48 | } 49 | 50 | 51 | def sendResource(resourceURI: String, mime: String): Unit = { 52 | withResource(resourceURI) { stream => 53 | resp.setContentType(mime) 54 | resp.setContentLength(stream.available()) 55 | resp.setStatus(200) 56 | transfer(stream, resp.getOutputStream) 57 | } getOrElse { 58 | resp.sendError(404) 59 | } 60 | } 61 | 62 | def sendResourceNormalized(resourceURI: String, mime: String): Unit = { 63 | val normalized = new URI(resourceURI).normalize().getPath 64 | if (normalized.startsWith("/assets")) sendResource(normalized, mime) 65 | else resp.sendError(404, s"Path: $normalized") 66 | } 67 | 68 | //extract request data 69 | val jar = req.getServletPath 70 | // e.g. /ui 71 | var resource = req.getPathInfo // e.g. /fonts/icon.gif 72 | if (resource.endsWith("/")) resource = resource + "index.html" 73 | // welcome file 74 | val file = resource.split("/").last 75 | //e.g. icon.gif 76 | val mediaType = file.split("\\.").lastOption.getOrElse("") 77 | //e.g. gif 78 | val mime = Option(getServletContext.getMimeType(file)).getOrElse(mimeType(mediaType)) 79 | //e.g plain/text 80 | val resourceURI = s"/assets$jar$resource" 81 | 82 | //log request data, since the names are not very intuitive 83 | if (log.isDebugEnabled) { 84 | log.debug( 85 | s""" 86 | |pathinfo: ${req.getPathInfo} 87 | |context: ${req.getContextPath} 88 | |servlet: ${req.getServletPath} 89 | |path: ${req.getPathTranslated} 90 | |uri: ${req.getRequestURI} 91 | |jar: $jar 92 | |resource: $resource 93 | |file: $file 94 | |mime: $mime 95 | |resourceURI: $resourceURI 96 | """.stripMargin) 97 | } 98 | 99 | sendResourceNormalized(resourceURI, mime) 100 | } 101 | 102 | private[this] def mimeType(mediaType: String): String = { 103 | mediaType.toLowerCase match { 104 | case "eot" => "application/vnd.ms-fontobject" 105 | case "svg" => "image/svg+xml" 106 | case "ttf" => "application/font-ttf" 107 | case _ => "application/octet-stream" 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/config/CassandraConfiguration.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.config 2 | 3 | import org.rogach.scallop.ScallopConf 4 | 5 | trait CassandraConfiguration extends ScallopConf { 6 | 7 | lazy val cassandraContactPoints = opt[String]("cassandra_contact_points", 8 | descr = "Comma separated list of contact points for Cassandra", 9 | default = None) 10 | 11 | lazy val cassandraPort = opt[Int]("cassandra_port", 12 | descr = "Port for Cassandra", 13 | default = Some(9042)) 14 | 15 | lazy val cassandraUser = opt[String]("cassandra_user", 16 | descr = "User", 17 | default = None) 18 | 19 | lazy val cassandraPassword = opt[String]("cassandra_password", 20 | descr = "Password", 21 | default = None) 22 | 23 | lazy val cassandraKeyspace = opt[String]("cassandra_keyspace", 24 | descr = "Keyspace to use for Cassandra", 25 | default = Some("metrics")) 26 | 27 | lazy val cassandraTable = opt[String]("cassandra_table", 28 | descr = "Table to use for Cassandra", 29 | default = Some("chronos")) 30 | 31 | lazy val cassandraStatCountTable = opt[String]("cassandra_stat_count_table", 32 | descr = "Table to track stat counts in Cassandra", 33 | default = Some("chronos_stat_count")) 34 | 35 | lazy val cassandraConsistency = opt[String]("cassandra_consistency", 36 | descr = "Consistency to use for Cassandra", 37 | default = Some("ANY")) 38 | 39 | lazy val cassandraTtl = opt[Int]("cassandra_ttl", 40 | descr = "TTL for records written to Cassandra", 41 | default = Some(3600 * 24 * 365)) 42 | 43 | lazy val jobHistoryLimit = opt[Int]("job_history_limit", 44 | descr = "Number of past job executions to show in history view", 45 | default = Some(5)) 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/config/GraphiteConfiguration.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.config 2 | 3 | import org.rogach.scallop.ScallopConf 4 | 5 | 6 | trait GraphiteConfiguration extends ScallopConf { 7 | 8 | lazy val graphiteHostPort = opt[String]("graphite_host_port", 9 | descr = "Host and port (in the form `host:port`) for Graphite", 10 | default = None) 11 | 12 | lazy val graphiteReportIntervalSeconds = opt[Long]( 13 | "graphite_reporting_interval", 14 | descr = "Graphite reporting interval (seconds)", 15 | default = Some(60L)) 16 | 17 | lazy val graphiteGroupPrefix = opt[String]("graphite_group_prefix", 18 | descr = "Group prefix for Graphite", 19 | default = Some("")) 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/config/JobMetricsModule.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.config 2 | 3 | import com.codahale.metrics.MetricRegistry 4 | import com.google.inject.{AbstractModule, Provides, Scopes, Singleton} 5 | import org.apache.mesos.chronos.scheduler.jobs.{JobMetrics, MetricReporterService} 6 | 7 | /** 8 | * Author: @andykram 9 | */ 10 | 11 | 12 | class JobMetricsModule(config: GraphiteConfiguration) extends AbstractModule { 13 | 14 | def configure() { 15 | bind(classOf[JobMetrics]).in(Scopes.SINGLETON) 16 | } 17 | 18 | @Provides 19 | @Singleton 20 | def provideMetricReporterService(registry: MetricRegistry) = { 21 | new MetricReporterService(config, registry) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/config/JobStatsModule.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.config 2 | 3 | import com.datastax.driver.core.Cluster 4 | import com.datastax.driver.core.ProtocolOptions.Compression 5 | import com.datastax.driver.core.policies.{DowngradingConsistencyRetryPolicy, LatencyAwarePolicy, RoundRobinPolicy} 6 | import com.google.inject.{AbstractModule, Provides, Scopes, Singleton} 7 | import org.apache.mesos.chronos.scheduler.jobs.stats.JobStats 8 | 9 | class JobStatsModule(config: CassandraConfiguration) extends AbstractModule { 10 | def configure() { 11 | bind(classOf[JobStats]).in(Scopes.SINGLETON) 12 | } 13 | 14 | @Provides 15 | @Singleton 16 | def provideCassandraClusterBuilder(): Option[Cluster.Builder] = { 17 | config.cassandraContactPoints.get match { 18 | case Some(contactPoints) => 19 | var builder = Cluster.builder() 20 | .addContactPoints(contactPoints.split(","): _*) 21 | .withPort(config.cassandraPort()) 22 | .withCompression(Compression.LZ4) 23 | .withRetryPolicy(DowngradingConsistencyRetryPolicy.INSTANCE) 24 | .withLoadBalancingPolicy(LatencyAwarePolicy.builder(new RoundRobinPolicy).build) 25 | 26 | if (config.cassandraUser.isDefined && config.cassandraPassword.isDefined) { 27 | builder = builder.withCredentials(config.cassandraUser.get.orNull, config.cassandraPassword.get.orNull) 28 | } 29 | Some(builder) 30 | case _ => 31 | None 32 | } 33 | } 34 | 35 | @Provides 36 | @Singleton 37 | def provideConfig() = { 38 | config 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/config/MediaType.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.config 2 | 3 | import javax.ws.rs.core.{MediaType => CoreMediaType} 4 | 5 | object MediaType extends CoreMediaType { 6 | val TEXT_JAVASCRIPT = "text/javascript" 7 | val TEXT_CSS = "text/css" 8 | } 9 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/config/ZookeeperModule.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.config 2 | 3 | import java.util.concurrent.TimeUnit 4 | import java.util.logging.Logger 5 | 6 | import com.google.inject.{AbstractModule, Inject, Provides, Singleton} 7 | import mesosphere.chaos.http.HttpConf 8 | import mesosphere.mesos.util.FrameworkIdUtil 9 | import org.apache.curator.framework.recipes.leader.LeaderLatch 10 | import org.apache.curator.framework.{CuratorFramework, CuratorFrameworkFactory} 11 | import org.apache.curator.retry.ExponentialBackoffRetry 12 | import org.apache.mesos.chronos.scheduler.jobs.ZookeeperService 13 | import org.apache.mesos.chronos.scheduler.state.{MesosStatePersistenceStore, PersistenceStore} 14 | import org.apache.mesos.state.{State, ZooKeeperState} 15 | import org.apache.zookeeper.KeeperException 16 | 17 | /** 18 | * Guice glue-code for zookeeper related things. 19 | * 20 | * @author Florian Leibert (flo@leibert.de) 21 | */ 22 | //TODO(FL): Consider using Sindi or Subcut for DI. 23 | class ZookeeperModule(val config: SchedulerConfiguration with HttpConf) 24 | extends AbstractModule { 25 | private val log = Logger.getLogger(getClass.getName) 26 | 27 | def configure() {} 28 | 29 | @Inject 30 | @Singleton 31 | @Provides 32 | def provideZookeeperClient(): CuratorFramework = { 33 | val builder = CuratorFrameworkFactory.builder() 34 | .connectionTimeoutMs(config.zooKeeperTimeout().toInt) 35 | .canBeReadOnly(false) 36 | .connectString(validateZkServers()) 37 | .retryPolicy(new ExponentialBackoffRetry(1000, 10)) 38 | 39 | config.zooKeeperAuth.get match { 40 | case Some(s) => 41 | builder.authorization("digest", s.getBytes()) 42 | case _ => 43 | } 44 | 45 | val curator = builder.build() 46 | 47 | curator.start() 48 | log.info("Connecting to ZK...") 49 | curator.blockUntilConnected() 50 | curator 51 | } 52 | 53 | private def validateZkServers(): String = { 54 | val servers: Array[String] = config.zookeeperServers().split(",") 55 | servers.foreach({ 56 | server => 57 | require(server.split(":").size == 2, "Error, zookeeper servers must be provided in the form host1:port2,host2:port2") 58 | }) 59 | servers.mkString(",") 60 | } 61 | 62 | @Inject 63 | @Singleton 64 | @Provides 65 | def provideZookeeperService(curator: CuratorFramework): ZookeeperService = { 66 | new ZookeeperService(curator) 67 | } 68 | 69 | @Inject 70 | @Singleton 71 | @Provides 72 | def provideState(): State = { 73 | new ZooKeeperState(config.zookeeperServers(), 74 | config.zooKeeperTimeout(), 75 | TimeUnit.MILLISECONDS, 76 | config.zooKeeperStatePath, 77 | getAuthScheme, 78 | getAuthBytes 79 | ) 80 | } 81 | 82 | private def getAuthScheme: String = { 83 | config.zooKeeperAuth.get match { 84 | case Some(_) => 85 | "digest" 86 | case _ => null 87 | } 88 | } 89 | 90 | private def getAuthBytes: Array[Byte] = { 91 | config.zooKeeperAuth.get match { 92 | case Some(s) => s.getBytes() 93 | case _ => null 94 | } 95 | } 96 | 97 | @Inject 98 | @Singleton 99 | @Provides 100 | def provideStore(curator: CuratorFramework, state: State): PersistenceStore = { 101 | scala.util.control.Exception.ignoring(classOf[KeeperException.NodeExistsException]) { 102 | curator.create() 103 | .creatingParentContainersIfNeeded() 104 | .forPath(config.zooKeeperStatePath) 105 | } 106 | new MesosStatePersistenceStore(curator, config, state) 107 | } 108 | 109 | @Provides 110 | @Singleton 111 | def provideFrameworkIdUtil(state: State): FrameworkIdUtil = { 112 | new FrameworkIdUtil(state) 113 | } 114 | 115 | @Inject 116 | @Singleton 117 | @Provides 118 | def provideLeaderLatch(curator: CuratorFramework): LeaderLatch = { 119 | val id = "%s:%d".format(config.hostname(), config.httpPort()) 120 | 121 | scala.util.control.Exception.ignoring(classOf[KeeperException.NodeExistsException]) { 122 | curator.create() 123 | .creatingParentContainersIfNeeded() 124 | .forPath(config.zooKeeperCandidatePath) 125 | } 126 | 127 | new LeaderLatch(curator, config.zooKeeperCandidatePath, id) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/Containers.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | object VolumeMode extends Enumeration { 6 | type VolumeMode = Value 7 | 8 | // read-write and read-only. 9 | val RW, RO = Value 10 | } 11 | 12 | object NetworkMode extends Enumeration { 13 | type NetworkMode = Value 14 | 15 | // Bridged, Host and USER 16 | val BRIDGE, HOST, USER = Value 17 | } 18 | 19 | object ContainerType extends Enumeration { 20 | type ContainerType = Value 21 | 22 | // Docker, Mesos 23 | val DOCKER, MESOS = Value 24 | } 25 | 26 | 27 | object ProtocolType extends Enumeration { 28 | type ProtocolType = Value 29 | 30 | val IPv4, IPv6 = Value 31 | } 32 | 33 | import org.apache.mesos.chronos.scheduler.jobs.NetworkMode._ 34 | import org.apache.mesos.chronos.scheduler.jobs.VolumeMode._ 35 | import org.apache.mesos.chronos.scheduler.jobs.ContainerType._ 36 | import org.apache.mesos.chronos.scheduler.jobs.ProtocolType._ 37 | 38 | case class ExternalVolume( 39 | @JsonProperty name: String, 40 | @JsonProperty provider: String, 41 | @JsonProperty options: Seq[Parameter]) 42 | 43 | case class Volume( 44 | @JsonProperty hostPath: Option[String], 45 | @JsonProperty containerPath: String, 46 | @JsonProperty mode: Option[VolumeMode], 47 | @JsonProperty external: Option[ExternalVolume]) 48 | 49 | case class PortMapping( 50 | @JsonProperty hostPort: Int, 51 | @JsonProperty containerPort: Int, 52 | @JsonProperty protocol: Option[String]) 53 | 54 | case class Network( 55 | @JsonProperty name: String, 56 | @JsonProperty protocol: Option[ProtocolType], 57 | @JsonProperty labels: Seq[Label], 58 | @JsonProperty portMappings: Seq[PortMapping]) 59 | 60 | case class Container( 61 | @JsonProperty image: String, 62 | @JsonProperty `type`: ContainerType = ContainerType.DOCKER, 63 | @JsonProperty volumes: Seq[Volume], 64 | @JsonProperty parameters: Seq[Parameter], 65 | @JsonProperty network: NetworkMode = NetworkMode.HOST, 66 | // DEPRECATED, "networkName" will be removed in a future version. 67 | @JsonProperty networkName: Option[String], 68 | @JsonProperty networkInfos: Seq[Network], 69 | @JsonProperty forcePullImage: Boolean = false) 70 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/EnvironmentVariable.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | /** 6 | * Represents an environment variable definition for the job 7 | */ 8 | case class EnvironmentVariable( 9 | @JsonProperty name: String, 10 | @JsonProperty value: String) 11 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/Fetch.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | case class Fetch(@JsonProperty uri: String, 6 | @JsonProperty output_file: String = "", 7 | @JsonProperty executable: Boolean = false, 8 | @JsonProperty cache: Boolean = false, 9 | @JsonProperty extract: Boolean = true) 10 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/Iso8601Expressions.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import java.util.TimeZone 4 | 5 | import org.joda.time.format.{ISODateTimeFormat, ISOPeriodFormat} 6 | import org.joda.time.{DateTime, DateTimeZone, Period} 7 | 8 | /** 9 | * Parsing, creating and validation for Iso8601 expressions. 10 | * 11 | * @author Florian Leibert (flo@leibert.de) 12 | */ 13 | object Iso8601Expressions { 14 | val iso8601ExpressionRegex = """(R[0-9]*)/(.*)/(P.*)""".r 15 | private[this] val formatter = ISODateTimeFormat.dateTime 16 | 17 | /** 18 | * Verifies that the given expression is a valid Iso8601Expression. Currently not all Iso8601Expression formats 19 | * are supported. 20 | * 21 | * @param input 22 | * @return 23 | */ 24 | def canParse(input: String, timeZoneStr: String = ""): Boolean = { 25 | parse(input, timeZoneStr) match { 26 | case Some((_, _, _)) => 27 | true 28 | case None => 29 | false 30 | } 31 | } 32 | 33 | /** 34 | * Parses a ISO8601 expression into a tuple consisting of the number of repetitions (or -1 for infinity), 35 | * the start and the period. 36 | * 37 | * @param input the input string which is a ISO8601 expression consisting of Repetition, Start and Period. 38 | * @return a three tuple (repetitions, start, period) 39 | */ 40 | def parse(input: String, timeZoneStr: String = ""): Option[(Long, DateTime, Period)] = { 41 | try { 42 | 43 | val iso8601ExpressionRegex(repeatStr, startStr, periodStr) = input 44 | 45 | val repeat: Long = { 46 | if (repeatStr.length == 1) 47 | -1L 48 | else 49 | repeatStr.substring(1).toLong 50 | } 51 | 52 | val start: DateTime = if (startStr.length == 0) DateTime.now(DateTimeZone.UTC).minusSeconds(1) else convertToDateTime(startStr, timeZoneStr) 53 | val period: Period = ISOPeriodFormat.standard.parsePeriod(periodStr) 54 | Some((repeat, start, period)) 55 | } catch { 56 | case e: scala.MatchError => 57 | None 58 | case e: IllegalArgumentException => 59 | None 60 | } 61 | } 62 | 63 | /** 64 | * Creates a DateTime object from an input string. This parses the object by first checking for a time zone and then 65 | * using a datetime formatter to format the date and time. 66 | * 67 | * @param dateTimeStr the input date time string with optional time zone 68 | * @return the date time 69 | */ 70 | def convertToDateTime(dateTimeStr: String, timeZoneStr: String): DateTime = { 71 | val dateTime = DateTime.parse(dateTimeStr) 72 | if (timeZoneStr != null && timeZoneStr.length > 0) { 73 | val timeZone = DateTimeZone.forTimeZone(TimeZone.getTimeZone(timeZoneStr)) 74 | dateTime.withZoneRetainFields(timeZone) 75 | } else { 76 | dateTime 77 | } 78 | } 79 | 80 | /** 81 | * Creates a valid Iso8601Expression based on the input parameters. 82 | * 83 | * @param recurrences 84 | * @param startDate 85 | * @param period 86 | * @return 87 | */ 88 | def create(recurrences: Long, startDate: DateTime, period: Period): String = { 89 | if (recurrences != -1) 90 | "R%d/%s/%s".format(recurrences, formatter.print(startDate), ISOPeriodFormat.standard.print(period)) 91 | else 92 | "R/%s/%s".format(formatter.print(startDate), ISOPeriodFormat.standard.print(period)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/JobMetrics.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import com.codahale.metrics.{Counter, Histogram, MetricRegistry} 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.databind.module.SimpleModule 6 | import com.google.inject.Inject 7 | import org.apache.mesos.chronos.scheduler.api.HistogramSerializer 8 | 9 | import scala.collection.mutable 10 | 11 | /** 12 | * Author: @andykram 13 | */ 14 | class JobMetrics @Inject()(registry: MetricRegistry) { 15 | 16 | protected val stats = new mutable.HashMap[String, Histogram]() 17 | protected val statuses = new mutable.HashMap[String, Map[String, Counter]]() 18 | protected val objectMapper = new ObjectMapper 19 | protected val mod = new SimpleModule("JobModule") 20 | 21 | mod.addSerializer(classOf[Histogram], new HistogramSerializer) 22 | objectMapper.registerModule(mod) 23 | 24 | def updateJobStat(jobName: String, timeMs: Long) { 25 | // Uses a Uniform Histogram by default for long term metrics. 26 | val stat: Histogram = stats.getOrElseUpdate(jobName, mkStat(jobName)) 27 | 28 | stat.update(timeMs) 29 | } 30 | 31 | def getJsonStats(jobName: String): String = { 32 | val snapshot = getJobHistogramStats(jobName) 33 | objectMapper.writeValueAsString(snapshot) 34 | } 35 | 36 | def getJobHistogramStats(jobName: String): Histogram = { 37 | stats.getOrElseUpdate(jobName, mkStat(jobName)) 38 | } 39 | 40 | protected def mkStat(jobName: String, name: String = "time") = { 41 | registry.histogram(MetricRegistry.name("jobs", "run", name, jobName)) 42 | } 43 | 44 | def updateJobStatus(jobName: String, success: Boolean) { 45 | val statusCounters: Map[String, Counter] = statuses.getOrElseUpdate(jobName, 46 | Map("success" -> mkCounter(jobName, "success"), 47 | "failure" -> mkCounter(jobName, "failure"))) 48 | 49 | val counter: Counter = if (success) { 50 | statusCounters.get("success").get 51 | } else { 52 | statusCounters.get("failure").get 53 | } 54 | 55 | counter.inc() 56 | } 57 | 58 | protected def mkCounter(jobName: String, name: String) = { 59 | registry.counter(MetricRegistry.name("jobs", "run", name, jobName)) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/JobSchedule.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | class JobSchedule(val schedule: String, val jobName: String, val scheduleTimeZone: String = "") { 4 | def getSchedule: Option[JobSchedule] = 5 | //TODO(FL) Represent the schedule as a data structure instead of a string. 6 | Iso8601Expressions.parse(schedule, scheduleTimeZone) match { 7 | case Some((rec, start, per)) => 8 | if (rec == -1) 9 | Some(new JobSchedule(Iso8601Expressions.create(rec, start.plus(per), per), jobName, 10 | scheduleTimeZone)) 11 | else if (rec > 0) 12 | Some(new JobSchedule(Iso8601Expressions.create(rec - 1, start.plus(per), per), jobName, 13 | scheduleTimeZone)) 14 | else 15 | None 16 | case None => 17 | None 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/JobStatWrapper.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import com.codahale.metrics.Histogram 4 | 5 | class JobStatWrapper(val taskStats: List[TaskStat], 6 | val hist: Histogram) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/JobSummaryWrapper.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | class JobSummary( 4 | val name: String, 5 | val status: String, 6 | val state: String, 7 | val schedule: String, 8 | val parents: List[String], 9 | val disabled: Boolean 10 | ) { 11 | } 12 | 13 | class JobSummaryWrapper( 14 | val jobs: List[JobSummary] 15 | ) { 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/JobsObserver.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import java.util.logging.Logger 4 | 5 | import org.apache.mesos.Protos.TaskStatus 6 | import org.joda.time.DateTime 7 | 8 | trait JobEvent 9 | 10 | case class JobQueued(job: BaseJob, taskId: String, attempt: Int) extends JobEvent 11 | 12 | case class JobSkipped(job: BaseJob, dateTime: DateTime) extends JobEvent 13 | 14 | case class JobStarted(job: BaseJob, taskStatus: TaskStatus, attempt: Int, runningCount: Int) extends JobEvent 15 | 16 | case class JobFinished(job: BaseJob, taskStatus: TaskStatus, attempt: Int, runningCount: Int) extends JobEvent 17 | 18 | // Either a job name or job object, depending on whether the JobSchedule still exists 19 | case class JobFailed(job: Either[String, BaseJob], taskStatus: TaskStatus, attempt: Int, runningCount: Int) extends JobEvent 20 | 21 | case class JobDisabled(job: BaseJob, cause: String) extends JobEvent 22 | 23 | case class JobRetriesExhausted(job: BaseJob, taskStatus: TaskStatus, attempt: Int) extends JobEvent 24 | 25 | case class JobRemoved(job: BaseJob) extends JobEvent 26 | 27 | // This event is fired when job is disabled (e.g. due to recurrence going to 0) and its queued tasks are purged 28 | case class JobExpired(job: BaseJob, taskId: String) extends JobEvent 29 | 30 | object JobsObserver { 31 | type Observer = PartialFunction[JobEvent, Unit] 32 | private[this] val log = Logger.getLogger(getClass.getName) 33 | 34 | def composite(observers: List[Observer]): Observer = { 35 | case event => observers.foreach(observer => observer.lift.apply(event).orElse { 36 | log.info(s"$observer does not handle $event") 37 | Some(Unit) 38 | }) 39 | } 40 | 41 | def withName(observer: Observer, name: String): Observer = new Observer { 42 | override def isDefinedAt(event: JobEvent) = observer.isDefinedAt(event) 43 | 44 | override def apply(event: JobEvent): Unit = observer.apply(event) 45 | 46 | override def toString(): String = name 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/Label.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import org.apache.mesos.{Protos => mesos} 4 | 5 | /** 6 | * Represents an environment variable definition for the job 7 | */ 8 | case class Label( 9 | key: String, 10 | value: String) { 11 | 12 | def toProto(): mesos.Label = 13 | mesos.Label.newBuilder 14 | .setKey(key) 15 | .setValue(value) 16 | .build 17 | } 18 | 19 | object Label { 20 | def apply(proto: mesos.Label): Label = 21 | Label( 22 | proto.getKey, 23 | proto.getValue 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/MetricReporterService.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.TimeUnit 5 | 6 | import com.codahale.metrics.MetricRegistry 7 | import com.codahale.metrics.graphite.{Graphite, GraphiteReporter} 8 | import com.google.common.util.concurrent.AbstractIdleService 9 | import org.apache.mesos.chronos.scheduler.config.GraphiteConfiguration 10 | 11 | object MetricReporterService { 12 | 13 | object HostPort { 14 | def unapply(str: String): Option[(String, Int)] = str.split(":") match { 15 | case Array(host: String, port: String) => Some(Tuple2(host, port.toInt)) 16 | case _ => None 17 | } 18 | } 19 | 20 | } 21 | 22 | class MetricReporterService(config: GraphiteConfiguration, 23 | registry: MetricRegistry) 24 | extends AbstractIdleService { 25 | private[this] var reporter: Option[GraphiteReporter] = None 26 | 27 | def startUp() { 28 | this.reporter = config.graphiteHostPort.get match { 29 | case Some(MetricReporterService.HostPort(host: String, port: Int)) => 30 | val graphite = new Graphite(new InetSocketAddress(host, port)) 31 | val reporter = GraphiteReporter.forRegistry(registry) 32 | .prefixedWith(config.graphiteGroupPrefix()) 33 | .convertRatesTo(TimeUnit.SECONDS) 34 | .convertDurationsTo(TimeUnit.MILLISECONDS) 35 | .build(graphite) 36 | reporter.start(config.graphiteReportIntervalSeconds(), TimeUnit.SECONDS) 37 | Some(reporter) 38 | case _ => None 39 | } 40 | } 41 | 42 | def shutDown() { 43 | this.reporter match { 44 | case Some(r: GraphiteReporter) => r.stop() 45 | case _ => // Nothing to shutdown! 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/Parameter.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import org.apache.mesos.{Protos => mesos} 4 | 5 | /** 6 | * Represents an environment variable definition for the job 7 | */ 8 | case class Parameter( 9 | key: String, 10 | value: String) { 11 | 12 | def toProto(): mesos.Parameter = 13 | mesos.Parameter.newBuilder 14 | .setKey(key) 15 | .setValue(value) 16 | .build 17 | } 18 | 19 | object Parameter { 20 | def apply(proto: mesos.Parameter): Parameter = 21 | Parameter( 22 | proto.getKey, 23 | proto.getValue 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/ScheduledTask.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import java.util.concurrent.Callable 4 | 5 | import org.joda.time.DateTime 6 | 7 | /** 8 | * A wrapper around a task-tuple (taskId, baseJob), which appends the tuple to the job queue once the task is due. 9 | * 10 | * @author Florian Leibert (flo@leibert.de) 11 | */ 12 | class ScheduledTask( 13 | val taskId: String, 14 | val due: DateTime, 15 | val job: BaseJob, 16 | val taskManager: TaskManager) 17 | extends Callable[Void] { 18 | 19 | def call(): Void = { 20 | //TODO(FL): Think about pulling the state updating into the TaskManager. 21 | taskManager.log.info("Triggering: '%s'".format(job.name)) 22 | taskManager.removeTaskFutureMapping(this) 23 | taskManager.enqueue(taskId, job.highPriority) 24 | null 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/TaskStat.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import java.util.Date 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty 6 | import org.joda.time.{DateTime, Duration} 7 | 8 | /* 9 | * Task status enum at Chronos level, don't care about 10 | * killed/lost state. Idle state is not valid, a TaskStat 11 | * should only be generated if a task is already running 12 | * or finished 13 | */ 14 | object ChronosTaskStatus extends Enumeration { 15 | type TaskStatus = Value 16 | val Success, Fail, Running, Idle = Value 17 | } 18 | 19 | class TaskStat(@JsonProperty val taskId: String, 20 | @JsonProperty val jobName: String, 21 | @JsonProperty val taskSlaveId: String) { 22 | /* 23 | * Cassandra column names 24 | */ 25 | val TASK_ID = "id" 26 | val JOB_NAME = "job_name" 27 | val TASK_EVENT_TIMESTAMP = "ts" 28 | val TASK_STATE = "task_state" 29 | val TASK_SLAVE_ID = "slave_id" 30 | 31 | //time stats 32 | @JsonProperty var taskStartTs: Option[DateTime] = None 33 | @JsonProperty var taskEndTs: Option[DateTime] = None 34 | @JsonProperty var taskDuration: Option[Duration] = None 35 | 36 | @JsonProperty var taskStatus: ChronosTaskStatus.Value = ChronosTaskStatus.Idle 37 | 38 | //move out of object later (this should be a data subclass) 39 | @JsonProperty var numElementsProcessed: Option[Long] = None 40 | //used only for output (HTTP GET) 41 | @JsonProperty var numAdditionalElementsProcessed: Option[Int] = None //used only for input (HTTP POST) 42 | 43 | def getTaskRuntime: Option[Duration] = taskDuration 44 | 45 | def setTaskStatus(status: ChronosTaskStatus.Value) = { 46 | //if already a terminal state, ignore 47 | if ((taskStatus != ChronosTaskStatus.Success) && 48 | (taskStatus != ChronosTaskStatus.Fail)) { 49 | taskStatus = status 50 | } 51 | } 52 | 53 | def setTaskStartTs(startTs: Date) = { 54 | //update taskStartTs if new value is older 55 | val taskStartDatetime = new DateTime(startTs) 56 | taskStartTs = taskStartTs match { 57 | case Some(currTs: DateTime) => 58 | if (taskStartDatetime.isBefore(currTs)) { 59 | Some(taskStartDatetime) 60 | } else { 61 | Some(currTs) 62 | } 63 | case None => 64 | Some(taskStartDatetime) 65 | } 66 | 67 | taskEndTs match { 68 | case Some(ts) => 69 | taskDuration = Some(new Duration(taskStartDatetime, ts)) 70 | case None => 71 | } 72 | } 73 | 74 | def setTaskEndTs(endTs: Date) = { 75 | val taskEndDatetime = new DateTime(endTs) 76 | taskEndTs = Some(taskEndDatetime) 77 | taskStartTs match { 78 | case Some(ts) => 79 | taskDuration = Some(new Duration(ts, taskEndDatetime)) 80 | case None => 81 | } 82 | } 83 | 84 | override def toString: String = { 85 | "taskId=%s job_name=%s slaveId=%s startTs=%s endTs=%s duration=%s status=%s".format( 86 | taskId, jobName, taskSlaveId, 87 | taskStartTs.toString, 88 | taskEndTs.toString, 89 | taskDuration.toString, 90 | taskStatus.toString) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/TaskUtils.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import java.util.logging.Logger 4 | 5 | import org.apache.mesos.Protos.{TaskID, TaskState, TaskStatus} 6 | import org.joda.time.DateTime 7 | 8 | import scala.util.matching.Regex 9 | 10 | /** 11 | * This file contains a number of classes and objects for dealing with tasks. Tasks are the actual units of work that 12 | * get translated into a chronos-task based on a job and it's schedule or as a dependency based on another task. They are 13 | * serialized to an underlying storage upon submission such that in the case of failed tasks or scheduler failover the 14 | * task can be retried, double submission during failover is prevented, etc. 15 | * 16 | * @author Florian Leibert (flo@leibert.de) 17 | */ 18 | 19 | object TaskUtils { 20 | 21 | //TaskIdFormat: ct:JOB_NAME:DUE:ATTEMPT:ARGUMENTS 22 | val taskIdTemplate = "ct:%d:%d:%s:%s" 23 | val argumentsPattern: Regex = """(.*)?""".r 24 | val taskIdPattern: Regex = """ct:(\d+):(\d+):%s:?%s""".format(JobUtils.jobNamePattern, argumentsPattern).r 25 | val commandInjectionFilter: Set[Any] = ";".toSet 26 | 27 | private[this] val log = Logger.getLogger(getClass.getName) 28 | 29 | def getTaskStatus(job: BaseJob, due: DateTime, attempt: Int = 0): TaskStatus = { 30 | TaskStatus.newBuilder.setTaskId(TaskID.newBuilder.setValue(getTaskId(job, due, attempt))).setState(TaskState.TASK_STAGING).build 31 | } 32 | 33 | def getTaskId(job: BaseJob, due: DateTime, attempt: Int = 0, arguments: Option[String] = None): String = { 34 | val args: String = arguments.getOrElse(job.arguments.mkString(" ")).filterNot(commandInjectionFilter) 35 | taskIdTemplate.format(due.getMillis, attempt, job.name, args) 36 | } 37 | 38 | def isValidVersion(taskIdString: String): Boolean = { 39 | taskIdPattern.findFirstIn(taskIdString).nonEmpty 40 | } 41 | 42 | def appendSchedulerMessage(msg: String, taskStatus: TaskStatus): String = { 43 | val schedulerMessage = 44 | if (taskStatus.hasMessage && taskStatus.getMessage.nonEmpty) 45 | Some(taskStatus.getMessage) 46 | else 47 | None 48 | schedulerMessage.fold(msg)(m => "%sThe scheduler provided this message:\n\n%s".format(msg, m)) 49 | } 50 | 51 | /** 52 | * Parses the task id into the jobname and the tasks creation time. 53 | * 54 | * @param taskId 55 | * @return 56 | */ 57 | def getJobNameForTaskId(taskId: String): String = { 58 | require(taskId != null, "taskId cannot be null") 59 | try { 60 | val TaskUtils.taskIdPattern(_, _, jobName, _) = taskId 61 | jobName 62 | } catch { 63 | case t: Exception => 64 | log.warning("Unable to parse idStr: '%s' due to a corrupted string or version error. " + 65 | "Warning, dependents will not be triggered!") 66 | "" 67 | } 68 | } 69 | 70 | /** 71 | * Parses the task id into job arguments 72 | * 73 | * @param taskId 74 | * @return 75 | */ 76 | def getJobArgumentsForTaskId(taskId: String): String = { 77 | require(taskId != null, "taskId cannot be null") 78 | try { 79 | val TaskUtils.taskIdPattern(_, _, _, jobArguments) = taskId 80 | jobArguments 81 | } catch { 82 | case t: Exception => 83 | log.warning("Unable to parse idStr: '%s' due to a corrupted string or version error. " + 84 | "Warning, dependents will not be triggered!") 85 | "" 86 | } 87 | } 88 | 89 | def parseTaskId(id: String): (String, Long, Int, String) = { 90 | val taskIdPattern(due, attempt, jobName, jobArguments) = id 91 | (jobName, due.toLong, attempt.toInt, jobArguments) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/ZookeeperService.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import com.google.common.util.concurrent.AbstractIdleService 4 | import org.apache.curator.framework.CuratorFramework 5 | import org.apache.curator.utils.CloseableUtils 6 | 7 | class ZookeeperService(curator: CuratorFramework) 8 | extends AbstractIdleService { 9 | override def startUp() {} 10 | 11 | override def shutDown() { 12 | CloseableUtils.closeQuietly(curator) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/Constraint.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import org.apache.mesos.Protos 4 | 5 | trait Constraint { 6 | def matches(attributes: Seq[Protos.Attribute]): Boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/EqualsConstraint.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import org.apache.mesos.Protos 4 | 5 | case class EqualsConstraint(attribute: String, value: String) extends Constraint { 6 | 7 | def matches(attributes: Seq[Protos.Attribute]): Boolean = { 8 | attributes.foreach { a => 9 | if (a.getName == attribute && a.getText.getValue == value) { 10 | return true 11 | } 12 | } 13 | false 14 | } 15 | } 16 | 17 | object EqualsConstraint { 18 | val OPERATOR = "EQUALS" 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/LikeConstraint.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import java.util.logging.Logger 4 | 5 | import org.apache.mesos.Protos 6 | 7 | import scala.collection.JavaConversions._ 8 | 9 | case class LikeConstraint(attribute: String, value: String) extends Constraint { 10 | 11 | val regex = value.r 12 | 13 | private[this] val log = Logger.getLogger(getClass.getName) 14 | 15 | def matches(attributes: Seq[Protos.Attribute]): Boolean = { 16 | attributes.find(a => a.getName == attribute).exists { a => 17 | a.getType match { 18 | case Protos.Value.Type.SCALAR => 19 | regex.pattern.matcher(a.getScalar.getValue.toString).matches() 20 | case Protos.Value.Type.SET => 21 | a.getSet.getItemList.exists(regex.pattern.matcher(_).matches()) 22 | case Protos.Value.Type.TEXT => 23 | regex.pattern.matcher(a.getText.getValue).matches() 24 | case Protos.Value.Type.RANGES => 25 | log.warning("Like constraint does not support attributes of type RANGES") 26 | false 27 | case _ => 28 | val t = a.getType.getNumber 29 | log.warning(s"Unknown constraint with number $t in Like constraint") 30 | false 31 | } 32 | } 33 | } 34 | } 35 | 36 | object LikeConstraint { 37 | val OPERATOR = "LIKE" 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/UnlikeConstraint.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import java.util.logging.Logger 4 | 5 | import org.apache.mesos.Protos 6 | 7 | import scala.collection.JavaConversions._ 8 | 9 | case class UnlikeConstraint(attribute: String, value: String) extends Constraint { 10 | 11 | val regex = value.r 12 | 13 | private[this] val log = Logger.getLogger(getClass.getName) 14 | 15 | def matches(attributes: Seq[Protos.Attribute]): Boolean = { 16 | attributes.find(a => a.getName == attribute).exists { a => 17 | a.getType match { 18 | case Protos.Value.Type.SCALAR => 19 | !regex.pattern.matcher(a.getScalar.getValue.toString).matches() 20 | case Protos.Value.Type.SET => 21 | !a.getSet.getItemList.exists(regex.pattern.matcher(_).matches()) 22 | case Protos.Value.Type.TEXT => 23 | !regex.pattern.matcher(a.getText.getValue).matches() 24 | case Protos.Value.Type.RANGES => 25 | log.warning("Unlike constraint does not support attributes of type RANGES") 26 | false 27 | case _ => 28 | val t = a.getType.getNumber 29 | log.warning(s"Unknown constraint with number $t in Unlike constraint") 30 | false 31 | } 32 | } 33 | } 34 | } 35 | 36 | object UnlikeConstraint { 37 | val OPERATOR = "UNLIKE" 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/jobs/graph/Exporter.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.graph 2 | 3 | import java.io._ 4 | 5 | import org.apache.mesos.chronos.scheduler.graph.JobGraph 6 | import org.apache.mesos.chronos.scheduler.jobs.BaseJob 7 | import org.apache.mesos.chronos.scheduler.jobs.stats.JobStats 8 | import org.jgrapht.graph.DefaultEdge 9 | import org.joda.time.DateTime 10 | 11 | import scala.collection.mutable 12 | 13 | /** 14 | * @author Florian Leibert (flo@leibert.de) 15 | */ 16 | object Exporter { 17 | 18 | def export(w: Writer, jobGraph: JobGraph, jobStats: JobStats) { 19 | val dag = jobGraph.dag 20 | val jobMap = new mutable.HashMap[String, BaseJob] 21 | import scala.collection.JavaConversions._ 22 | dag.vertexSet.flatMap(jobGraph.lookupVertex).foreach(x => jobMap.put(x.name, x)) 23 | jobMap.foreach({ case (k, v) => w.write("node,%s,%s,%s\n".format(k, getLastState(v).toString, jobStats.getJobState(k).toString)) }) 24 | for (e: DefaultEdge <- dag.edgeSet) { 25 | val source = dag.getEdgeSource(e) 26 | val target = dag.getEdgeTarget(e) 27 | w.write("link,%s,%s\n".format(source, target)) 28 | } 29 | } 30 | 31 | def getLastState(job: BaseJob) = { 32 | if (job.lastSuccess.isEmpty && job.lastError.isEmpty) LastState.fresh 33 | else if (job.lastSuccess.isEmpty) LastState.failure 34 | else if (job.lastError.isEmpty) LastState.success 35 | else { 36 | val lastSuccessTime = DateTime.parse(job.lastSuccess) 37 | val lastErrorTime = DateTime.parse(job.lastError) 38 | if (lastSuccessTime.isAfter(lastErrorTime)) LastState.success 39 | else LastState.failure 40 | } 41 | } 42 | 43 | object LastState extends Enumeration { 44 | type LastState = Value 45 | val success, failure, fresh = Value 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintChecker.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import java.util.logging.Logger 4 | 5 | import org.apache.mesos.Protos 6 | import org.apache.mesos.chronos.scheduler.jobs.constraints.Constraint 7 | 8 | import scala.collection.JavaConverters._ 9 | 10 | /** 11 | * Helper for checking resource offer against job constraints 12 | */ 13 | object ConstraintChecker { 14 | val Hostname = "hostname" 15 | private[this] val log = Logger.getLogger(getClass.getName) 16 | 17 | def checkConstraints(offer: Protos.Offer, constraints: Seq[Constraint]): Boolean = { 18 | var attributes = offer.getAttributesList.asScala 19 | 20 | if (!attributes.exists(attr => attr.getName == Hostname)) { 21 | log.fine(s"adding hostname-attribute=${offer.getHostname} to offer=${offer}") 22 | val hostnameText = Protos.Value.Text.newBuilder().setValue(offer.getHostname).build() 23 | val hostnameAttribute = Protos.Attribute.newBuilder().setName(Hostname).setText(hostnameText).setType(Protos.Value.Type.TEXT).build() 24 | attributes = offer.getAttributesList.asScala :+ hostnameAttribute 25 | } 26 | 27 | constraints.forall(_.matches(attributes)) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosDriverFactory.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import java.util.logging.Logger 4 | 5 | import mesosphere.chaos.http.HttpConf 6 | import mesosphere.mesos.util.FrameworkIdUtil 7 | import org.apache.mesos.Protos.{FrameworkID, Status} 8 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 9 | import org.apache.mesos.{Scheduler, SchedulerDriver} 10 | 11 | /** 12 | * The chronos driver doesn't allow calling the start() method after stop() has been called, thus we need a factory to 13 | * create a new driver once we call stop() - which will be called if the leader abdicates or is no longer a leader. 14 | * 15 | * @author Florian Leibert (flo@leibert.de) 16 | */ 17 | class MesosDriverFactory( 18 | scheduler: Scheduler, 19 | frameworkIdUtil: FrameworkIdUtil, 20 | config: SchedulerConfiguration with HttpConf, 21 | schedulerDriverBuilder: SchedulerDriverBuilder = new SchedulerDriverBuilder) { 22 | 23 | private[this] val log = Logger.getLogger(getClass.getName) 24 | 25 | var mesosDriver: Option[SchedulerDriver] = None 26 | 27 | def start(): Unit = { 28 | val status = get.start() 29 | if (status != Status.DRIVER_RUNNING) { 30 | log.severe(s"MesosSchedulerDriver start resulted in status: $status. Committing suicide!") 31 | System.exit(1) 32 | } 33 | } 34 | 35 | def get: SchedulerDriver = { 36 | if (mesosDriver.isEmpty) { 37 | mesosDriver = Some(makeDriver()) 38 | } 39 | mesosDriver.get 40 | } 41 | 42 | private[this] def makeDriver(): SchedulerDriver = { 43 | import mesosphere.util.BackToTheFuture.Implicits.defaultTimeout 44 | 45 | import scala.concurrent.ExecutionContext.Implicits.global 46 | 47 | val maybeFrameworkID: Option[FrameworkID] = frameworkIdUtil.fetch 48 | schedulerDriverBuilder.newDriver(config, maybeFrameworkID, scheduler) 49 | } 50 | 51 | def close(): Unit = { 52 | assert(mesosDriver.nonEmpty, "Attempted to close a non initialized driver") 53 | if (mesosDriver.isEmpty) { 54 | log.severe("Attempted to close a non initialized driver") 55 | System.exit(1) 56 | } 57 | 58 | mesosDriver.get.stop(true) 59 | mesosDriver = None 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosOfferReviver.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | /** 4 | * Request offers from Mesos that we have already seen because we have new launching requirements. 5 | */ 6 | trait MesosOfferReviver { 7 | def reviveOffers(): Unit 8 | } 9 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosOfferReviverActor.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import akka.actor.{Actor, ActorLogging, Cancellable, Props} 4 | import akka.event.LoggingReceive 5 | import com.codahale.metrics.MetricRegistry 6 | import org.apache.mesos.Protos.Status 7 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 8 | import org.apache.mesos.chronos.utils.Timestamp 9 | 10 | import scala.concurrent.duration._ 11 | 12 | object MesosOfferReviverActor { 13 | final val NAME = "mesosOfferReviver" 14 | 15 | def props(config: SchedulerConfiguration, mesosDriverFactory: MesosDriverFactory, registry: MetricRegistry): Props = { 16 | Props(new MesosOfferReviverActor(config, mesosDriverFactory, registry)) 17 | } 18 | } 19 | 20 | /** 21 | * Revive offers whenever interest is signaled but maximally every 5 seconds. 22 | */ 23 | class MesosOfferReviverActor( 24 | config: SchedulerConfiguration, 25 | mesosDriverFactory: MesosDriverFactory, 26 | registry: MetricRegistry) extends Actor with ActorLogging { 27 | private[this] val reviveCounter = registry.counter( 28 | MetricRegistry.name(classOf[MesosOfferReviver], "reviveOffersCount")) 29 | private[this] var lastRevive: Timestamp = Timestamp(0) 30 | private[this] var nextReviveCancellableOpt: Option[Cancellable] = None 31 | 32 | override def receive: Receive = LoggingReceive { 33 | case MesosOfferReviverDelegate.ReviveOffers => 34 | log.info("Received request to revive offers") 35 | reviveOffers() 36 | } 37 | 38 | private[this] def reviveOffers(): Unit = { 39 | val now: Timestamp = Timestamp.now() 40 | val nextRevive: Timestamp = lastRevive + config.minReviveOffersInterval().milliseconds 41 | 42 | if (nextRevive <= now) { 43 | log.info("=> reviving offers NOW, canceling any scheduled revives") 44 | nextReviveCancellableOpt.foreach(_.cancel()) 45 | nextReviveCancellableOpt = None 46 | 47 | if (mesosDriverFactory.get.reviveOffers() != Status.DRIVER_RUNNING) { 48 | throw new RuntimeException("Driver is no longer running") 49 | } 50 | lastRevive = now 51 | 52 | reviveCounter.inc() 53 | } 54 | else { 55 | lazy val untilNextRevive = now until nextRevive 56 | if (nextReviveCancellableOpt.isEmpty) { 57 | log.info("=> Scheduling next revive at {} in {}, adhering to --{} {} (ms)", 58 | nextRevive, untilNextRevive, config.minReviveOffersInterval.name, config.minReviveOffersInterval()) 59 | nextReviveCancellableOpt = Some(scheduleCheck(untilNextRevive)) 60 | } 61 | else { 62 | log.info("=> Next revive already scheduled at {}, due in {}", nextRevive, untilNextRevive) 63 | } 64 | } 65 | } 66 | 67 | protected def scheduleCheck(duration: FiniteDuration): Cancellable = { 68 | import context.dispatcher 69 | context.system.scheduler.scheduleOnce(duration, self, MesosOfferReviverDelegate.ReviveOffers) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosOfferReviverDelegate.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import akka.actor.ActorRef 4 | import com.codahale.metrics.MetricRegistry 5 | 6 | object MesosOfferReviverDelegate { 7 | 8 | case object ReviveOffers 9 | 10 | } 11 | 12 | class MesosOfferReviverDelegate(offerReviverRef: ActorRef, registry: MetricRegistry) extends MesosOfferReviver { 13 | val reviveOffersRequestCounter = registry.counter( 14 | MetricRegistry.name(classOf[MesosOfferReviver], "reviveOffersRequestCount")) 15 | 16 | override def reviveOffers(): Unit = { 17 | reviveOffersRequestCounter.inc() 18 | offerReviverRef ! MesosOfferReviverDelegate.ReviveOffers 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/mesos/SchedulerDriverBuilder.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import java.io.{FileInputStream, IOException} 4 | import java.nio.file.attribute.PosixFilePermission 5 | import java.nio.file.{Files, Paths} 6 | import java.util.logging.Logger 7 | 8 | import com.google.protobuf.ByteString 9 | import mesosphere.chaos.http.HttpConf 10 | import org.apache.mesos.Protos.{Credential, FrameworkID, FrameworkInfo} 11 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 12 | import org.apache.mesos.{ 13 | MesosSchedulerDriver, 14 | Protos, 15 | Scheduler, 16 | SchedulerDriver 17 | } 18 | 19 | import scala.collection.JavaConverters.asScalaSetConverter 20 | 21 | class SchedulerDriverBuilder { 22 | private[this] val log = Logger.getLogger(getClass.getName) 23 | 24 | def newDriver(config: SchedulerConfiguration with HttpConf, 25 | frameworkId: Option[FrameworkID], 26 | scheduler: Scheduler): SchedulerDriver = { 27 | def buildCredentials(principal: String, secretFile: String): Credential = { 28 | val credentialBuilder = 29 | Credential 30 | .newBuilder() 31 | .setPrincipalBytes(ByteString.copyFromUtf8(principal)) 32 | 33 | try { 34 | val secretBytes = ByteString.readFrom(new FileInputStream(secretFile)) 35 | 36 | val filePermissions = 37 | Files.getPosixFilePermissions(Paths.get(secretFile)).asScala 38 | if ((filePermissions & Set(PosixFilePermission.OTHERS_READ, 39 | PosixFilePermission.OTHERS_WRITE)).nonEmpty) 40 | log.warning( 41 | s"Secret file $secretFile should not be globally accessible.") 42 | 43 | credentialBuilder.setSecretBytes(secretBytes) 44 | } catch { 45 | case cause: Throwable => 46 | throw new IOException( 47 | s"Error reading authentication secret from file [$secretFile]", 48 | cause) 49 | } 50 | 51 | credentialBuilder.build() 52 | } 53 | 54 | def buildFrameworkInfo( 55 | config: SchedulerConfiguration with HttpConf, 56 | frameworkId: Option[FrameworkID]): Protos.FrameworkInfo = { 57 | val frameworkInfoBuilder = FrameworkInfo 58 | .newBuilder() 59 | .setName(config.mesosFrameworkName()) 60 | .setCheckpoint(config.mesosCheckpoint()) 61 | .setRole(config.mesosRole()) 62 | .setFailoverTimeout(config.failoverTimeoutSeconds()) 63 | .setUser(config.user()) 64 | .setHostname(config.hostname()) 65 | 66 | // Set the ID, if provided 67 | frameworkId.foreach(frameworkInfoBuilder.setId) 68 | 69 | if (config.webuiUrl.isSupplied) { 70 | frameworkInfoBuilder.setWebuiUrl(config.webuiUrl()) 71 | } else if (config.sslKeystorePath.isDefined) { 72 | // ssl enabled, use https 73 | frameworkInfoBuilder.setWebuiUrl( 74 | s"https://${config.hostname()}:${config.httpsPort()}") 75 | } else { 76 | // ssl disabled, use http 77 | frameworkInfoBuilder.setWebuiUrl( 78 | s"http://${config.hostname()}:${config.httpPort()}") 79 | } 80 | 81 | // set the authentication principal, if provided 82 | config.mesosAuthenticationPrincipal.get 83 | .foreach(frameworkInfoBuilder.setPrincipal) 84 | 85 | frameworkInfoBuilder.build() 86 | } 87 | 88 | val frameworkInfo: FrameworkInfo = buildFrameworkInfo(config, frameworkId) 89 | 90 | val credential: Option[Credential] = 91 | config.mesosAuthenticationPrincipal.get.flatMap { principal => 92 | config.mesosAuthenticationSecretFile.get.map { secretFile => 93 | buildCredentials(principal, secretFile) 94 | } 95 | } 96 | 97 | credential match { 98 | case Some(cred) => 99 | new MesosSchedulerDriver(scheduler, 100 | frameworkInfo, 101 | config.master(), 102 | cred) 103 | 104 | case None => 105 | new MesosSchedulerDriver(scheduler, frameworkInfo, config.master()) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/state/MesosStatePersistenceStore.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.state 2 | 3 | import java.util.logging.{Level, Logger} 4 | 5 | import com.google.inject.Inject 6 | import org.apache.curator.framework.CuratorFramework 7 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 8 | import org.apache.mesos.chronos.scheduler.jobs._ 9 | import org.apache.mesos.state.{InMemoryState, State} 10 | 11 | /** 12 | * Handles storage and retrieval of job and task level data within the cluster. 13 | * 14 | * @author Florian Leibert (flo@leibert.de) 15 | */ 16 | 17 | class MesosStatePersistenceStore @Inject()(val zk: CuratorFramework, 18 | val config: SchedulerConfiguration, 19 | val state: State = new InMemoryState) 20 | extends PersistenceStore { 21 | 22 | val log = Logger.getLogger(getClass.getName) 23 | val lock = new Object 24 | 25 | //TODO(FL): Redo string parsing once namespacing the states is implemented in chronos. 26 | //TODO(FL): Add task serialization such that in-flight tasks are accounted for. 27 | //TODO(FL): Add proper retry logic into the persistence. 28 | val jobPrefix = "J_" 29 | val taskPrefix = "T_" 30 | 31 | // There are many jobs in the system at any given point in time. 32 | val jobName = (name: String) => "%s%s".format(jobPrefix, name) 33 | 34 | // There are only few tasks (i.e. active tasks) in the system. 35 | val taskName = (name: String) => "%s%s".format(taskPrefix, name) 36 | 37 | /** 38 | * Retries a function 39 | * 40 | * @param max the maximum retries 41 | * @param attempt the current attempt number 42 | * @param i the input 43 | * @param fnc the function to wrap 44 | * @tparam I the input parameter type 45 | * @tparam O the output parameter type 46 | * @return either Some(instanceOf[O]) or None if more exceptions occurred than permitted by max. 47 | */ 48 | def retry[I, O](max: Int, attempt: Int, i: I, fnc: (I) => O): Option[O] = { 49 | try { 50 | Some(fnc(i)) 51 | } catch { 52 | case t: Exception => if (attempt < max) { 53 | log.log(Level.WARNING, "Retrying attempt:" + attempt, t) 54 | retry(max, attempt + 1, i, fnc) 55 | } else { 56 | log.severe("Giving up after attempts:" + attempt) 57 | None 58 | } 59 | } 60 | } 61 | 62 | def persistJob(job: BaseJob): Boolean = { 63 | log.info("Persisting job '%s' with data '%s'" format(job.name, job.toString)) 64 | persistData(jobName(job.name), JobUtils.toBytes(job)) 65 | } 66 | 67 | private def persistData(name: String, data: Array[Byte]): Boolean = { 68 | val existingVar = state.fetch(name).get 69 | 70 | if (existingVar.value.size == 0) { 71 | log.info("State %s does not exist yet. Adding to state".format(name)) 72 | } else { 73 | log.info("Key for state exists already: %s".format(name)) 74 | } 75 | 76 | val newVar = state.store(existingVar.mutate(data)) 77 | 78 | // to avoid throw NullPointerException 79 | if (newVar.get == null) { 80 | log.warning("State update failed.") 81 | return false 82 | } 83 | 84 | val success = newVar.get.value.deep == data.deep 85 | 86 | log.info("State update successful: " + success) 87 | success 88 | } 89 | 90 | def removeJob(job: BaseJob) { 91 | log.fine("Removing job:" + job.name) 92 | remove(jobName(job.name)) 93 | } 94 | 95 | def getJob(name: String): BaseJob = { 96 | val bytes = state.fetch(jobName(name)).get 97 | JobUtils.fromBytes(bytes.value) 98 | } 99 | 100 | def getJobs: Iterator[BaseJob] = { 101 | 102 | import scala.collection.JavaConversions._ 103 | 104 | state.names.get.filter(_.startsWith(jobPrefix)) 105 | .map({ 106 | x: String => JobUtils.fromBytes(state.fetch(x).get.value) 107 | }) 108 | } 109 | 110 | private def remove(name: String): Boolean = { 111 | try { 112 | log.info("Purging entry '%s' via: %s".format(name, state.getClass.getName)) 113 | val path = "%s/%s".format(config.zooKeeperStatePath, name) 114 | 115 | //Once state supports deletion, we can remove the ZK wiring. 116 | def fnc(s: String) { 117 | if (zk.checkExists().forPath(path) != null) { 118 | zk.delete().forPath(path) 119 | } 120 | } 121 | 122 | retry[String, Unit](2, 0, path, fnc) 123 | zk.checkExists().forPath(path) == null 124 | } catch { 125 | case t: Exception => { 126 | log.log(Level.WARNING, "Error while deleting zookeeper node: %s".format(name), t) 127 | } 128 | false 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/scheduler/state/PersistenceStore.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.state 2 | 3 | import org.apache.mesos.chronos.scheduler.jobs._ 4 | 5 | /** 6 | * @author Florian Leibert (flo@leibert.de) 7 | */ 8 | trait PersistenceStore { 9 | 10 | /** 11 | * Persists a job with the underlying persistence store 12 | * 13 | * @param job 14 | * @return 15 | */ 16 | def persistJob(job: BaseJob): Boolean 17 | 18 | /** 19 | * Removes a job from the ZooKeeperState abstraction. 20 | * 21 | * @param job the job to remove. 22 | * @return true if the job was saved, false if the job couldn't be saved. 23 | */ 24 | def removeJob(job: BaseJob) 25 | 26 | /** 27 | * Loads a job from the underlying store 28 | * 29 | * @param name 30 | * @return 31 | */ 32 | def getJob(name: String): BaseJob 33 | 34 | /** 35 | * Returns all jobs from the underlying store 36 | * 37 | * @return 38 | */ 39 | def getJobs: Iterator[BaseJob] 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/utils/Supervisor.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.utils 2 | 3 | import akka.actor.{Actor, Props} 4 | 5 | class Supervisor extends Actor { 6 | 7 | import akka.actor.OneForOneStrategy 8 | import akka.actor.SupervisorStrategy._ 9 | 10 | import scala.concurrent.duration._ 11 | 12 | override val supervisorStrategy = 13 | OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { 14 | case _: ArithmeticException => Resume 15 | case _: NullPointerException => Restart 16 | case _: IllegalArgumentException => Stop 17 | case _: Exception => Escalate 18 | } 19 | 20 | def receive = { 21 | case p: Props => sender() ! context.actorOf(p) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/org/apache/mesos/chronos/utils/Timestamp.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.utils 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import org.joda.time.{DateTime, DateTimeZone} 6 | 7 | import scala.concurrent.duration.FiniteDuration 8 | import scala.math.Ordered 9 | 10 | /** 11 | * An ordered wrapper for UTC timestamps. 12 | */ 13 | abstract case class Timestamp private(utcDateTime: DateTime) extends Ordered[Timestamp] { 14 | def compare(that: Timestamp): Int = this.utcDateTime compareTo that.utcDateTime 15 | 16 | override def toString: String = utcDateTime.toString 17 | 18 | def toDateTime: DateTime = utcDateTime 19 | 20 | def until(other: Timestamp): FiniteDuration = { 21 | val millis = other.utcDateTime.getMillis - utcDateTime.getMillis 22 | FiniteDuration(millis, TimeUnit.MILLISECONDS) 23 | } 24 | 25 | def +(duration: FiniteDuration): Timestamp = Timestamp(utcDateTime.getMillis + duration.toMillis) 26 | 27 | def -(duration: FiniteDuration): Timestamp = Timestamp(utcDateTime.getMillis - duration.toMillis) 28 | } 29 | 30 | object Timestamp { 31 | /** 32 | * Returns a new Timestamp representing the supplied time. 33 | * 34 | * See the Joda time documentation for a description of acceptable formats: 35 | * http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTimeParser() 36 | */ 37 | def apply(time: String): Timestamp = Timestamp(DateTime.parse(time)) 38 | 39 | /** 40 | * Returns a new Timestamp representing the current instant. 41 | */ 42 | def now(): Timestamp = Timestamp(System.currentTimeMillis) 43 | 44 | /** 45 | * Returns a new Timestamp representing the instant that is the supplied 46 | * number of milliseconds after the epoch. 47 | */ 48 | def apply(ms: Long): Timestamp = Timestamp(new DateTime(ms)) 49 | 50 | /** 51 | * Returns a new Timestamp representing the instant that is the supplied 52 | * currentDateTime converted to UTC. 53 | */ 54 | def apply(dateTime: DateTime): Timestamp = new Timestamp(dateTime.toDateTime(DateTimeZone.UTC)) {} 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/resources/logging.properties: -------------------------------------------------------------------------------- 1 | handlers=java.util.logging.ConsoleHandler 2 | java.util.logging.ConsoleHandler.level=WARNING 3 | java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter 4 | 5 | root.level=WARNING 6 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/ChronosTestHelper.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos 2 | 3 | import mesosphere.chaos.http.HttpConf 4 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 5 | import org.rogach.scallop.ScallopConf 6 | 7 | object ChronosTestHelper { 8 | def makeConfig(args: String*): SchedulerConfiguration with HttpConf = { 9 | val opts = new ScallopConf(args) with SchedulerConfiguration with HttpConf { 10 | // scallop will trigger sys exit 11 | override protected def onError(e: Throwable): Unit = throw e 12 | } 13 | opts.verify() 14 | opts 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/jobs/MockJobUtils.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import com.codahale.metrics.MetricRegistry 4 | import com.google.common.util.concurrent.ListeningScheduledExecutorService 5 | import org.apache.mesos.Protos.{Filters, OfferID, Status} 6 | import org.apache.mesos.SchedulerDriver 7 | import org.apache.mesos.chronos.ChronosTestHelper.makeConfig 8 | import org.apache.mesos.chronos.scheduler.graph.JobGraph 9 | import org.apache.mesos.chronos.scheduler.mesos.{MesosDriverFactory, MesosOfferReviver} 10 | import org.apache.mesos.chronos.scheduler.state.PersistenceStore 11 | import org.specs2.mock._ 12 | 13 | object MockJobUtils extends Mockito { 14 | def mockScheduler(taskManager: TaskManager, 15 | jobGraph: JobGraph, 16 | persistenceStore: PersistenceStore = mock[PersistenceStore], 17 | jobsObserver: JobsObserver.Observer = mock[JobsObserver.Observer]): JobScheduler = 18 | new JobScheduler(taskManager, jobGraph, persistenceStore, 19 | jobMetrics = mock[JobMetrics], jobsObserver = jobsObserver) 20 | 21 | def mockDriverFactory: MesosDriverFactory = { 22 | val mockSchedulerDriver = mock[SchedulerDriver] 23 | mockSchedulerDriver.reviveOffers() returns Status.DRIVER_RUNNING 24 | mockSchedulerDriver.declineOffer(any[OfferID], any[Filters]) returns Status.DRIVER_RUNNING 25 | val mesosDriverFactory = mock[MesosDriverFactory] 26 | mesosDriverFactory.get returns mockSchedulerDriver 27 | } 28 | 29 | def mockTaskManager: TaskManager = { 30 | val mockJobGraph = mock[JobGraph] 31 | val mockPersistencStore: PersistenceStore = mock[PersistenceStore] 32 | val mockMesosOfferReviver = mock[MesosOfferReviver] 33 | val config = makeConfig() 34 | 35 | val mockTaskManager = new TaskManager(mock[ListeningScheduledExecutorService], mockPersistencStore, 36 | mockJobGraph, null, MockJobUtils.mockFullObserver, mock[MetricRegistry], config, mockMesosOfferReviver) 37 | mockTaskManager 38 | } 39 | 40 | def mockFullObserver: JobsObserver.Observer = { 41 | val observer = mock[JobsObserver.Observer] 42 | observer.apply(any[JobEvent]) returns Unit 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/jobs/TaskUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs 2 | 3 | import org.joda.time._ 4 | import org.specs2.mock._ 5 | import org.specs2.mutable._ 6 | 7 | class TaskUtilsSpec extends SpecificationWithJUnit with Mockito { 8 | 9 | "TaskUtils" should { 10 | "Get taskId" in { 11 | val schedule = "R/2012-01-01T00:00:01.000Z/P1M" 12 | val arguments = "-a 1 -b 2" 13 | val cmdArgs = "-c 1 -d 2" 14 | val job1 = new ScheduleBasedJob(schedule, "sample-name", "sample-command", arguments = List(arguments)) 15 | val job2 = new ScheduleBasedJob(schedule, "sample-name", "sample-command") 16 | val job3 = new ScheduleBasedJob(schedule, "sample-name", "sample-command", arguments = List(arguments)) 17 | val ts = 1420843781398L 18 | val due = new DateTime(ts) 19 | 20 | val taskIdOne = TaskUtils.getTaskId(job1, due, 0) 21 | val taskIdTwo = TaskUtils.getTaskId(job2, due, 0) 22 | val taskIdThree = TaskUtils.getTaskId(job3, due, 0, Option(cmdArgs)) 23 | val taskIdFour = TaskUtils.getTaskId(job2, due, 0, Option(cmdArgs)) 24 | 25 | taskIdOne must_== "ct:1420843781398:0:sample-name:" + arguments 26 | taskIdTwo must_== "ct:1420843781398:0:sample-name:" 27 | taskIdThree must_== "ct:1420843781398:0:sample-name:" + cmdArgs // test override 28 | taskIdFour must_== "ct:1420843781398:0:sample-name:" + cmdArgs // test adding args 29 | } 30 | 31 | "Get job arguments for taskId" in { 32 | val arguments = "-a 1 -b 2" 33 | var taskId = "ct:1420843781398:0:test:" + arguments 34 | val jobArguments = TaskUtils.getJobArgumentsForTaskId(taskId) 35 | 36 | jobArguments must_== arguments 37 | } 38 | 39 | "Disable command injection" in { 40 | val schedule = "R/2012-01-01T00:00:01.000Z/P1M" 41 | val cmdArgs = "-c 1 ; ./evil.sh" 42 | val expectedArgs = "-c 1 ./evil.sh" 43 | val job1 = new ScheduleBasedJob(schedule, "sample-name", "sample-command") 44 | val ts = 1420843781398L 45 | val due = new DateTime(ts) 46 | 47 | val taskIdOne = TaskUtils.getTaskId(job1, due, 0, Option(cmdArgs)) 48 | 49 | taskIdOne must_== "ct:1420843781398:0:sample-name:" + expectedArgs 50 | } 51 | 52 | "Parse taskId" in { 53 | val arguments = "-a 1 -b 2" 54 | val arguments2 = "-a 1:2 --B test" 55 | 56 | val taskIdOne = "ct:1420843781398:0:test:" + arguments 57 | val (jobName, jobDue, attempt, jobArguments) = TaskUtils.parseTaskId(taskIdOne) 58 | 59 | jobName must_== "test" 60 | jobDue must_== 1420843781398L 61 | attempt must_== 0 62 | jobArguments must_== arguments 63 | 64 | val taskIdTwo = "ct:1420843781398:0:test:" + arguments2 65 | val (_, _, _, jobArguments2) = TaskUtils.parseTaskId(taskIdTwo) 66 | 67 | jobArguments2 must_== arguments2 68 | 69 | val taskIdThree = "ct:1420843781398:0:test" 70 | val (jobName3, _, _, jobArguments3) = TaskUtils.parseTaskId(taskIdThree) 71 | 72 | jobName3 must_== "test" 73 | jobArguments3 must_== "" 74 | } 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/ConstraintSpecHelper.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import org.apache.mesos.Protos 4 | 5 | trait ConstraintSpecHelper { 6 | def createTextAttribute(name: String, value: String): Protos.Attribute = { 7 | Protos.Attribute.newBuilder() 8 | .setName(name) 9 | .setText(Protos.Value.Text.newBuilder().setValue(value)) 10 | .setType(Protos.Value.Type.TEXT) 11 | .build() 12 | } 13 | 14 | def createScalarAttribute(name: String, value: Double): Protos.Attribute = { 15 | Protos.Attribute.newBuilder() 16 | .setName(name) 17 | .setScalar(Protos.Value.Scalar.newBuilder().setValue(value)) 18 | .setType(Protos.Value.Type.SCALAR) 19 | .build() 20 | } 21 | 22 | def createSetAttribute(name: String, value: Array[String]): Protos.Attribute = { 23 | val set = Protos.Attribute.newBuilder() 24 | .setName(name) 25 | .setType(Protos.Value.Type.SET) 26 | 27 | val builder = Protos.Value.Set.newBuilder() 28 | value.foreach(builder.addItem) 29 | set.setSet(builder) 30 | 31 | set.build() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/EqualsConstraintSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import org.specs2.mutable.SpecificationWithJUnit 4 | 5 | class EqualsConstraintSpec extends SpecificationWithJUnit 6 | with ConstraintSpecHelper { 7 | 8 | "matches attributes" in { 9 | val attributes = List(createTextAttribute("dc", "north"), createTextAttribute("rack", "rack-1")) 10 | 11 | val constraint = EqualsConstraint("rack", "rack-1") 12 | 13 | constraint.matches(attributes) must_== true 14 | } 15 | 16 | "does not match attributes" in { 17 | val attributes = List(createTextAttribute("dc", "north"), createTextAttribute("rack", "rack-1")) 18 | 19 | val constraint = EqualsConstraint("rack", "rack-2") 20 | 21 | constraint.matches(attributes) must_== false 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/LikeConstraintSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import java.util.regex.PatternSyntaxException 4 | 5 | import org.specs2.mutable.SpecificationWithJUnit 6 | 7 | class LikeConstraintSpec extends SpecificationWithJUnit 8 | with ConstraintSpecHelper { 9 | "matches attributes of type text" in { 10 | val attributes = List(createTextAttribute("dc", "north"), createTextAttribute("rack", "rack-1")) 11 | val constraint = LikeConstraint("rack", "rack-[1-3]") 12 | 13 | constraint.matches(attributes) must_== true 14 | 15 | val attributes2 = List(createTextAttribute("dc", "north")) 16 | val constraint2 = LikeConstraint("dc", "north|south") 17 | 18 | constraint2.matches(attributes2) must_== true 19 | } 20 | 21 | "matches attributes of type scalar" in { 22 | val attributes = List(createScalarAttribute("number", 1)) 23 | val constraint = LikeConstraint("number", """\d\.\d""") 24 | 25 | constraint.matches(attributes) must_== true 26 | } 27 | 28 | "matches attributes of type set" in { 29 | val attributes = List(createSetAttribute("dc", Array("north"))) 30 | val constraint = LikeConstraint("dc", "^n.*") 31 | 32 | constraint.matches(attributes) must_== true 33 | 34 | val attributes2 = List(createSetAttribute("dc", Array("south"))) 35 | 36 | constraint.matches(attributes2) must_== false 37 | } 38 | 39 | "does not match attributes" in { 40 | val attributes = List(createTextAttribute("dc", "north"), createTextAttribute("rack", "rack-1")) 41 | 42 | val constraint = LikeConstraint("rack", "rack-[2-3]") 43 | 44 | constraint.matches(attributes) must_== false 45 | } 46 | 47 | "fails in case of an invalid regular expression" in { 48 | LikeConstraint("invalid-regex", "[[[") must throwA[PatternSyntaxException] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/jobs/constraints/UnlikeConstraintSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.constraints 2 | 3 | import java.util.regex.PatternSyntaxException 4 | 5 | import org.specs2.mutable.SpecificationWithJUnit 6 | 7 | class UnlikeConstraintSpec extends SpecificationWithJUnit 8 | with ConstraintSpecHelper { 9 | "matches attributes of type text" in { 10 | val attributes = List(createTextAttribute("dc", "north"), createTextAttribute("rack", "rack-4")) 11 | val constraint = UnlikeConstraint("rack", "rack-[1-3]") 12 | 13 | constraint.matches(attributes) must_== true 14 | 15 | val attributes2 = List(createTextAttribute("dc", "north")) 16 | val constraint2 = UnlikeConstraint("dc", "north|south") 17 | 18 | constraint2.matches(attributes2) must_== false 19 | } 20 | 21 | "matches attributes of type scalar" in { 22 | val attributes = List(createScalarAttribute("number", 1)) 23 | val constraint = UnlikeConstraint("number", """\d\.\d""") 24 | 25 | constraint.matches(attributes) must_== false 26 | 27 | val attributes2 = List(createScalarAttribute("number", 1)) 28 | val constraint2 = UnlikeConstraint("number", """100.\d""") 29 | constraint2.matches(attributes) must_== true 30 | 31 | } 32 | 33 | "matches attributes of type set" in { 34 | val attributes = List(createSetAttribute("dc", Array("north"))) 35 | val constraint = UnlikeConstraint("dc", "^n.*") 36 | 37 | constraint.matches(attributes) must_== false 38 | 39 | val attributes2 = List(createSetAttribute("dc", Array("south"))) 40 | 41 | constraint.matches(attributes2) must_== true 42 | } 43 | 44 | "fails in case of an invalid regular expression" in { 45 | UnlikeConstraint("invalid-regex", "[[[") must throwA[PatternSyntaxException] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/jobs/stats/JobStatsSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.jobs.stats 2 | 3 | import mesosphere.mesos.protos._ 4 | import org.apache.mesos.chronos.scheduler.config.CassandraConfiguration 5 | import org.apache.mesos.chronos.scheduler.jobs._ 6 | import org.specs2.mock.Mockito 7 | import org.specs2.mutable.SpecificationWithJUnit 8 | 9 | class JobStatsSpec extends SpecificationWithJUnit with Mockito { 10 | "JobStats" should { 11 | "Correctly return states" in { 12 | import mesosphere.mesos.protos.Implicits._ 13 | val jobStats = new JobStats(None, mock[CassandraConfiguration]) 14 | 15 | val observer = jobStats.asObserver 16 | 17 | val job1 = ScheduleBasedJob("R5/2012-01-01T00:00:00.000Z/P1D", "job1", "CMD") 18 | 19 | observer.apply(JobQueued(job1, "ct:0:1:job1:", 0)) 20 | jobStats.getJobState("job1") must_== "queued" 21 | observer.apply(JobStarted(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskRunning), 0, 1)) 22 | jobStats.getJobState("job1") must_== "1 running" 23 | observer.apply(JobFinished(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskFinished), 0, 0)) 24 | jobStats.getJobState("job1") must_== "idle" 25 | 26 | observer.apply(JobQueued(job1, "ct:0:1:job1:", 0)) 27 | jobStats.getJobState("job1") must_== "queued" 28 | observer.apply(JobStarted(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskRunning), 0, 1)) 29 | jobStats.getJobState("job1") must_== "1 running" 30 | observer.apply(JobStarted(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskRunning), 0, 2)) 31 | jobStats.getJobState("job1") must_== "2 running" 32 | observer.apply(JobStarted(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskRunning), 0, 3)) 33 | jobStats.getJobState("job1") must_== "3 running" 34 | observer.apply(JobFinished(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskFinished), 0, 2)) 35 | jobStats.getJobState("job1") must_== "2 running" 36 | observer.apply(JobFinished(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskFinished), 0, 1)) 37 | jobStats.getJobState("job1") must_== "1 running" 38 | observer.apply(JobQueued(job1, "ct:0:1:job1:", 0)) 39 | jobStats.getJobState("job1") must_== "1 running" 40 | observer.apply(JobStarted(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskRunning), 0, 2)) 41 | jobStats.getJobState("job1") must_== "2 running" 42 | observer.apply(JobFailed(Right(job1), TaskStatus(TaskID("ct:0:1:job1:"), TaskFailed), 0, 1)) 43 | jobStats.getJobState("job1") must_== "1 running" 44 | observer.apply(JobFinished(job1, TaskStatus(TaskID("ct:0:1:job1:"), TaskFinished), 0, 0)) 45 | jobStats.getJobState("job1") must_== "idle" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintCheckerSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import mesosphere.mesos.protos.Implicits._ 4 | import mesosphere.mesos.protos._ 5 | import org.apache.mesos.Protos 6 | import org.apache.mesos.chronos.scheduler.jobs.constraints.{ConstraintSpecHelper, EqualsConstraint, LikeConstraint} 7 | import org.specs2.mock.Mockito 8 | import org.specs2.mutable.SpecificationWithJUnit 9 | 10 | class ConstraintCheckerSpec extends SpecificationWithJUnit 11 | with Mockito 12 | with ConstraintSpecHelper { 13 | 14 | val offer = Protos.Offer.newBuilder() 15 | .setId(OfferID("1")) 16 | .setFrameworkId(FrameworkID("chronos")) 17 | .setSlaveId(SlaveID("slave-1")) 18 | .setHostname("slave.one.com") 19 | .addAttributes(createTextAttribute("rack", "rack-1")) 20 | .build() 21 | 22 | val offerWithHostname = Protos.Offer.newBuilder() 23 | .setId(OfferID("1")) 24 | .setFrameworkId(FrameworkID("chronos")) 25 | .setSlaveId(SlaveID("slave-1")) 26 | .setHostname("slave.one.com") 27 | .addAttributes(createTextAttribute("hostname", "slave.explicit.com")) 28 | .build() 29 | 30 | "check constraints" should { 31 | 32 | "be true when equal" in { 33 | val constraints = Seq(EqualsConstraint("rack", "rack-1")) 34 | ConstraintChecker.checkConstraints(offer, constraints) must beTrue 35 | } 36 | 37 | "be false when not equal" in { 38 | val constraints = Seq(EqualsConstraint("rack", "rack-2")) 39 | ConstraintChecker.checkConstraints(offer, constraints) must beFalse 40 | } 41 | 42 | "be true when like" in { 43 | val constraints = Seq(LikeConstraint("rack", "rack-[1-3]")) 44 | ConstraintChecker.checkConstraints(offer, constraints) must beTrue 45 | } 46 | 47 | "be false when not like" in { 48 | val constraints = Seq(LikeConstraint("rack", "rack-[2-3]")) 49 | ConstraintChecker.checkConstraints(offer, constraints) must beFalse 50 | } 51 | 52 | "be true when hostname equal" in { 53 | val constraints = Seq(EqualsConstraint("hostname", "slave.one.com")) 54 | ConstraintChecker.checkConstraints(offer, constraints) must beTrue 55 | } 56 | 57 | "be false when hostname not equal" in { 58 | val constraints = Seq(EqualsConstraint("hostname", "slave.two.com")) 59 | ConstraintChecker.checkConstraints(offer, constraints) must beFalse 60 | } 61 | 62 | "be false when hostname explicitly set to something else and not equal" in { 63 | val constraints = Seq(EqualsConstraint("hostname", "slave.one.com")) 64 | ConstraintChecker.checkConstraints(offerWithHostname, constraints) must beFalse 65 | } 66 | 67 | "be true when hostname explicitly set to something else and equal" in { 68 | val constraints = Seq(EqualsConstraint("hostname", "slave.explicit.com")) 69 | ConstraintChecker.checkConstraints(offerWithHostname, constraints) must beTrue 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/mesos/MesosDriverFactorySpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import mesosphere.mesos.util.FrameworkIdUtil 4 | import org.apache.mesos.Scheduler 5 | import org.apache.mesos.chronos.ChronosTestHelper._ 6 | import org.specs2.mock.Mockito 7 | import org.specs2.mutable.SpecificationWithJUnit 8 | 9 | class MesosDriverFactorySpec extends SpecificationWithJUnit with Mockito { 10 | "MesosDriverFactorySpec" should { 11 | "always fetch the frameworkId from the state store before creating a driver" in { 12 | val scheduler: Scheduler = mock[Scheduler] 13 | val frameworkIdUtil: FrameworkIdUtil = mock[FrameworkIdUtil] 14 | val mesosDriverFactory = new MesosDriverFactory( 15 | scheduler, 16 | frameworkIdUtil, 17 | makeConfig(), 18 | mock[SchedulerDriverBuilder]) 19 | 20 | frameworkIdUtil.fetch(any, any).returns(None) 21 | 22 | mesosDriverFactory.get 23 | 24 | there was one(frameworkIdUtil).fetch(any, any) 25 | ok 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/mesos/MesosOfferReviverActorSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import akka.actor._ 4 | import akka.testkit.TestProbe 5 | import com.codahale.metrics.{Counter, MetricRegistry} 6 | import mesosphere.chaos.http.HttpConf 7 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 8 | import org.apache.mesos.chronos.scheduler.jobs.MockJobUtils 9 | import org.specs2.matcher.ThrownExpectations 10 | import org.specs2.mock.Mockito 11 | import org.specs2.mutable._ 12 | 13 | import scala.concurrent.duration.FiniteDuration 14 | 15 | class MesosOfferReviverActorSpec extends SpecificationWithJUnit with Mockito { 16 | "MesosOfferReviverActor" should { 17 | "Call the driver's reviveOffers on ReviveOffers message" in new context { 18 | val actorRef = startActor() 19 | 20 | delegate.reviveOffers() 21 | 22 | // wait for the actor to process all messages and die 23 | actorRef ! PoisonPill 24 | val probe = TestProbe() 25 | probe.watch(actorRef) 26 | probe.expectMsgAnyClassOf(classOf[Terminated]) 27 | 28 | there was one(driverFactory.get).reviveOffers() 29 | } 30 | 31 | "increment the reviveOffersCounter when it revives offers" in new context { 32 | val actorRef = startActor() 33 | 34 | delegate.reviveOffers() 35 | 36 | // wait for the actor to process all messages and die 37 | actorRef ! PoisonPill 38 | val probe = TestProbe() 39 | probe.watch(actorRef) 40 | probe.expectMsgAnyClassOf(classOf[Terminated]) 41 | 42 | actorSystem.shutdown() 43 | actorSystem.awaitTermination() 44 | 45 | there was one(reviveOffersCounter).inc() 46 | } 47 | 48 | "Second revive offers message results in scheduling a call to the driver's reviveOffers method" in new context { 49 | @volatile 50 | var scheduleCheckCalled = 0 51 | 52 | val actorRef = startActor(Props( 53 | new MesosOfferReviverActor(conf, driverFactory, metrics) { 54 | override protected def scheduleCheck(duration: FiniteDuration): Cancellable = { 55 | scheduleCheckCalled += 1 56 | mock[Cancellable] 57 | } 58 | } 59 | )) 60 | 61 | delegate.reviveOffers() 62 | delegate.reviveOffers() 63 | 64 | // wait for the actor to process all messages and die 65 | actorRef ! PoisonPill 66 | val probe = TestProbe() 67 | probe.watch(actorRef) 68 | probe.expectMsgAnyClassOf(classOf[Terminated]) 69 | 70 | there was one(driverFactory.get).reviveOffers() 71 | there was one(reviveOffersCounter).inc() 72 | 73 | scheduleCheckCalled must beEqualTo(1) 74 | } 75 | } 76 | } 77 | 78 | trait context extends BeforeAfter with Mockito with ThrownExpectations { 79 | implicit var actorSystem: ActorSystem = _ 80 | var driverFactory: MesosDriverFactory = _ 81 | var conf: SchedulerConfiguration with HttpConf = _ 82 | var delegate: MesosOfferReviverDelegate = _ 83 | var metrics: MetricRegistry = _ 84 | var reviveOffersCounter: Counter = _ 85 | 86 | def before = { 87 | actorSystem = ActorSystem() 88 | conf = new SchedulerConfiguration with HttpConf {} 89 | conf.verify() 90 | driverFactory = MockJobUtils.mockDriverFactory 91 | 92 | metrics = spy(new MetricRegistry()) 93 | reviveOffersCounter = metrics.register( 94 | MetricRegistry.name(classOf[MesosOfferReviver], "reviveOffersCount"), 95 | mock[Counter] 96 | ) 97 | } 98 | 99 | def after = { 100 | actorSystem.shutdown() 101 | actorSystem.awaitTermination() 102 | } 103 | 104 | def startActor(props: Props = MesosOfferReviverActor.props(conf, driverFactory, metrics)): ActorRef = { 105 | val actorRef = actorSystem.actorOf(props, MesosOfferReviverActor.NAME) 106 | delegate = new MesosOfferReviverDelegate(actorRef, metrics) 107 | 108 | actorRef 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/mesos/MesosOfferReviverDelegateSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import akka.actor.ActorSystem 6 | import akka.testkit.TestProbe 7 | import com.codahale.metrics.{Counter, MetricRegistry} 8 | import org.specs2.mock._ 9 | import org.specs2.mutable._ 10 | 11 | import scala.concurrent.duration._ 12 | 13 | class MesosOfferReviverDelegateSpec extends SpecificationWithJUnit with Mockito { 14 | implicit lazy val system = ActorSystem() 15 | 16 | "MesosOfferReviverDelegate" should { 17 | "Send a ReviveOffers message " in { 18 | val registry = spy(new MetricRegistry()) 19 | val testProbe = TestProbe() 20 | val mesosOfferReviverDelegate = new MesosOfferReviverDelegate(testProbe.ref, registry) 21 | 22 | mesosOfferReviverDelegate.reviveOffers() 23 | 24 | testProbe.expectMsg(FiniteDuration(500, TimeUnit.MILLISECONDS), MesosOfferReviverDelegate.ReviveOffers) 25 | 26 | ok 27 | } 28 | 29 | "Increment the ReviveOfferRequests counter" in { 30 | val registry = spy(new MetricRegistry()) 31 | 32 | val reviveOffersRequestCounter = registry.register( 33 | MetricRegistry.name(classOf[MesosOfferReviver], "reviveOffersRequestCount"), 34 | mock[Counter] 35 | ) 36 | 37 | val testProbe = TestProbe() 38 | val mesosOfferReviverDelegate = new MesosOfferReviverDelegate(testProbe.ref, registry) 39 | 40 | mesosOfferReviverDelegate.reviveOffers() 41 | 42 | there was one(reviveOffersRequestCounter).inc() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/mesos/MesosTaskBuilderSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.mesos 2 | 3 | import org.apache.mesos.Protos._ 4 | import org.apache.mesos.chronos.scheduler.config.SchedulerConfiguration 5 | import org.apache.mesos.chronos.scheduler.jobs.{Label, Parameter, Volume, _} 6 | import org.apache.mesos.chronos.scheduler.jobs.constraints.{EqualsConstraint, LikeConstraint} 7 | import org.specs2.mock.Mockito 8 | import org.specs2.mutable.SpecificationWithJUnit 9 | 10 | import scala.collection.JavaConversions._ 11 | 12 | 13 | class MesosTaskBuilderSpec extends SpecificationWithJUnit with Mockito { 14 | 15 | val taskId = "ct:1454467003926:0:test2Execution:run" 16 | 17 | val (_, start, attempt, _) = TaskUtils.parseTaskId(taskId) 18 | 19 | val offer = Offer.newBuilder().mergeFrom(Offer.getDefaultInstance) 20 | .setHostname("localport") 21 | .setId(OfferID.newBuilder().setValue("123").build()) 22 | .setFrameworkId(FrameworkID.newBuilder().setValue("123").build()) 23 | .setSlaveId(SlaveID.newBuilder().setValue("123").build()) 24 | .build() 25 | 26 | val job = { 27 | val volumes = Seq( 28 | Volume(Option("/host/dir"), "container/dir", Option(VolumeMode.RW), None), 29 | Volume(None, "container/dir", None, None) 30 | ) 31 | 32 | val networks = Seq( 33 | Network("testnet", None, Seq(), Seq()), 34 | Network("testnet", None, Seq(Label("testlabel", "testvalue")), Seq()) 35 | ) 36 | 37 | val parameters = scala.collection.mutable.ListBuffer[Parameter]() 38 | 39 | val container = Container("dockerImage", ContainerType.DOCKER, volumes, parameters, NetworkMode.HOST, None, networks, forcePullImage = true) 40 | 41 | val constraints = Seq( 42 | EqualsConstraint("rack", "rack-1"), 43 | LikeConstraint("rack", "rack-[1-3]") 44 | ) 45 | 46 | ScheduleBasedJob("FOO/BAR/BAM", "AJob", "noop", 10L, 20L, 47 | "fooexec", "fooflags", "none", 7, "foo@bar.com", "Foo", "Test schedule based job", "TODAY", 48 | "YESTERDAY", cpus = 2, disk = 3, mem = 5, container = container, environmentVariables = Seq(), 49 | shell = true, arguments = Seq(), softError = true, constraints = constraints) 50 | } 51 | 52 | val defaultEnv = Map( 53 | "mesos_task_id" -> taskId, 54 | "MESOS_TASK_ID" -> taskId, 55 | "CHRONOS_JOB_OWNER" -> job.owner, 56 | "CHRONOS_JOB_NAME" -> job.name, 57 | "HOST" -> offer.getHostname, 58 | "CHRONOS_RESOURCE_MEM" -> job.mem.toString, 59 | "CHRONOS_RESOURCE_CPU" -> job.cpus.toString, 60 | "CHRONOS_RESOURCE_DISK" -> job.disk.toString, 61 | "CHRONOS_JOB_RUN_TIME" -> start.toString, 62 | "CHRONOS_JOB_RUN_ATTEMPT" -> attempt.toString 63 | ) 64 | 65 | def toMap(envs: Environment): Map[String, String] = 66 | envs.getVariablesList.foldLeft(Map[String, String]())((m, v) => m + (v.getName -> v.getValue)) 67 | 68 | "MesosTaskBuilder" should { 69 | "Setup all the default environment variables" in { 70 | val target = new MesosTaskBuilder(mock[SchedulerConfiguration]) 71 | 72 | defaultEnv must_== toMap(target.envs(taskId, job, offer).build()) 73 | } 74 | } 75 | 76 | "MesosTaskBuilder" should { 77 | "Setup all the default environment variables and job environment variables" in { 78 | val target = new MesosTaskBuilder(mock[SchedulerConfiguration]) 79 | 80 | val testJob = job.copy(environmentVariables = Seq( 81 | EnvironmentVariable("FOO", "BAR"), 82 | EnvironmentVariable("TOM", "JERRY") 83 | )) 84 | 85 | val finalEnv = defaultEnv ++ Map("FOO" -> "BAR", "TOM" -> "JERRY") 86 | 87 | finalEnv must_== toMap(target.envs(taskId, testJob, offer).build()) 88 | } 89 | } 90 | 91 | "MesosTaskBuilder" should { 92 | "Should not allow job environment variables to overwrite any default environment variables" in { 93 | val target = new MesosTaskBuilder(mock[SchedulerConfiguration]) 94 | 95 | val testJob = job.copy(environmentVariables = Seq( 96 | EnvironmentVariable("CHRONOS_RESOURCE_MEM", "10000"), 97 | EnvironmentVariable("CHRONOS_RESOURCE_DISK", "40000") 98 | )) 99 | 100 | defaultEnv must_== toMap(target.envs(taskId, testJob, offer).build()) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/scala/org/apache/mesos/chronos/scheduler/state/PersistenceStoreSpec.scala: -------------------------------------------------------------------------------- 1 | package org.apache.mesos.chronos.scheduler.state 2 | 3 | import org.apache.mesos.chronos.scheduler.jobs._ 4 | import org.specs2.mock._ 5 | import org.specs2.mutable._ 6 | 7 | class PersistenceStoreSpec extends SpecificationWithJUnit with Mockito { 8 | 9 | "MesosStatePersistenceStore" should { 10 | 11 | "Writing and reading ScheduledBasedJob a job works" in { 12 | val store = new MesosStatePersistenceStore(null, null) 13 | val startTime = "R1/2012-01-01T00:00:01.000Z/PT1M" 14 | val job = ScheduleBasedJob(schedule = startTime, name = "sample-name", 15 | command = "sample-command", successCount = 1L, 16 | executor = "fooexecutor", executorFlags = "args", taskInfoData = "SomeData", 17 | maxCompletionTime = 1L) 18 | 19 | store.persistJob(job) 20 | val job2 = store.getJob(job.name) 21 | 22 | job2.name must_== job.name 23 | job2.executor must_== job.executor 24 | job2.taskInfoData must_== job.taskInfoData 25 | job2.successCount must_== job.successCount 26 | job2.command must_== job.command 27 | job2.maxCompletionTime must_== job.maxCompletionTime 28 | 29 | } 30 | 31 | "Writing and reading DependencyBasedJob a job works" in { 32 | val store = new MesosStatePersistenceStore(null, null) 33 | val startTime = "R1/2012-01-01T00:00:01.000Z/PT1M" 34 | val schedJob = ScheduleBasedJob(schedule = startTime, name = "sample-name", 35 | command = "sample-command") 36 | val job = DependencyBasedJob(parents = Set("sample-name"), 37 | name = "sample-dep", command = "sample-command", 38 | softError = true, 39 | successCount = 1L, errorCount = 0L, 40 | executor = "fooexecutor", executorFlags = "-w", taskInfoData = "SomeData", 41 | retries = 1, disabled = false) 42 | 43 | store.persistJob(job) 44 | val job2 = store.getJob(job.name) 45 | 46 | job2.name must_== job.name 47 | job2.command must_== job.command 48 | job2.softError must_== job.softError 49 | job2.successCount must_== job.successCount 50 | job2.errorCount must_== job.errorCount 51 | job2.executor must_== job.executor 52 | job2.executorFlags must_== job.executorFlags 53 | job2.taskInfoData must_== job.taskInfoData 54 | job2.retries must_== job.retries 55 | job2.disabled must_== job.disabled 56 | } 57 | } 58 | 59 | } 60 | --------------------------------------------------------------------------------