dynflow 0.7.6 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +11 -3
- data/doc/pages/.gitignore +7 -0
- data/doc/pages/Gemfile +8 -0
- data/doc/pages/Rakefile +26 -0
- data/doc/pages/_config.yml +38 -0
- data/doc/pages/plugins/alert_block.rb +27 -0
- data/doc/pages/plugins/div_tag.rb +24 -0
- data/doc/pages/plugins/graphviz.rb +121 -0
- data/doc/pages/plugins/plantuml.rb +85 -0
- data/doc/pages/plugins/play.rb +13 -0
- data/doc/pages/plugins/tags.rb +138 -0
- data/doc/pages/plugins/toc.rb +20 -0
- data/doc/pages/source/.nojekyll +0 -0
- data/doc/pages/source/404.md +6 -0
- data/doc/pages/source/_includes/disqus.html +25 -0
- data/doc/pages/source/_includes/google_analytics.html +12 -0
- data/doc/pages/source/_includes/google_plus_one.html +2 -0
- data/doc/pages/source/_includes/menu.html +19 -0
- data/doc/pages/source/_includes/menu_brand.html +2 -0
- data/doc/pages/source/_includes/menu_right.html +1 -0
- data/doc/pages/source/_includes/post_item.html +10 -0
- data/doc/pages/source/_includes/scroll_to.html +24 -0
- data/doc/pages/source/_includes/twitter_sharing.html +9 -0
- data/doc/pages/source/_layouts/default.html +70 -0
- data/doc/pages/source/_layouts/page.html +47 -0
- data/doc/pages/source/_layouts/post.html +19 -0
- data/doc/pages/source/_layouts/presentation.html +39 -0
- data/doc/pages/source/_layouts/tag_page.html +12 -0
- data/doc/pages/source/_sass/_bootstrap-compass.scss +9 -0
- data/doc/pages/source/_sass/_bootstrap-mincer.scss +19 -0
- data/doc/pages/source/_sass/_bootstrap-sprockets.scss +9 -0
- data/doc/pages/source/_sass/_bootstrap-variables.sass +865 -0
- data/doc/pages/source/_sass/_bootstrap.scss +50 -0
- data/doc/pages/source/_sass/_specific.scss +16 -0
- data/doc/pages/source/_sass/_style.scss +172 -0
- data/doc/pages/source/_sass/bootstrap/_alerts.scss +73 -0
- data/doc/pages/source/_sass/bootstrap/_badges.scss +67 -0
- data/doc/pages/source/_sass/bootstrap/_breadcrumbs.scss +26 -0
- data/doc/pages/source/_sass/bootstrap/_button-groups.scss +243 -0
- data/doc/pages/source/_sass/bootstrap/_buttons.scss +160 -0
- data/doc/pages/source/_sass/bootstrap/_carousel.scss +269 -0
- data/doc/pages/source/_sass/bootstrap/_close.scss +36 -0
- data/doc/pages/source/_sass/bootstrap/_code.scss +69 -0
- data/doc/pages/source/_sass/bootstrap/_component-animations.scss +38 -0
- data/doc/pages/source/_sass/bootstrap/_dropdowns.scss +214 -0
- data/doc/pages/source/_sass/bootstrap/_forms.scss +570 -0
- data/doc/pages/source/_sass/bootstrap/_glyphicons.scss +301 -0
- data/doc/pages/source/_sass/bootstrap/_grid.scss +84 -0
- data/doc/pages/source/_sass/bootstrap/_input-groups.scss +166 -0
- data/doc/pages/source/_sass/bootstrap/_jumbotron.scss +50 -0
- data/doc/pages/source/_sass/bootstrap/_labels.scss +66 -0
- data/doc/pages/source/_sass/bootstrap/_list-group.scss +124 -0
- data/doc/pages/source/_sass/bootstrap/_media.scss +61 -0
- data/doc/pages/source/_sass/bootstrap/_mixins.scss +39 -0
- data/doc/pages/source/_sass/bootstrap/_modals.scss +148 -0
- data/doc/pages/source/_sass/bootstrap/_navbar.scss +663 -0
- data/doc/pages/source/_sass/bootstrap/_navs.scss +244 -0
- data/doc/pages/source/_sass/bootstrap/_normalize.scss +427 -0
- data/doc/pages/source/_sass/bootstrap/_pager.scss +54 -0
- data/doc/pages/source/_sass/bootstrap/_pagination.scss +88 -0
- data/doc/pages/source/_sass/bootstrap/_panels.scss +265 -0
- data/doc/pages/source/_sass/bootstrap/_popovers.scss +135 -0
- data/doc/pages/source/_sass/bootstrap/_print.scss +107 -0
- data/doc/pages/source/_sass/bootstrap/_progress-bars.scss +87 -0
- data/doc/pages/source/_sass/bootstrap/_responsive-embed.scss +35 -0
- data/doc/pages/source/_sass/bootstrap/_responsive-utilities.scss +177 -0
- data/doc/pages/source/_sass/bootstrap/_scaffolding.scss +150 -0
- data/doc/pages/source/_sass/bootstrap/_tables.scss +234 -0
- data/doc/pages/source/_sass/bootstrap/_theme.scss +273 -0
- data/doc/pages/source/_sass/bootstrap/_thumbnails.scss +38 -0
- data/doc/pages/source/_sass/bootstrap/_tooltip.scss +103 -0
- data/doc/pages/source/_sass/bootstrap/_type.scss +298 -0
- data/doc/pages/source/_sass/bootstrap/_utilities.scss +56 -0
- data/doc/pages/source/_sass/bootstrap/_variables.scss +862 -0
- data/doc/pages/source/_sass/bootstrap/_wells.scss +29 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_alerts.scss +14 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_background-variant.scss +11 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_border-radius.scss +18 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_buttons.scss +52 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_center-block.scss +7 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_clearfix.scss +22 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_forms.scss +88 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_gradients.scss +58 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_grid-framework.scss +81 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_grid.scss +122 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_hide-text.scss +21 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_image.scss +33 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_labels.scss +12 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_list-group.scss +31 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_nav-divider.scss +10 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_nav-vertical-align.scss +9 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_opacity.scss +8 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_pagination.scss +23 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_panels.scss +24 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_progress-bar.scss +10 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_reset-filter.scss +8 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_resize.scss +6 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_responsive-visibility.scss +21 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_size.scss +10 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_tab-focus.scss +9 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_table-row.scss +28 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_text-emphasis.scss +11 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_text-overflow.scss +8 -0
- data/doc/pages/source/_sass/bootstrap/mixins/_vendor-prefixes.scss +222 -0
- data/doc/pages/source/atom.xml +32 -0
- data/doc/pages/source/bootstrap/config.json +429 -0
- data/doc/pages/source/bootstrap/css/bootstrap-theme.css +479 -0
- data/doc/pages/source/bootstrap/css/bootstrap-theme.min.css +10 -0
- data/doc/pages/source/bootstrap/css/bootstrap.css +6564 -0
- data/doc/pages/source/bootstrap/css/bootstrap.min.css +10 -0
- data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
- data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
- data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
- data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/doc/pages/source/bootstrap/js/bootstrap.js +2309 -0
- data/doc/pages/source/bootstrap/js/bootstrap.min.js +12 -0
- data/doc/pages/source/css/app.scss +10 -0
- data/doc/pages/source/css/syntax.css +60 -0
- data/doc/pages/source/documentation/index.md +977 -0
- data/doc/pages/source/faq/index.md +16 -0
- data/doc/pages/source/images/dynflow-logos.svg +423 -0
- data/doc/pages/source/images/logo-long.png +0 -0
- data/doc/pages/source/images/logo-long.svg +116 -0
- data/doc/pages/source/images/logo-square.png +0 -0
- data/doc/pages/source/images/logo-square.svg +75 -0
- data/doc/pages/source/images/noise.png +0 -0
- data/doc/pages/source/images/screenshot.png +0 -0
- data/doc/pages/source/index.md +64 -0
- data/doc/pages/source/media/index.md +20 -0
- data/doc/pages/source/projects/index.md +32 -0
- data/dynflow.gemspec +2 -3
- data/examples/sub_plans.rb +37 -0
- data/lib/dynflow/action.rb +28 -8
- data/lib/dynflow/action/polling.rb +6 -5
- data/lib/dynflow/action/with_sub_plans.rb +162 -0
- data/lib/dynflow/execution_plan.rb +25 -7
- data/lib/dynflow/execution_plan/steps/abstract.rb +5 -2
- data/lib/dynflow/execution_plan/steps/plan_step.rb +6 -2
- data/lib/dynflow/execution_plan/steps/run_step.rb +4 -0
- data/lib/dynflow/persistence.rb +5 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +12 -2
- data/lib/dynflow/persistence_adapters/sequel_migrations/003_parent_action.rb +9 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +21 -7
- data/lib/dynflow/world.rb +26 -2
- data/test/action_test.rb +107 -0
- data/test/persistance_adapters_test.rb +2 -2
- data/test/test_helper.rb +1 -1
- data/web/views/flow_step.erb +3 -0
- metadata +137 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Bootstrap v3.3.2 (http://getbootstrap.com)
|
|
3
|
+
* Copyright 2011-2015 Twitter, Inc.
|
|
4
|
+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/*!
|
|
8
|
+
* Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=9d12231ad700ab8f1be0)
|
|
9
|
+
* Config saved to config.json and https://gist.github.com/9d12231ad700ab8f1be0
|
|
10
|
+
*/
|
|
11
|
+
if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(t){"use strict";var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var i=t(this),s=i.data("bs.alert");s||i.data("bs.alert",s=new o(this)),"string"==typeof e&&s[e].call(i)})}var i='[data-dismiss="alert"]',o=function(e){t(e).on("click",i,this.close)};o.VERSION="3.3.2",o.TRANSITION_DURATION=150,o.prototype.close=function(e){function i(){a.detach().trigger("closed.bs.alert").remove()}var s=t(this),n=s.attr("data-target");n||(n=s.attr("href"),n=n&&n.replace(/.*(?=#[^\s]*$)/,""));var a=t(n);e&&e.preventDefault(),a.length||(a=s.closest(".alert")),a.trigger(e=t.Event("close.bs.alert")),e.isDefaultPrevented()||(a.removeClass("in"),t.support.transition&&a.hasClass("fade")?a.one("bsTransitionEnd",i).emulateTransitionEnd(o.TRANSITION_DURATION):i())};var s=t.fn.alert;t.fn.alert=e,t.fn.alert.Constructor=o,t.fn.alert.noConflict=function(){return t.fn.alert=s,this},t(document).on("click.bs.alert.data-api",i,o.prototype.close)}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.button"),n="object"==typeof e&&e;s||o.data("bs.button",s=new i(this,n)),"toggle"==e?s.toggle():e&&s.setState(e)})}var i=function(e,o){this.$element=t(e),this.options=t.extend({},i.DEFAULTS,o),this.isLoading=!1};i.VERSION="3.3.2",i.DEFAULTS={loadingText:"loading..."},i.prototype.setState=function(e){var i="disabled",o=this.$element,s=o.is("input")?"val":"html",n=o.data();e+="Text",null==n.resetText&&o.data("resetText",o[s]()),setTimeout(t.proxy(function(){o[s](null==n[e]?this.options[e]:n[e]),"loadingText"==e?(this.isLoading=!0,o.addClass(i).attr(i,i)):this.isLoading&&(this.isLoading=!1,o.removeClass(i).removeAttr(i))},this),0)},i.prototype.toggle=function(){var t=!0,e=this.$element.closest('[data-toggle="buttons"]');if(e.length){var i=this.$element.find("input");"radio"==i.prop("type")&&(i.prop("checked")&&this.$element.hasClass("active")?t=!1:e.find(".active").removeClass("active")),t&&i.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));t&&this.$element.toggleClass("active")};var o=t.fn.button;t.fn.button=e,t.fn.button.Constructor=i,t.fn.button.noConflict=function(){return t.fn.button=o,this},t(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(i){var o=t(i.target);o.hasClass("btn")||(o=o.closest(".btn")),e.call(o,"toggle"),i.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(e){t(e.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(e.type))})}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.carousel"),n=t.extend({},i.DEFAULTS,o.data(),"object"==typeof e&&e),a="string"==typeof e?e:n.slide;s||o.data("bs.carousel",s=new i(this,n)),"number"==typeof e?s.to(e):a?s[a]():n.interval&&s.pause().cycle()})}var i=function(e,i){this.$element=t(e),this.$indicators=this.$element.find(".carousel-indicators"),this.options=i,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",t.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",t.proxy(this.pause,this)).on("mouseleave.bs.carousel",t.proxy(this.cycle,this))};i.VERSION="3.3.2",i.TRANSITION_DURATION=600,i.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},i.prototype.keydown=function(t){if(!/input|textarea/i.test(t.target.tagName)){switch(t.which){case 37:this.prev();break;case 39:this.next();break;default:return}t.preventDefault()}},i.prototype.cycle=function(e){return e||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(t.proxy(this.next,this),this.options.interval)),this},i.prototype.getItemIndex=function(t){return this.$items=t.parent().children(".item"),this.$items.index(t||this.$active)},i.prototype.getItemForDirection=function(t,e){var i=this.getItemIndex(e),o="prev"==t&&0===i||"next"==t&&i==this.$items.length-1;if(o&&!this.options.wrap)return e;var s="prev"==t?-1:1,n=(i+s)%this.$items.length;return this.$items.eq(n)},i.prototype.to=function(t){var e=this,i=this.getItemIndex(this.$active=this.$element.find(".item.active"));return t>this.$items.length-1||0>t?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(t>i?"next":"prev",this.$items.eq(t))},i.prototype.pause=function(e){return e||(this.paused=!0),this.$element.find(".next, .prev").length&&t.support.transition&&(this.$element.trigger(t.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},i.prototype.next=function(){return this.sliding?void 0:this.slide("next")},i.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},i.prototype.slide=function(e,o){var s=this.$element.find(".item.active"),n=o||this.getItemForDirection(e,s),a=this.interval,r="next"==e?"left":"right",l=this;if(n.hasClass("active"))return this.sliding=!1;var h=n[0],d=t.Event("slide.bs.carousel",{relatedTarget:h,direction:r});if(this.$element.trigger(d),!d.isDefaultPrevented()){if(this.sliding=!0,a&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var p=t(this.$indicators.children()[this.getItemIndex(n)]);p&&p.addClass("active")}var c=t.Event("slid.bs.carousel",{relatedTarget:h,direction:r});return t.support.transition&&this.$element.hasClass("slide")?(n.addClass(e),n[0].offsetWidth,s.addClass(r),n.addClass(r),s.one("bsTransitionEnd",function(){n.removeClass([e,r].join(" ")).addClass("active"),s.removeClass(["active",r].join(" ")),l.sliding=!1,setTimeout(function(){l.$element.trigger(c)},0)}).emulateTransitionEnd(i.TRANSITION_DURATION)):(s.removeClass("active"),n.addClass("active"),this.sliding=!1,this.$element.trigger(c)),a&&this.cycle(),this}};var o=t.fn.carousel;t.fn.carousel=e,t.fn.carousel.Constructor=i,t.fn.carousel.noConflict=function(){return t.fn.carousel=o,this};var s=function(i){var o,s=t(this),n=t(s.attr("data-target")||(o=s.attr("href"))&&o.replace(/.*(?=#[^\s]+$)/,""));if(n.hasClass("carousel")){var a=t.extend({},n.data(),s.data()),r=s.attr("data-slide-to");r&&(a.interval=!1),e.call(n,a),r&&n.data("bs.carousel").to(r),i.preventDefault()}};t(document).on("click.bs.carousel.data-api","[data-slide]",s).on("click.bs.carousel.data-api","[data-slide-to]",s),t(window).on("load",function(){t('[data-ride="carousel"]').each(function(){var i=t(this);e.call(i,i.data())})})}(jQuery),+function(t){"use strict";function e(e){e&&3===e.which||(t(s).remove(),t(n).each(function(){var o=t(this),s=i(o),n={relatedTarget:this};s.hasClass("open")&&(s.trigger(e=t.Event("hide.bs.dropdown",n)),e.isDefaultPrevented()||(o.attr("aria-expanded","false"),s.removeClass("open").trigger("hidden.bs.dropdown",n)))}))}function i(e){var i=e.attr("data-target");i||(i=e.attr("href"),i=i&&/#[A-Za-z]/.test(i)&&i.replace(/.*(?=#[^\s]*$)/,""));var o=i&&t(i);return o&&o.length?o:e.parent()}function o(e){return this.each(function(){var i=t(this),o=i.data("bs.dropdown");o||i.data("bs.dropdown",o=new a(this)),"string"==typeof e&&o[e].call(i)})}var s=".dropdown-backdrop",n='[data-toggle="dropdown"]',a=function(e){t(e).on("click.bs.dropdown",this.toggle)};a.VERSION="3.3.2",a.prototype.toggle=function(o){var s=t(this);if(!s.is(".disabled, :disabled")){var n=i(s),a=n.hasClass("open");if(e(),!a){"ontouchstart"in document.documentElement&&!n.closest(".navbar-nav").length&&t('<div class="dropdown-backdrop"/>').insertAfter(t(this)).on("click",e);var r={relatedTarget:this};if(n.trigger(o=t.Event("show.bs.dropdown",r)),o.isDefaultPrevented())return;s.trigger("focus").attr("aria-expanded","true"),n.toggleClass("open").trigger("shown.bs.dropdown",r)}return!1}},a.prototype.keydown=function(e){if(/(38|40|27|32)/.test(e.which)&&!/input|textarea/i.test(e.target.tagName)){var o=t(this);if(e.preventDefault(),e.stopPropagation(),!o.is(".disabled, :disabled")){var s=i(o),a=s.hasClass("open");if(!a&&27!=e.which||a&&27==e.which)return 27==e.which&&s.find(n).trigger("focus"),o.trigger("click");var r=" li:not(.divider):visible a",l=s.find('[role="menu"]'+r+', [role="listbox"]'+r);if(l.length){var h=l.index(e.target);38==e.which&&h>0&&h--,40==e.which&&h<l.length-1&&h++,~h||(h=0),l.eq(h).trigger("focus")}}}};var r=t.fn.dropdown;t.fn.dropdown=o,t.fn.dropdown.Constructor=a,t.fn.dropdown.noConflict=function(){return t.fn.dropdown=r,this},t(document).on("click.bs.dropdown.data-api",e).on("click.bs.dropdown.data-api",".dropdown form",function(t){t.stopPropagation()}).on("click.bs.dropdown.data-api",n,a.prototype.toggle).on("keydown.bs.dropdown.data-api",n,a.prototype.keydown).on("keydown.bs.dropdown.data-api",'[role="menu"]',a.prototype.keydown).on("keydown.bs.dropdown.data-api",'[role="listbox"]',a.prototype.keydown)}(jQuery),+function(t){"use strict";function e(e,o){return this.each(function(){var s=t(this),n=s.data("bs.modal"),a=t.extend({},i.DEFAULTS,s.data(),"object"==typeof e&&e);n||s.data("bs.modal",n=new i(this,a)),"string"==typeof e?n[e](o):a.show&&n.show(o)})}var i=function(e,i){this.options=i,this.$body=t(document.body),this.$element=t(e),this.$backdrop=this.isShown=null,this.scrollbarWidth=0,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,t.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};i.VERSION="3.3.2",i.TRANSITION_DURATION=300,i.BACKDROP_TRANSITION_DURATION=150,i.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},i.prototype.toggle=function(t){return this.isShown?this.hide():this.show(t)},i.prototype.show=function(e){var o=this,s=t.Event("show.bs.modal",{relatedTarget:e});this.$element.trigger(s),this.isShown||s.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',t.proxy(this.hide,this)),this.backdrop(function(){var s=t.support.transition&&o.$element.hasClass("fade");o.$element.parent().length||o.$element.appendTo(o.$body),o.$element.show().scrollTop(0),o.options.backdrop&&o.adjustBackdrop(),o.adjustDialog(),s&&o.$element[0].offsetWidth,o.$element.addClass("in").attr("aria-hidden",!1),o.enforceFocus();var n=t.Event("shown.bs.modal",{relatedTarget:e});s?o.$element.find(".modal-dialog").one("bsTransitionEnd",function(){o.$element.trigger("focus").trigger(n)}).emulateTransitionEnd(i.TRANSITION_DURATION):o.$element.trigger("focus").trigger(n)}))},i.prototype.hide=function(e){e&&e.preventDefault(),e=t.Event("hide.bs.modal"),this.$element.trigger(e),this.isShown&&!e.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),t(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.bs.modal"),t.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",t.proxy(this.hideModal,this)).emulateTransitionEnd(i.TRANSITION_DURATION):this.hideModal())},i.prototype.enforceFocus=function(){t(document).off("focusin.bs.modal").on("focusin.bs.modal",t.proxy(function(t){this.$element[0]===t.target||this.$element.has(t.target).length||this.$element.trigger("focus")},this))},i.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",t.proxy(function(t){27==t.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},i.prototype.resize=function(){this.isShown?t(window).on("resize.bs.modal",t.proxy(this.handleUpdate,this)):t(window).off("resize.bs.modal")},i.prototype.hideModal=function(){var t=this;this.$element.hide(),this.backdrop(function(){t.$body.removeClass("modal-open"),t.resetAdjustments(),t.resetScrollbar(),t.$element.trigger("hidden.bs.modal")})},i.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},i.prototype.backdrop=function(e){var o=this,s=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var n=t.support.transition&&s;if(this.$backdrop=t('<div class="modal-backdrop '+s+'" />').prependTo(this.$element).on("click.dismiss.bs.modal",t.proxy(function(t){t.target===t.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),n&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!e)return;n?this.$backdrop.one("bsTransitionEnd",e).emulateTransitionEnd(i.BACKDROP_TRANSITION_DURATION):e()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var a=function(){o.removeBackdrop(),e&&e()};t.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",a).emulateTransitionEnd(i.BACKDROP_TRANSITION_DURATION):a()}else e&&e()},i.prototype.handleUpdate=function(){this.options.backdrop&&this.adjustBackdrop(),this.adjustDialog()},i.prototype.adjustBackdrop=function(){this.$backdrop.css("height",0).css("height",this.$element[0].scrollHeight)},i.prototype.adjustDialog=function(){var t=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},i.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},i.prototype.checkScrollbar=function(){this.bodyIsOverflowing=document.body.scrollHeight>document.documentElement.clientHeight,this.scrollbarWidth=this.measureScrollbar()},i.prototype.setScrollbar=function(){var t=parseInt(this.$body.css("padding-right")||0,10);this.bodyIsOverflowing&&this.$body.css("padding-right",t+this.scrollbarWidth)},i.prototype.resetScrollbar=function(){this.$body.css("padding-right","")},i.prototype.measureScrollbar=function(){var t=document.createElement("div");t.className="modal-scrollbar-measure",this.$body.append(t);var e=t.offsetWidth-t.clientWidth;return this.$body[0].removeChild(t),e};var o=t.fn.modal;t.fn.modal=e,t.fn.modal.Constructor=i,t.fn.modal.noConflict=function(){return t.fn.modal=o,this},t(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(i){var o=t(this),s=o.attr("href"),n=t(o.attr("data-target")||s&&s.replace(/.*(?=#[^\s]+$)/,"")),a=n.data("bs.modal")?"toggle":t.extend({remote:!/#/.test(s)&&s},n.data(),o.data());o.is("a")&&i.preventDefault(),n.one("show.bs.modal",function(t){t.isDefaultPrevented()||n.one("hidden.bs.modal",function(){o.is(":visible")&&o.trigger("focus")})}),e.call(n,a,this)})}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.tooltip"),n="object"==typeof e&&e;(s||"destroy"!=e)&&(s||o.data("bs.tooltip",s=new i(this,n)),"string"==typeof e&&s[e]())})}var i=function(t,e){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",t,e)};i.VERSION="3.3.2",i.TRANSITION_DURATION=150,i.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},i.prototype.init=function(e,i,o){this.enabled=!0,this.type=e,this.$element=t(i),this.options=this.getOptions(o),this.$viewport=this.options.viewport&&t(this.options.viewport.selector||this.options.viewport);for(var s=this.options.trigger.split(" "),n=s.length;n--;){var a=s[n];if("click"==a)this.$element.on("click."+this.type,this.options.selector,t.proxy(this.toggle,this));else if("manual"!=a){var r="hover"==a?"mouseenter":"focusin",l="hover"==a?"mouseleave":"focusout";this.$element.on(r+"."+this.type,this.options.selector,t.proxy(this.enter,this)),this.$element.on(l+"."+this.type,this.options.selector,t.proxy(this.leave,this))}}this.options.selector?this._options=t.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},i.prototype.getDefaults=function(){return i.DEFAULTS},i.prototype.getOptions=function(e){return e=t.extend({},this.getDefaults(),this.$element.data(),e),e.delay&&"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),e},i.prototype.getDelegateOptions=function(){var e={},i=this.getDefaults();return this._options&&t.each(this._options,function(t,o){i[t]!=o&&(e[t]=o)}),e},i.prototype.enter=function(e){var i=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return i&&i.$tip&&i.$tip.is(":visible")?void(i.hoverState="in"):(i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i)),clearTimeout(i.timeout),i.hoverState="in",i.options.delay&&i.options.delay.show?void(i.timeout=setTimeout(function(){"in"==i.hoverState&&i.show()},i.options.delay.show)):i.show())},i.prototype.leave=function(e){var i=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i)),clearTimeout(i.timeout),i.hoverState="out",i.options.delay&&i.options.delay.hide?void(i.timeout=setTimeout(function(){"out"==i.hoverState&&i.hide()},i.options.delay.hide)):i.hide()},i.prototype.show=function(){var e=t.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(e);var o=t.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(e.isDefaultPrevented()||!o)return;var s=this,n=this.tip(),a=this.getUID(this.type);this.setContent(),n.attr("id",a),this.$element.attr("aria-describedby",a),this.options.animation&&n.addClass("fade");var r="function"==typeof this.options.placement?this.options.placement.call(this,n[0],this.$element[0]):this.options.placement,l=/\s?auto?\s?/i,h=l.test(r);h&&(r=r.replace(l,"")||"top"),n.detach().css({top:0,left:0,display:"block"}).addClass(r).data("bs."+this.type,this),this.options.container?n.appendTo(this.options.container):n.insertAfter(this.$element);var d=this.getPosition(),p=n[0].offsetWidth,c=n[0].offsetHeight;if(h){var f=r,u=this.options.container?t(this.options.container):this.$element.parent(),g=this.getPosition(u);r="bottom"==r&&d.bottom+c>g.bottom?"top":"top"==r&&d.top-c<g.top?"bottom":"right"==r&&d.right+p>g.width?"left":"left"==r&&d.left-p<g.left?"right":r,n.removeClass(f).addClass(r)}var v=this.getCalculatedOffset(r,d,p,c);this.applyPlacement(v,r);var m=function(){var t=s.hoverState;s.$element.trigger("shown.bs."+s.type),s.hoverState=null,"out"==t&&s.leave(s)};t.support.transition&&this.$tip.hasClass("fade")?n.one("bsTransitionEnd",m).emulateTransitionEnd(i.TRANSITION_DURATION):m()}},i.prototype.applyPlacement=function(e,i){var o=this.tip(),s=o[0].offsetWidth,n=o[0].offsetHeight,a=parseInt(o.css("margin-top"),10),r=parseInt(o.css("margin-left"),10);isNaN(a)&&(a=0),isNaN(r)&&(r=0),e.top=e.top+a,e.left=e.left+r,t.offset.setOffset(o[0],t.extend({using:function(t){o.css({top:Math.round(t.top),left:Math.round(t.left)})}},e),0),o.addClass("in");var l=o[0].offsetWidth,h=o[0].offsetHeight;"top"==i&&h!=n&&(e.top=e.top+n-h);var d=this.getViewportAdjustedDelta(i,e,l,h);d.left?e.left+=d.left:e.top+=d.top;var p=/top|bottom/.test(i),c=p?2*d.left-s+l:2*d.top-n+h,f=p?"offsetWidth":"offsetHeight";o.offset(e),this.replaceArrow(c,o[0][f],p)},i.prototype.replaceArrow=function(t,e,i){this.arrow().css(i?"left":"top",50*(1-t/e)+"%").css(i?"top":"left","")},i.prototype.setContent=function(){var t=this.tip(),e=this.getTitle();t.find(".tooltip-inner")[this.options.html?"html":"text"](e),t.removeClass("fade in top bottom left right")},i.prototype.hide=function(e){function o(){"in"!=s.hoverState&&n.detach(),s.$element.removeAttr("aria-describedby").trigger("hidden.bs."+s.type),e&&e()}var s=this,n=this.tip(),a=t.Event("hide.bs."+this.type);return this.$element.trigger(a),a.isDefaultPrevented()?void 0:(n.removeClass("in"),t.support.transition&&this.$tip.hasClass("fade")?n.one("bsTransitionEnd",o).emulateTransitionEnd(i.TRANSITION_DURATION):o(),this.hoverState=null,this)},i.prototype.fixTitle=function(){var t=this.$element;(t.attr("title")||"string"!=typeof t.attr("data-original-title"))&&t.attr("data-original-title",t.attr("title")||"").attr("title","")},i.prototype.hasContent=function(){return this.getTitle()},i.prototype.getPosition=function(e){e=e||this.$element;var i=e[0],o="BODY"==i.tagName,s=i.getBoundingClientRect();null==s.width&&(s=t.extend({},s,{width:s.right-s.left,height:s.bottom-s.top}));var n=o?{top:0,left:0}:e.offset(),a={scroll:o?document.documentElement.scrollTop||document.body.scrollTop:e.scrollTop()},r=o?{width:t(window).width(),height:t(window).height()}:null;return t.extend({},s,a,r,n)},i.prototype.getCalculatedOffset=function(t,e,i,o){return"bottom"==t?{top:e.top+e.height,left:e.left+e.width/2-i/2}:"top"==t?{top:e.top-o,left:e.left+e.width/2-i/2}:"left"==t?{top:e.top+e.height/2-o/2,left:e.left-i}:{top:e.top+e.height/2-o/2,left:e.left+e.width}},i.prototype.getViewportAdjustedDelta=function(t,e,i,o){var s={top:0,left:0};if(!this.$viewport)return s;var n=this.options.viewport&&this.options.viewport.padding||0,a=this.getPosition(this.$viewport);if(/right|left/.test(t)){var r=e.top-n-a.scroll,l=e.top+n-a.scroll+o;r<a.top?s.top=a.top-r:l>a.top+a.height&&(s.top=a.top+a.height-l)}else{var h=e.left-n,d=e.left+n+i;h<a.left?s.left=a.left-h:d>a.width&&(s.left=a.left+a.width-d)}return s},i.prototype.getTitle=function(){var t,e=this.$element,i=this.options;return t=e.attr("data-original-title")||("function"==typeof i.title?i.title.call(e[0]):i.title)},i.prototype.getUID=function(t){do t+=~~(1e6*Math.random());while(document.getElementById(t));return t},i.prototype.tip=function(){return this.$tip=this.$tip||t(this.options.template)},i.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},i.prototype.enable=function(){this.enabled=!0},i.prototype.disable=function(){this.enabled=!1},i.prototype.toggleEnabled=function(){this.enabled=!this.enabled},i.prototype.toggle=function(e){var i=this;e&&(i=t(e.currentTarget).data("bs."+this.type),i||(i=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,i))),i.tip().hasClass("in")?i.leave(i):i.enter(i)},i.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type)})};var o=t.fn.tooltip;t.fn.tooltip=e,t.fn.tooltip.Constructor=i,t.fn.tooltip.noConflict=function(){return t.fn.tooltip=o,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.popover"),n="object"==typeof e&&e;(s||"destroy"!=e)&&(s||o.data("bs.popover",s=new i(this,n)),"string"==typeof e&&s[e]())})}var i=function(t,e){this.init("popover",t,e)};if(!t.fn.tooltip)throw new Error("Popover requires tooltip.js");i.VERSION="3.3.2",i.DEFAULTS=t.extend({},t.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),i.prototype=t.extend({},t.fn.tooltip.Constructor.prototype),i.prototype.constructor=i,i.prototype.getDefaults=function(){return i.DEFAULTS},i.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();t.find(".popover-title")[this.options.html?"html":"text"](e),t.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof i?"html":"append":"text"](i),t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},i.prototype.hasContent=function(){return this.getTitle()||this.getContent()},i.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},i.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},i.prototype.tip=function(){return this.$tip||(this.$tip=t(this.options.template)),this.$tip};var o=t.fn.popover;t.fn.popover=e,t.fn.popover.Constructor=i,t.fn.popover.noConflict=function(){return t.fn.popover=o,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.tab");s||o.data("bs.tab",s=new i(this)),"string"==typeof e&&s[e]()})}var i=function(e){this.element=t(e)};i.VERSION="3.3.2",i.TRANSITION_DURATION=150,i.prototype.show=function(){var e=this.element,i=e.closest("ul:not(.dropdown-menu)"),o=e.data("target");if(o||(o=e.attr("href"),o=o&&o.replace(/.*(?=#[^\s]*$)/,"")),!e.parent("li").hasClass("active")){var s=i.find(".active:last a"),n=t.Event("hide.bs.tab",{relatedTarget:e[0]}),a=t.Event("show.bs.tab",{relatedTarget:s[0]});if(s.trigger(n),e.trigger(a),!a.isDefaultPrevented()&&!n.isDefaultPrevented()){var r=t(o);this.activate(e.closest("li"),i),this.activate(r,r.parent(),function(){s.trigger({type:"hidden.bs.tab",relatedTarget:e[0]}),e.trigger({type:"shown.bs.tab",relatedTarget:s[0]})})}}},i.prototype.activate=function(e,o,s){function n(){a.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),e.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),r?(e[0].offsetWidth,e.addClass("in")):e.removeClass("fade"),e.parent(".dropdown-menu")&&e.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),s&&s()}var a=o.find("> .active"),r=s&&t.support.transition&&(a.length&&a.hasClass("fade")||!!o.find("> .fade").length);a.length&&r?a.one("bsTransitionEnd",n).emulateTransitionEnd(i.TRANSITION_DURATION):n(),a.removeClass("in")};var o=t.fn.tab;t.fn.tab=e,t.fn.tab.Constructor=i,t.fn.tab.noConflict=function(){return t.fn.tab=o,this};var s=function(i){i.preventDefault(),e.call(t(this),"show")};t(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',s).on("click.bs.tab.data-api",'[data-toggle="pill"]',s)}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.affix"),n="object"==typeof e&&e;s||o.data("bs.affix",s=new i(this,n)),"string"==typeof e&&s[e]()})}var i=function(e,o){this.options=t.extend({},i.DEFAULTS,o),this.$target=t(this.options.target).on("scroll.bs.affix.data-api",t.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",t.proxy(this.checkPositionWithEventLoop,this)),this.$element=t(e),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};i.VERSION="3.3.2",i.RESET="affix affix-top affix-bottom",i.DEFAULTS={offset:0,target:window},i.prototype.getState=function(t,e,i,o){var s=this.$target.scrollTop(),n=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return i>s?"top":!1;if("bottom"==this.affixed)return null!=i?s+this.unpin<=n.top?!1:"bottom":t-o>=s+a?!1:"bottom";var r=null==this.affixed,l=r?s:n.top,h=r?a:e;return null!=i&&i>=s?"top":null!=o&&l+h>=t-o?"bottom":!1},i.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(i.RESET).addClass("affix");var t=this.$target.scrollTop(),e=this.$element.offset();return this.pinnedOffset=e.top-t},i.prototype.checkPositionWithEventLoop=function(){setTimeout(t.proxy(this.checkPosition,this),1)},i.prototype.checkPosition=function(){if(this.$element.is(":visible")){var e=this.$element.height(),o=this.options.offset,s=o.top,n=o.bottom,a=t("body").height();"object"!=typeof o&&(n=s=o),"function"==typeof s&&(s=o.top(this.$element)),"function"==typeof n&&(n=o.bottom(this.$element));var r=this.getState(a,e,s,n);if(this.affixed!=r){null!=this.unpin&&this.$element.css("top","");var l="affix"+(r?"-"+r:""),h=t.Event(l+".bs.affix");if(this.$element.trigger(h),h.isDefaultPrevented())return;this.affixed=r,this.unpin="bottom"==r?this.getPinnedOffset():null,this.$element.removeClass(i.RESET).addClass(l).trigger(l.replace("affix","affixed")+".bs.affix")}"bottom"==r&&this.$element.offset({top:a-e-n})}};var o=t.fn.affix;t.fn.affix=e,t.fn.affix.Constructor=i,t.fn.affix.noConflict=function(){return t.fn.affix=o,this},t(window).on("load",function(){t('[data-spy="affix"]').each(function(){var i=t(this),o=i.data();o.offset=o.offset||{},null!=o.offsetBottom&&(o.offset.bottom=o.offsetBottom),null!=o.offsetTop&&(o.offset.top=o.offsetTop),e.call(i,o)})})}(jQuery),+function(t){"use strict";function e(e){var i,o=e.attr("data-target")||(i=e.attr("href"))&&i.replace(/.*(?=#[^\s]+$)/,"");return t(o)}function i(e){return this.each(function(){var i=t(this),s=i.data("bs.collapse"),n=t.extend({},o.DEFAULTS,i.data(),"object"==typeof e&&e);!s&&n.toggle&&"show"==e&&(n.toggle=!1),s||i.data("bs.collapse",s=new o(this,n)),"string"==typeof e&&s[e]()})}var o=function(e,i){this.$element=t(e),this.options=t.extend({},o.DEFAULTS,i),this.$trigger=t(this.options.trigger).filter('[href="#'+e.id+'"], [data-target="#'+e.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};o.VERSION="3.3.2",o.TRANSITION_DURATION=350,o.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},o.prototype.dimension=function(){var t=this.$element.hasClass("width");return t?"width":"height"},o.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var e,s=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(s&&s.length&&(e=s.data("bs.collapse"),e&&e.transitioning))){var n=t.Event("show.bs.collapse");if(this.$element.trigger(n),!n.isDefaultPrevented()){s&&s.length&&(i.call(s,"hide"),e||s.data("bs.collapse",null));var a=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[a](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var r=function(){this.$element.removeClass("collapsing").addClass("collapse in")[a](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!t.support.transition)return r.call(this);var l=t.camelCase(["scroll",a].join("-"));this.$element.one("bsTransitionEnd",t.proxy(r,this)).emulateTransitionEnd(o.TRANSITION_DURATION)[a](this.$element[0][l])}}}},o.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var e=t.Event("hide.bs.collapse");if(this.$element.trigger(e),!e.isDefaultPrevented()){var i=this.dimension();this.$element[i](this.$element[i]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var s=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return t.support.transition?void this.$element[i](0).one("bsTransitionEnd",t.proxy(s,this)).emulateTransitionEnd(o.TRANSITION_DURATION):s.call(this)}}},o.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},o.prototype.getParent=function(){return t(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(t.proxy(function(i,o){var s=t(o);this.addAriaAndCollapsedClass(e(s),s)},this)).end()},o.prototype.addAriaAndCollapsedClass=function(t,e){var i=t.hasClass("in");t.attr("aria-expanded",i),e.toggleClass("collapsed",!i).attr("aria-expanded",i)};var s=t.fn.collapse;t.fn.collapse=i,t.fn.collapse.Constructor=o,t.fn.collapse.noConflict=function(){return t.fn.collapse=s,this},t(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(o){var s=t(this);s.attr("data-target")||o.preventDefault();
|
|
12
|
+
var n=e(s),a=n.data("bs.collapse"),r=a?"toggle":t.extend({},s.data(),{trigger:this});i.call(n,r)})}(jQuery),+function(t){"use strict";function e(i,o){var s=t.proxy(this.process,this);this.$body=t("body"),this.$scrollElement=t(t(i).is("body")?window:i),this.options=t.extend({},e.DEFAULTS,o),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s),this.refresh(),this.process()}function i(i){return this.each(function(){var o=t(this),s=o.data("bs.scrollspy"),n="object"==typeof i&&i;s||o.data("bs.scrollspy",s=new e(this,n)),"string"==typeof i&&s[i]()})}e.VERSION="3.3.2",e.DEFAULTS={offset:10},e.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},e.prototype.refresh=function(){var e="offset",i=0;t.isWindow(this.$scrollElement[0])||(e="position",i=this.$scrollElement.scrollTop()),this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight();var o=this;this.$body.find(this.selector).map(function(){var o=t(this),s=o.data("target")||o.attr("href"),n=/^#./.test(s)&&t(s);return n&&n.length&&n.is(":visible")&&[[n[e]().top+i,s]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){o.offsets.push(this[0]),o.targets.push(this[1])})},e.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),s=this.offsets,n=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),e>=o)return a!=(t=n[n.length-1])&&this.activate(t);if(a&&e<s[0])return this.activeTarget=null,this.clear();for(t=s.length;t--;)a!=n[t]&&e>=s[t]&&(!s[t+1]||e<=s[t+1])&&this.activate(n[t])},e.prototype.activate=function(e){this.activeTarget=e,this.clear();var i=this.selector+'[data-target="'+e+'"],'+this.selector+'[href="'+e+'"]',o=t(i).parents("li").addClass("active");o.parent(".dropdown-menu").length&&(o=o.closest("li.dropdown").addClass("active")),o.trigger("activate.bs.scrollspy")},e.prototype.clear=function(){t(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var o=t.fn.scrollspy;t.fn.scrollspy=i,t.fn.scrollspy.Constructor=e,t.fn.scrollspy.noConflict=function(){return t.fn.scrollspy=o,this},t(window).on("load.bs.scrollspy.data-api",function(){t('[data-spy="scroll"]').each(function(){var e=t(this);i.call(e,e.data())})})}(jQuery),+function(t){"use strict";function e(){var t=document.createElement("bootstrap"),e={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var i in e)if(void 0!==t.style[i])return{end:e[i]};return!1}t.fn.emulateTransitionEnd=function(e){var i=!1,o=this;t(this).one("bsTransitionEnd",function(){i=!0});var s=function(){i||t(o).trigger(t.support.transition.end)};return setTimeout(s,e),this},t(function(){t.support.transition=e(),t.support.transition&&(t.event.special.bsTransitionEnd={bindType:t.support.transition.end,delegateType:t.support.transition.end,handle:function(e){return t(e.target).is(this)?e.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery);
|
|
@@ -0,0 +1,60 @@
|
|
|
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 */
|
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: page
|
|
3
|
+
title: Documentation
|
|
4
|
+
countheads: true
|
|
5
|
+
toc: true
|
|
6
|
+
comments: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{% danger_block %}
|
|
10
|
+
|
|
11
|
+
Work in progress! It contains a lot of tpyos, please let us know. There are comments at the bottom
|
|
12
|
+
or you can submit a PR against [pages branch](https://github.com/dynflow/dynflow/tree/pages).
|
|
13
|
+
|
|
14
|
+
Please help with the documentation if you know Dynflow.
|
|
15
|
+
|
|
16
|
+
Thanks!
|
|
17
|
+
|
|
18
|
+
{% enddanger_block %}
|
|
19
|
+
|
|
20
|
+
## High level overview TODO
|
|
21
|
+
|
|
22
|
+
*TODO to be refined*
|
|
23
|
+
|
|
24
|
+
Dynflow (**DYN**amic work**FLOW**) is a workflow engine
|
|
25
|
+
written in Ruby that allows to:
|
|
26
|
+
|
|
27
|
+
- Keep track of the progress of running processes
|
|
28
|
+
- Run the code asynchronously
|
|
29
|
+
- Resume the process when something goes wrong, skip some steps when needed
|
|
30
|
+
- Detect independent parts and run them concurrently
|
|
31
|
+
- Compose simple actions into more complex scenarios
|
|
32
|
+
- Extend the workflows from third-party libraries
|
|
33
|
+
- Keep consistency between local transactional database and
|
|
34
|
+
external services
|
|
35
|
+
- Suspend the long-running steps, not blocking the thread pool
|
|
36
|
+
- Cancel steps when possible
|
|
37
|
+
- Extend the actions behavior with middlewares
|
|
38
|
+
- Pick different adapters to provide: storage backend, transactions, or executor implementation
|
|
39
|
+
|
|
40
|
+
Dynflow has been developed to be able to support orchestration of services in the
|
|
41
|
+
[Katello](http://katello.org) and [Foreman](http://theforeman.org/) projects.
|
|
42
|
+
|
|
43
|
+
*TODO*
|
|
44
|
+
|
|
45
|
+
- what problems does Dynflow solve?
|
|
46
|
+
- maybe a little history
|
|
47
|
+
|
|
48
|
+
## Glossary
|
|
49
|
+
|
|
50
|
+
- **Action** - building block of execution plans, a Ruby class inherited
|
|
51
|
+
from `Dynflow::Action`, defines code to be run in each phase.
|
|
52
|
+
- **Phase** - Each action has three phases: `plan`, `run`, `finalize`.
|
|
53
|
+
- **Input** - A `Hash` of data coming to the action.
|
|
54
|
+
- **Output** - A `Hash` of data that the action produces. It's
|
|
55
|
+
persisted and can be used as input of other actions.
|
|
56
|
+
- **Execution plan** - definition of the workflow: product of the plan phase,
|
|
57
|
+
- **Triggering an action** - entering the plan phase, starting with the plan
|
|
58
|
+
method of the action. The execution follows immediately.
|
|
59
|
+
- **Flow** - definition of the `run`/`finalize` phase, holding the information
|
|
60
|
+
about steps that can run concurrently/in sequence. Part of execution plan.
|
|
61
|
+
- **Executor** - service that executes the run and finalize flows based on
|
|
62
|
+
the execution plan. It can run in the same process as the plan phase or in
|
|
63
|
+
different process (using the remote executor).
|
|
64
|
+
- **World** - the universe where the Dynflow runs the code: it holds all
|
|
65
|
+
needed configuration. Usually there's only one world per Dynflow process,
|
|
66
|
+
besides configuration it also holds `Persistence`, `Logger`, `Executor` and
|
|
67
|
+
all the other objects necessary for action executing. This concept
|
|
68
|
+
allows us to avoid globally shared state. Also, the worlds can
|
|
69
|
+
talk to each other, which is helpful for production and
|
|
70
|
+
high-availability setups,
|
|
71
|
+
having multiple worlds on different hosts handle the execution of the execution plans.
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
See the
|
|
76
|
+
[examples directory](https://github.com/Dynflow/dynflow/tree/master/examples)
|
|
77
|
+
for the code in action. Running those files (except the
|
|
78
|
+
`example_helper.rb` file) leads to the Dynflow runtime being initialized
|
|
79
|
+
(including the web console where one can explore the features and
|
|
80
|
+
experiment).
|
|
81
|
+
|
|
82
|
+
*TODO*
|
|
83
|
+
|
|
84
|
+
- for async operations
|
|
85
|
+
- for orchestrating system/ssh calls
|
|
86
|
+
- for keeping consistency between local database and external systems
|
|
87
|
+
- sub-tasks
|
|
88
|
+
|
|
89
|
+
## How to use
|
|
90
|
+
|
|
91
|
+
### World creation TODO
|
|
92
|
+
|
|
93
|
+
- *include executor description*
|
|
94
|
+
|
|
95
|
+
### Development vs production TODO
|
|
96
|
+
|
|
97
|
+
- *In development execution runs in the same process, in production there is an
|
|
98
|
+
executor process.*
|
|
99
|
+
|
|
100
|
+
### Action anatomy
|
|
101
|
+
|
|
102
|
+
{% digraph %}
|
|
103
|
+
rankdir=LR
|
|
104
|
+
Trigger -> Plan -> Run -> Finalize
|
|
105
|
+
{% enddigraph %}
|
|
106
|
+
|
|
107
|
+
When action is triggered, Dynflow executes plan method on this action, which
|
|
108
|
+
is responsible for building the execution plan. It builds the execution plan by calling
|
|
109
|
+
`plan_action` and `plan_self` methods, effectively listing actions that should be run as
|
|
110
|
+
a part of this execution plan. In other words, it compiles a list of actions on which
|
|
111
|
+
method `run` will be called. Also it's responsible for giving these actions
|
|
112
|
+
an order. A simple example of such plan action might look like this
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# this would plan deletion of files passed as an input array
|
|
116
|
+
def plan(files)
|
|
117
|
+
files.each do |filename|
|
|
118
|
+
plan_action MyActions::File::Destroy, filename
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Note that it does not have to be only other actions that are planned to run.
|
|
124
|
+
In fact it's very common that the action plan itself, which means it will
|
|
125
|
+
put it's own `run` method call in the execution plan. In order to do that
|
|
126
|
+
you can use `plan_self`. This could be used in MyActions::File::Destroy
|
|
127
|
+
used in previous example
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
class MyActions::File::Destroy < Dynflow::Action
|
|
131
|
+
def plan(filename)
|
|
132
|
+
plan_self path: filename
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def run
|
|
136
|
+
File.rm(input.fetch(:path))
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
In example above, it seems that `plan_self` is just shortcut to
|
|
142
|
+
`plan_action MyActions::File::Destroy, filename` but it's not entirely true.
|
|
143
|
+
Note that `plan_action` always trigger `plan` of a given action while `plan_self`
|
|
144
|
+
plans only the `run` of Action, so by using `plan_action` we'd end up in
|
|
145
|
+
endless loop.
|
|
146
|
+
|
|
147
|
+
Also note, that run method does not take any input. In fact, it can use
|
|
148
|
+
`input` method that refers to arguments, that were used in `plan_self`.
|
|
149
|
+
|
|
150
|
+
Similar to the input mentioned above, the run produces output.
|
|
151
|
+
After that some finalizing steps can be taken. Actions can use outputs of other actions
|
|
152
|
+
as parts of their inputs establishing dependency. Action's state is serialized between each phase
|
|
153
|
+
and survives machine/executor restarts.
|
|
154
|
+
|
|
155
|
+
As lightly touched in the previous paragraph there are 3 phases: `plan`, `run`, `finalize`.
|
|
156
|
+
Plan phase starts by triggering an action.
|
|
157
|
+
|
|
158
|
+
#### Input and Output
|
|
159
|
+
|
|
160
|
+
Both input and output are `Hash`es accessible by `Action#input` and `Action#output` methods. They
|
|
161
|
+
need to be serializable to JSON so it should contain only combination of primitive Ruby types
|
|
162
|
+
like: `Hash`, `Array`, `String`, `Integer`, etc.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
{% warning_block %}
|
|
166
|
+
|
|
167
|
+
One should avoid using directly
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
self.output = { key: data }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It might delete other data stored in the output (potentially by middleware and other
|
|
174
|
+
parts of the action). Therefore it's preferred to use
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
output.update(key: data, another_key: another_data)
|
|
178
|
+
# or for one key
|
|
179
|
+
output[:key] = data
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
{% endwarning_block %}
|
|
183
|
+
|
|
184
|
+
{% info_block %}
|
|
185
|
+
|
|
186
|
+
You may sometime find these input/output format definitions:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
class AnAction < Dynflow::Action
|
|
190
|
+
input_format do
|
|
191
|
+
param :id, Integer
|
|
192
|
+
param :name, String
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
output_format do
|
|
196
|
+
param :uuid, String
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This might me quite handy especially in combination with
|
|
202
|
+
[subscriptions](#subscriptions) functionality.
|
|
203
|
+
|
|
204
|
+
The format follows [apipie-params](https://github.com/iNecas/apipie-params) for more details.
|
|
205
|
+
Validations of input/output could be performed against this description but it's not turned on
|
|
206
|
+
by default. (It needs to be revisited and updated to be fully functional.)
|
|
207
|
+
|
|
208
|
+
{% endinfo_block %}
|
|
209
|
+
|
|
210
|
+
#### Triggering
|
|
211
|
+
|
|
212
|
+
Triggering the action means starting the plan phase, followed by immediate execution.
|
|
213
|
+
Any action is triggered by calling:
|
|
214
|
+
|
|
215
|
+
``` ruby
|
|
216
|
+
world_instance.trigger(AnAction, *args)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
{% info_block %}
|
|
220
|
+
|
|
221
|
+
In Foreman and Katello actions are usually triggered by `ForemanTask.sync_task` and
|
|
222
|
+
`ForemanTasks.async_task` so following part is not that important if you are using
|
|
223
|
+
`ForemanTasks`.
|
|
224
|
+
|
|
225
|
+
{% endinfo_block %}
|
|
226
|
+
|
|
227
|
+
`World#trigger` method returns object of `TriggerResult` type. Which is
|
|
228
|
+
[Algebrick](http://blog.pitr.ch/projects/algebrick/) variant type where definition follows:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
TriggerResult = Algebrick.type do
|
|
232
|
+
# Returned by #trigger when planning fails.
|
|
233
|
+
PlaningFailed = type { fields! execution_plan_id: String, error: Exception }
|
|
234
|
+
# Returned by #trigger when planning is successful but execution fails to start.
|
|
235
|
+
ExecutionFailed = type { fields! execution_plan_id: String, error: Exception }
|
|
236
|
+
# Returned by #trigger when planning is successful, #future will resolve after
|
|
237
|
+
# ExecutionPlan is executed.
|
|
238
|
+
Triggered = type { fields! execution_plan_id: String, future: Future }
|
|
239
|
+
|
|
240
|
+
variants PlaningFailed, ExecutionFailed, Triggered
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
If you do not know `Algebrick` you can think about these as `Struct`s with types.
|
|
245
|
+
You can see how it's used to distinguish all the possible results
|
|
246
|
+
[in ForemanTasks module](https://github.com/theforeman/foreman-tasks/blob/master/lib/foreman_tasks.rb#L20-L32).
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
def self.trigger_task(async, action, *args, &block)
|
|
250
|
+
Match! async, true, false
|
|
251
|
+
|
|
252
|
+
match trigger(action, *args, &block),
|
|
253
|
+
# Raise if there is any error caused either by failed planning or
|
|
254
|
+
# by faild start of execution.
|
|
255
|
+
(on ::Dynflow::World::PlaningFailed.(error: ~any) |
|
|
256
|
+
::Dynflow::World::ExecutionFailed.(error: ~any) do |error|
|
|
257
|
+
raise error
|
|
258
|
+
end),
|
|
259
|
+
# Succesfully triggered.
|
|
260
|
+
(on ::Dynflow::World::Triggered.(
|
|
261
|
+
execution_plan_id: ~any, future: ~any) do |id, finished|
|
|
262
|
+
# block on the finished Future if this is called synchronously
|
|
263
|
+
finished.wait if async == false
|
|
264
|
+
return ForemanTasks::Task::DynflowTask.find_by_external_id!(id)
|
|
265
|
+
end)
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### Plan phase
|
|
270
|
+
|
|
271
|
+
Planning always uses the thread triggering the action. Plan phase
|
|
272
|
+
configures action's input for run phase. It starts by executing
|
|
273
|
+
`plan` method of the action instance passing in arguments from
|
|
274
|
+
`World#trigger method`
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
world_instance.trigger(AnAction, *args)
|
|
278
|
+
# executes following
|
|
279
|
+
an_action.plan(*args) # an_action is AnAction
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`plan` method is inherited from Dynflow::Action and by default it plans itself if
|
|
283
|
+
`run` method is present using first argument as input.
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
class AnAction < Dynflow::Action
|
|
287
|
+
def run
|
|
288
|
+
output.update self.input
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
world_instance.trigger AnAction, data: 'nothing'
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The above will just plan itself copying input to output in run phase.
|
|
296
|
+
|
|
297
|
+
In most cases the `plan` method is overridden to plan self with transformed arguments and/or
|
|
298
|
+
to plan other actions. In the Rails application, the arguments of the
|
|
299
|
+
plan phase are often the ActiveRecord objects, that are then
|
|
300
|
+
used to produce the inputs for the actions.
|
|
301
|
+
|
|
302
|
+
Let's look at the argument transformation first:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
class AnAction < Dynflow::Action
|
|
306
|
+
def plan(any_array)
|
|
307
|
+
# pick just numbers
|
|
308
|
+
plan_self numbers: any_array.select { |v| v.is_a? Number }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def run
|
|
312
|
+
# compute sum - simulating a time consuming operation
|
|
313
|
+
output.update sum: input[:numbers].reduce(&:+)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
{% info_block %}
|
|
319
|
+
|
|
320
|
+
It's considered a good practice to use the just enough data for the
|
|
321
|
+
input for the action to perform the job. That means not too much
|
|
322
|
+
(such as using ActiveRecord's attributes), as it might have
|
|
323
|
+
performance impact as well as causes issues when changing the
|
|
324
|
+
attributes later.
|
|
325
|
+
|
|
326
|
+
On the other hand, the input should contain enough data to perform
|
|
327
|
+
the job without the need for reaching to external sources. Therefore,
|
|
328
|
+
instead of passing just the ActiveRecord id and loading the whole
|
|
329
|
+
record again in run phase, just to use some attributes, it's better to
|
|
330
|
+
use these attributes directly as input of the action.
|
|
331
|
+
|
|
332
|
+
Following theses rules should lead to the best results, both from
|
|
333
|
+
readability and performance point of view.
|
|
334
|
+
|
|
335
|
+
{% endinfo_block %}
|
|
336
|
+
|
|
337
|
+
Now let's see an example with action planning:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
class SumNumbers < Dynflow::Action
|
|
341
|
+
def plan(numbers)
|
|
342
|
+
plan_self numbers: numbers
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def run
|
|
346
|
+
output.update sum: input[:numbers].reduce(&:+)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
class SumManyNumbers < Dynflow::Action
|
|
351
|
+
def plan(numbers)
|
|
352
|
+
# references to planned actions
|
|
353
|
+
planned_sub_sum_actions = numbers.each_slice(10).map do |numbers|
|
|
354
|
+
plan_action SumNumbers, numbers
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# prepare array of output references where each points to sum in the
|
|
358
|
+
# output of particular action
|
|
359
|
+
sub_sums = planned_sub_sum_actions.map do |action|
|
|
360
|
+
action.output[:sum]
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# plan one last action which will sum the sub_sums
|
|
364
|
+
# it depends on all planned_sub_sum_actions because it uses theirs outputs
|
|
365
|
+
plan_action SumNumbers, sub_sums
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
world_instance.trigger SumManyNumbers, (1..100).to_a
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Above example will in parallel sum numbers by slices of 10 values: first action sums `1..10`,
|
|
373
|
+
second action sums `11..20`, ..., tenth action sums `91..100`. After all sub sums are computed
|
|
374
|
+
one final action sums the sub sums into final sum.
|
|
375
|
+
|
|
376
|
+
{% warning_block %}
|
|
377
|
+
|
|
378
|
+
This example is here to demonstrate the planning abilities. In reality this parallelization of
|
|
379
|
+
compute intensive tasks does not have a positive effect on Dynflow running on MRI. The pool of
|
|
380
|
+
workers may starve. It is not a big issue since Dynflow is mainly used to orchestrate external
|
|
381
|
+
services.
|
|
382
|
+
|
|
383
|
+
*TODO add link to detail explanation in How it works when available.*
|
|
384
|
+
|
|
385
|
+
{% endwarning_block %}
|
|
386
|
+
|
|
387
|
+
Action may access local DB in plan phase,
|
|
388
|
+
see [Database and Transactions](#database-and-transactions).
|
|
389
|
+
|
|
390
|
+
#### Run phase
|
|
391
|
+
|
|
392
|
+
Actions has a run phase if there is `run` method implemented.
|
|
393
|
+
(There may be actions just planning other actions.)
|
|
394
|
+
|
|
395
|
+
The run method implements the main piece of work done by this action converting
|
|
396
|
+
input into output. Input is immutable in this phase. It's the right place for all the steps
|
|
397
|
+
which are likely to fail. Action `run` phase are allowed to have side effects like: file operations,
|
|
398
|
+
calls to other systems, etc.
|
|
399
|
+
Local DB should not be accessed in this phase,
|
|
400
|
+
see [Database and Transactions](#database-and-transactions)
|
|
401
|
+
|
|
402
|
+
#### Finale phase
|
|
403
|
+
|
|
404
|
+
Main purpose of `finalize` phase is to be able access local DB after action finishes
|
|
405
|
+
successfully, like: indexing based on new data, updating records as fully created, etc.
|
|
406
|
+
Finalize phase does not modify input or output of the action.
|
|
407
|
+
Action may access local DB in `finalize` phase and must be **idempotent**,
|
|
408
|
+
see [Database and Transactions](#database-and-transactions).
|
|
409
|
+
|
|
410
|
+
### Dependencies
|
|
411
|
+
|
|
412
|
+
As already mentioned, actions can use output of different actions as their input (or just parts).
|
|
413
|
+
When they do it creates dependency between actions, which is automatically detected
|
|
414
|
+
by Dynflow and the execution plan is built accordingly.
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
def plan
|
|
418
|
+
first_action = plan_action AnAction
|
|
419
|
+
second_action = plan_action AnAction, first_action.output[:a_key_in_output]
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
`second_action` uses part of the `first_action`'s output
|
|
424
|
+
therefore it depends on the `first_action`.
|
|
425
|
+
|
|
426
|
+
If actions are planned without this dependency as follows
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
def plan
|
|
430
|
+
first_action = plan_action AnAction
|
|
431
|
+
second_action = plan_action AnAction
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
then they are independent and they are executed concurrently.
|
|
436
|
+
|
|
437
|
+
There is also other mechanism how to describe dependencies between actions than just
|
|
438
|
+
the one based on output usage. Dynflow user can specify the order between planned
|
|
439
|
+
actions with DSL methods `sequence` and `concurrence`. Both methods are taking blocks
|
|
440
|
+
and they specify how actions planned inside the block
|
|
441
|
+
(or inner `sequence` and `concurrence` blocks) should be executed.
|
|
442
|
+
|
|
443
|
+
By default `plan` considers it's space as inside `concurrence`. Which means
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
def plan
|
|
447
|
+
first_action = plan_action AnAction
|
|
448
|
+
second_action = plan_action AnAction
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
equals
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
def plan
|
|
455
|
+
concurrence do
|
|
456
|
+
first_action = plan_action AnAction
|
|
457
|
+
second_action = plan_action AnAction
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
You can establish same dependency between `first_action` and `second_action` without
|
|
463
|
+
using output by using `sequence`
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
def plan
|
|
467
|
+
sequence do
|
|
468
|
+
first_action = plan_action AnAction
|
|
469
|
+
second_action = plan_action AnAction
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
As mentioned the `sequence` and `concurrence` methods can be nested and mixed
|
|
475
|
+
with output usage to create more complex dependencies. Let see commented example:
|
|
476
|
+
|
|
477
|
+
```ruby
|
|
478
|
+
def plan
|
|
479
|
+
# Plans 3 actions of type AnAction to be executed in sequence
|
|
480
|
+
# argument is the index in the sequence.
|
|
481
|
+
actions_executed_sequentially = sequence do
|
|
482
|
+
3.times.map { |i| plan_action AnAction, i }
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Depends on output of the last action in `actions_executed_sequentially`
|
|
486
|
+
# so it's added to the above sequence to be executed as 4th.
|
|
487
|
+
action1 = plan_action AnAction, actions_executed_sequentially.last.output
|
|
488
|
+
|
|
489
|
+
# It's planed in default plan's concurrency scope it's executed concurrently
|
|
490
|
+
# to about four actions.
|
|
491
|
+
action2 = plan_action AnAction
|
|
492
|
+
end
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
The order than will be:
|
|
496
|
+
|
|
497
|
+
- concurrently:
|
|
498
|
+
- sequentially:
|
|
499
|
+
1. `*actions_executed_sequentially`
|
|
500
|
+
1. `action1`
|
|
501
|
+
- `action2`
|
|
502
|
+
|
|
503
|
+
Let's see one more example:
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
def plan
|
|
507
|
+
actions = sequence do
|
|
508
|
+
2.times.map do |i|
|
|
509
|
+
concurrency do
|
|
510
|
+
2.times.map { plan_action AnAction, i }
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
```
|
|
516
|
+
Which results in order of execution:
|
|
517
|
+
|
|
518
|
+
- sequentially:
|
|
519
|
+
1. concurrently:
|
|
520
|
+
- `actions[0][0]` argument: 0
|
|
521
|
+
- `actions[0][1]` argument: 0
|
|
522
|
+
1. concurrently:
|
|
523
|
+
- `actions[1][0]` argument: 1
|
|
524
|
+
- `actions[1][1]` argument: 1
|
|
525
|
+
|
|
526
|
+
{% info_block %}
|
|
527
|
+
|
|
528
|
+
It's on our todo-list to change that to be able to define acyclic-graph of dependencies
|
|
529
|
+
between the actions. `sequence` and `concurrence` methods will then be deprecated and kept
|
|
530
|
+
just for backward compatibility.
|
|
531
|
+
|
|
532
|
+
{% endinfo_block %}
|
|
533
|
+
|
|
534
|
+
{% warning_block %}
|
|
535
|
+
|
|
536
|
+
Internally dependencies are also modeled with objects representing Sequences and Concurrences,
|
|
537
|
+
which makes it weaker than acyclic-graph so in some cases during the dependency resolution
|
|
538
|
+
it might not lead into the most effective execution plan. Some actions will run in sequence even
|
|
539
|
+
though they could be run concurrently. This limitation is likely to be
|
|
540
|
+
removed in some of the further releases.
|
|
541
|
+
|
|
542
|
+
{% endwarning_block %}
|
|
543
|
+
|
|
544
|
+
### Database and Transactions
|
|
545
|
+
|
|
546
|
+
Dynflow was designed to help with orchestration of other services.
|
|
547
|
+
The usual execution looks as follows, we use an ActiveRecord User as example of a resource.
|
|
548
|
+
|
|
549
|
+
1. Trigger user creation, argument is an unsaved ActiveRecord user object
|
|
550
|
+
1. Planning: The user is stored in local DB (in the Dynflow hosting application) within the
|
|
551
|
+
`plan` phase. The record is marked as incomplete.
|
|
552
|
+
1. Running: Operations needed for the user in external services with (e.g.) REST call.
|
|
553
|
+
The phase finishes when the all the external calls succeeded successfully.
|
|
554
|
+
1. Finalizing: The record in local DB is marked as done: ready to be
|
|
555
|
+
used. Potentially, saving some data that were retrieved in the `run`
|
|
556
|
+
phase back to the local database.
|
|
557
|
+
|
|
558
|
+
For that reason there are transactions around whole `plan` and `finale` phase
|
|
559
|
+
(all action's plan methods are in one transaction).
|
|
560
|
+
If anything goes wrong in the `plan` phase any change made during planning to local DB is
|
|
561
|
+
reverted. Same holds for finalizing, if anything goes wrong, all changes are reverted. Therefore
|
|
562
|
+
all finalization methods has to be **idempotent**.
|
|
563
|
+
|
|
564
|
+
Internally Dynflow uses Sequel as its ORM, but users may choose what they need
|
|
565
|
+
to access they data. There is an interface `TransactionAdapters::Abstract` where its
|
|
566
|
+
implementations may provide transactions using different ORMs.
|
|
567
|
+
The most common one probably being `TransactionAdapters::ActiveRecord`.
|
|
568
|
+
|
|
569
|
+
So in the above example 2. and 4. step would be wrapped in `ActiveRecord` transaction
|
|
570
|
+
if `TransactionAdapters::ActiveRecord` is used.
|
|
571
|
+
|
|
572
|
+
Second outcome of the design is convention when actions should be accessing local Database:
|
|
573
|
+
|
|
574
|
+
- **allowed** - in `plan` and `finalize` phases
|
|
575
|
+
- **disallowed** - (or at least discouraged) in the `run` phase
|
|
576
|
+
|
|
577
|
+
{% warning_block %}
|
|
578
|
+
|
|
579
|
+
*TODO warning about AR pool configuration, needs to have sufficient size*
|
|
580
|
+
|
|
581
|
+
{% endwarning_block %}
|
|
582
|
+
|
|
583
|
+
### Composition
|
|
584
|
+
|
|
585
|
+
Dynflow is designed to allow easy composition of small building blocks
|
|
586
|
+
called `Action`s. Typically there are actions composing smaller pieces
|
|
587
|
+
together and other actions doing actual steps of work as in following
|
|
588
|
+
example:
|
|
589
|
+
|
|
590
|
+
```ruby
|
|
591
|
+
class CreateInfrastructure < Dynflow::Action
|
|
592
|
+
def plan
|
|
593
|
+
sequence do
|
|
594
|
+
concurrence do
|
|
595
|
+
plan_action(CreateMachine, 'host1', 'db')
|
|
596
|
+
plan_action(CreateMachine, 'host2', 'storage')
|
|
597
|
+
end
|
|
598
|
+
plan_action(CreateMachine,
|
|
599
|
+
'host3',
|
|
600
|
+
'web_server',
|
|
601
|
+
:db_machine => 'host1',
|
|
602
|
+
:storage_machine => 'host2')
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
```
|
|
607
|
+
Action `CreateInfrastructure` does not have a `run` method defined, it only
|
|
608
|
+
defines `plan` action where other actions composed together.
|
|
609
|
+
|
|
610
|
+
### Subscriptions
|
|
611
|
+
|
|
612
|
+
Even though composing actions is quite easy and allows to decompose
|
|
613
|
+
business logic to small pieces it does not directly support extensions
|
|
614
|
+
by plugins. For that there are subscriptions.
|
|
615
|
+
|
|
616
|
+
`Actions` can subscribe from a plugin, gem, any other library to already
|
|
617
|
+
loaded `Actions`, doing so they extend the planning process with self.
|
|
618
|
+
|
|
619
|
+
Lets look at an example starting by definition of a core action
|
|
620
|
+
|
|
621
|
+
```ruby
|
|
622
|
+
# This action can be extended without doing any
|
|
623
|
+
# other steps to support it.
|
|
624
|
+
class ACoreAppAction < Dynflow::Action
|
|
625
|
+
def plan(arguments)
|
|
626
|
+
plan_self(args: arguments)
|
|
627
|
+
plan_action(AnotherCoreAppAction, arguments.first)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def run
|
|
631
|
+
puts "Running core action: #{input[:args]}"
|
|
632
|
+
self.output.update success: true
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
followed by an action definition defined in a plugin/gem/etc.
|
|
638
|
+
|
|
639
|
+
```ruby
|
|
640
|
+
class APluginAction < Dynflow::Action
|
|
641
|
+
# plan this action whenever ACoreAppAction action is planned
|
|
642
|
+
def self.subscribe
|
|
643
|
+
ACoreAppAction
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def plan(arguments)
|
|
647
|
+
# arguments are same as in ACoreAppAction#plan
|
|
648
|
+
plan_self(args: arguments)
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def run
|
|
652
|
+
puts "Running plugin action: #{input[:args]}"
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Subscribed actions are planned with same arguments as action they are
|
|
658
|
+
subscribing to which is called `trigger`. Their plan method is called right
|
|
659
|
+
after planning of the triggering action finishes.
|
|
660
|
+
|
|
661
|
+
It's also possible to access target action and use its output which
|
|
662
|
+
makes it dependent (running in sequence) on triggering action.
|
|
663
|
+
|
|
664
|
+
```ruby
|
|
665
|
+
def plan(arguments)
|
|
666
|
+
plan_self trigger_success: trigger.output[:success]
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def run
|
|
670
|
+
self.output.update 'trigger succeeded' if self.input[:trigger_success]
|
|
671
|
+
end
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Subscription is designed for extension by plugins, it should **not** be used
|
|
675
|
+
inside a single library/app-module. It would make the process definition
|
|
676
|
+
hard to follow (all subscribed actions would need to be looked up).
|
|
677
|
+
|
|
678
|
+
### Suspending
|
|
679
|
+
|
|
680
|
+
Sometimes action represents tasks taken in different services,
|
|
681
|
+
(e.g. repository synchronization in [Pulp](http://www.pulpproject.org/)).
|
|
682
|
+
Dynflow tries not to waste computer resources so it offers tools to free
|
|
683
|
+
threads to work on other actions while waiting on external tasks or events.
|
|
684
|
+
|
|
685
|
+
Dynflow allows actions to suspend and be woken up on external events.
|
|
686
|
+
Lets create a simulation of an external service before showing the example
|
|
687
|
+
of suspending action.
|
|
688
|
+
|
|
689
|
+
```ruby
|
|
690
|
+
class AnExternalService
|
|
691
|
+
def start_synchronization(report_to)
|
|
692
|
+
Thread.new do
|
|
693
|
+
sleep 1
|
|
694
|
+
report_to << :done
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
The `AnExternalService` can be invoked to `start_synchronization` and it will
|
|
701
|
+
report back a second later to action passed in argument `report_to`. It sends
|
|
702
|
+
event `:done` back by `<<` method.
|
|
703
|
+
|
|
704
|
+
Lets look at an action example.
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
class AnAction < Dynflow::Action
|
|
708
|
+
EXTERNAL_SERVICE = AnExternalService.new
|
|
709
|
+
|
|
710
|
+
def plan
|
|
711
|
+
plan_self
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def run(event)
|
|
715
|
+
case event
|
|
716
|
+
when nil # first run
|
|
717
|
+
suspend do |suspended_action|
|
|
718
|
+
EXTERNAL_SERVICE.start_synchronization suspended_action
|
|
719
|
+
end
|
|
720
|
+
when :done # external task is done
|
|
721
|
+
output.update success: true
|
|
722
|
+
# let the run phase finish normally
|
|
723
|
+
else
|
|
724
|
+
raise 'unknown event'
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
```
|
|
729
|
+
Which is then executed as follows:
|
|
730
|
+
|
|
731
|
+
1. `AnAction` is triggered
|
|
732
|
+
1. It's planned.
|
|
733
|
+
1. Its `run` phase begins.
|
|
734
|
+
1. `run` method is invoked with no event (`nil`).
|
|
735
|
+
1. Matches with case branch initiating the external synchronization.
|
|
736
|
+
1. Action initializes the synchronization and pass in reference
|
|
737
|
+
to suspended_action.
|
|
738
|
+
1. Action is suspended, execution of the run method finishes immediately
|
|
739
|
+
after `suspend` is called, its block parameter is evaluated right after
|
|
740
|
+
suspending.
|
|
741
|
+
1. Action is kept on memory to be woken up when events are received but it does not
|
|
742
|
+
block any threads.
|
|
743
|
+
1. Action receives `:done` event through suspend action reference.
|
|
744
|
+
1. `run` method is executed again with `:done` event.
|
|
745
|
+
1. Output is updated with `success: true` and actions finishes `run` phase.
|
|
746
|
+
1. There is no `finalize` phase, action is done.
|
|
747
|
+
|
|
748
|
+
This event mechanism is quite flexible, it can be used for example to build a
|
|
749
|
+
[polling action abstraction](https://github.com/Dynflow/dynflow/blob/master/lib/dynflow/action/polling.rb)
|
|
750
|
+
which is a topic for next chapter.
|
|
751
|
+
|
|
752
|
+
### Polling
|
|
753
|
+
|
|
754
|
+
Not all services support callbacks to be registered which would allow to wake up suspended
|
|
755
|
+
actions only once at the end when the external task is finished. In that case we often
|
|
756
|
+
need to poll the service to see if the task is still running or finished.
|
|
757
|
+
|
|
758
|
+
For that purpose there is `Polling` module in Dynflow. Any action can be turned into a polling one
|
|
759
|
+
just by including the module.
|
|
760
|
+
|
|
761
|
+
```ruby
|
|
762
|
+
class AnAction < Dynflow::Action
|
|
763
|
+
include Dynflow::Action::Polling
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
There are 3 methods need to be always implemented:
|
|
767
|
+
|
|
768
|
+
- `done?` - determines when the task is complete based on external task's data.
|
|
769
|
+
- `invoke_external_task` - starts the external task.
|
|
770
|
+
- `poll_external_task` - polls the external task status data and returns a status
|
|
771
|
+
(JSON serializable data like: `Hash`, `Array`, `String`, etc.) which are stored in action's
|
|
772
|
+
output.
|
|
773
|
+
|
|
774
|
+
```ruby
|
|
775
|
+
def done?
|
|
776
|
+
external_task[:progress] == 1
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def invoke_external_task
|
|
780
|
+
triger_the_task_with_rest_call
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def poll_external_task
|
|
784
|
+
data = poll_data_with_rest_call
|
|
785
|
+
progress = calculate_progress data # => a float in 0..1
|
|
786
|
+
{ progress: progress
|
|
787
|
+
data: data }
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
This action will do following in run phase:
|
|
793
|
+
|
|
794
|
+
1. `invoke_external_task` on first run of the action
|
|
795
|
+
1. suspends and then periodically:
|
|
796
|
+
1. wakes up
|
|
797
|
+
1. `poll_external_task`
|
|
798
|
+
1. checks if `done?`:
|
|
799
|
+
- `true` -> it concludes the run phase
|
|
800
|
+
- `false` -> it schedules next polling
|
|
801
|
+
|
|
802
|
+
There are 2 other methods handling external task data which can optionally overridden:
|
|
803
|
+
|
|
804
|
+
- `external_task` - reads the external task's stored data, by default it reads `self.output[:task]`
|
|
805
|
+
- `external_task=` - writes the the external task's stored data, by default it writes to
|
|
806
|
+
`self.output[:task] = value`
|
|
807
|
+
|
|
808
|
+
There are also other features implemented like:
|
|
809
|
+
|
|
810
|
+
- Gradual prolongation of the polling interval.
|
|
811
|
+
- Retries on a poll failing.
|
|
812
|
+
|
|
813
|
+
Please see the
|
|
814
|
+
[`Polling` module](https://github.com/Dynflow/dynflow/blob/master/lib/dynflow/action/polling.rb)
|
|
815
|
+
for more details.
|
|
816
|
+
|
|
817
|
+
### States
|
|
818
|
+
|
|
819
|
+
Each **Action phase** can be in one of the following states:
|
|
820
|
+
|
|
821
|
+
- **Pending** - Not yet executed.
|
|
822
|
+
- **Running** - An action phase id being executed right now.
|
|
823
|
+
- **Success** - Execution of an action phase finished successfully.
|
|
824
|
+
- **Error** - There was an error during execution.
|
|
825
|
+
- **Suspended** - Only `run` phase, when action sleeps waiting for events to be woken up.
|
|
826
|
+
- **Skipped** - Failed actions can be marked as skipped allowing rest of the
|
|
827
|
+
execution plan to finish successfully.
|
|
828
|
+
- **Skipping** - Action is marked for skipping but execution plan was not yet
|
|
829
|
+
resumed to mark it as Skipped.
|
|
830
|
+
|
|
831
|
+
**Execution plan** has following states:
|
|
832
|
+
|
|
833
|
+
- **Pending** - Planning did not start yet.
|
|
834
|
+
- **Planning** - It's being planned.
|
|
835
|
+
- **Planned** - It've been planned, running phase did not start yet.
|
|
836
|
+
- **Running** - It's running, `run` and `finalize` phases of actions are executed.
|
|
837
|
+
- **Paused** - It was paused when running. Happens on error or executor restart.
|
|
838
|
+
- **Stopped** - Execution plan is completed.
|
|
839
|
+
|
|
840
|
+
**Execution plan** also has following results:
|
|
841
|
+
|
|
842
|
+
- **Success** - Everything finished without error or skips.
|
|
843
|
+
- **Warning** - When there are skipped steps.
|
|
844
|
+
- **Error** - When one or more actions failed.
|
|
845
|
+
- **Pending** - Execution plan still runs.
|
|
846
|
+
|
|
847
|
+
*TODO how do I access such states as a programmer?*
|
|
848
|
+
*TODO which Action phase states are "finish" and which requires user interaction?*
|
|
849
|
+
|
|
850
|
+
### Error handling
|
|
851
|
+
|
|
852
|
+
If there is an error risen in **`plan` phase**, the error is persisted in the Action object
|
|
853
|
+
for later inspection and it bubbles up in `World#trigger` method which was used to trigger
|
|
854
|
+
the action leading to this error.
|
|
855
|
+
If you compare it to errors raised during `run` and `finalize` phase,
|
|
856
|
+
there's the major difference: Those never bubble up in `trigger` because they are running
|
|
857
|
+
in executor not in triggering Thread, they are just persisted in Action object.
|
|
858
|
+
|
|
859
|
+
If there is an error in **`run` phase**, the execution pauses. You can inspect the error in
|
|
860
|
+
[console](#console). The error may be intermittent or you may fix the problem manually. After
|
|
861
|
+
that the execution plan can be resumed and it'll continue by rerunning the failed action and
|
|
862
|
+
continuing with the rest of the actions. During fixing the problem you may also do the steps
|
|
863
|
+
in the actions manually, in that case the failed action can be also marked as skipped. After
|
|
864
|
+
resuming the skipped action is not executed and the execution plan continues with the rest.
|
|
865
|
+
|
|
866
|
+
If there is an error in **`finalize` phase**, whole `finalize` phase for all the actions is
|
|
867
|
+
rollbacked and can be rerun when the problem is fixed by resuming.
|
|
868
|
+
|
|
869
|
+
If you encounter an error during run phase `error!` or usual `raise` can be used.
|
|
870
|
+
|
|
871
|
+
#### Rescue strategy TODO
|
|
872
|
+
|
|
873
|
+
### Console TODO
|
|
874
|
+
|
|
875
|
+
- *where to access*
|
|
876
|
+
- *screenshots*
|
|
877
|
+
|
|
878
|
+
### Testing TODO
|
|
879
|
+
|
|
880
|
+
- *testing helper methods*
|
|
881
|
+
- *examples*
|
|
882
|
+
- *see [testing of testing](https://github.com/Dynflow/dynflow/blob/master/test/testing_test.rb)*
|
|
883
|
+
|
|
884
|
+
### Long-running actions
|
|
885
|
+
|
|
886
|
+
Dynflow was designed as an Orchestration tool, parallelization of heavy CPU computation tasks
|
|
887
|
+
was not directly considered. Even with multiple executors single execution plan always runs
|
|
888
|
+
on one executor, so without JRuby it wont scale well (MRI's GIL). However JRuby support
|
|
889
|
+
should be added soon (TODO update when merged).
|
|
890
|
+
|
|
891
|
+
Another problem with long-running actions are blocked worker. Executor has only a limited pool of
|
|
892
|
+
workers, if more of them become busy it may result in worsen performance.
|
|
893
|
+
|
|
894
|
+
Blocking actions for long time are also problematic.
|
|
895
|
+
|
|
896
|
+
Solutions are:
|
|
897
|
+
|
|
898
|
+
- **Using action suspending** - suspending the action until a condition is met,
|
|
899
|
+
freeing the worker.
|
|
900
|
+
- **Offloading computation** - CPU heavy parts can be offloaded to different services
|
|
901
|
+
notifying the suspended actions when the computation is done.
|
|
902
|
+
|
|
903
|
+
### Middleware
|
|
904
|
+
|
|
905
|
+
Each action class has chain of middlewares which wrap phases of the action execution.
|
|
906
|
+
It's very similar to rack middlewares.
|
|
907
|
+
To create new middleware inherit from `Dynflow::Middleware` class. It has 5 methods which can be
|
|
908
|
+
overridden: `plan`, `run`, `finalize`, `plan_phase`, `finalize_phase`. Where the default
|
|
909
|
+
implementation for all the methods looks as following
|
|
910
|
+
|
|
911
|
+
```ruby
|
|
912
|
+
def plan(*args)
|
|
913
|
+
pass *args
|
|
914
|
+
end
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
When overriding user can insert code before and/or after the `pass` method which executes next
|
|
918
|
+
middleware in the chain or the action itself which is at the end of the chain. Most usually the
|
|
919
|
+
`pass` is always called somewhere in the overridden method. There may be some cases when it can
|
|
920
|
+
be omitted, then it'll prevent all following middlewares and action from running.
|
|
921
|
+
|
|
922
|
+
Some implementation examples:
|
|
923
|
+
[KeepCurrentUser](https://github.com/theforeman/foreman-tasks/blob/master/app/lib/actions/middleware/keep_current_user.rb),
|
|
924
|
+
[Action::Progress::Calculate](https://github.com/Dynflow/dynflow/blob/master/lib/dynflow/action/progress.rb#L13-L42).
|
|
925
|
+
|
|
926
|
+
Each Action has a chain of middlewares defined. Middleware can be added by calling `use`
|
|
927
|
+
in the action class.
|
|
928
|
+
|
|
929
|
+
```ruby
|
|
930
|
+
class AnAction < Dynflow::Action
|
|
931
|
+
use AMiddleware, after: AnotherMiddleware
|
|
932
|
+
end
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
Method `use` understands 3 option keys:
|
|
936
|
+
|
|
937
|
+
- `:before` - makes this middleware to be ordered before a given middleware
|
|
938
|
+
- `:after` - makes this middleware to be ordered after a given middleware
|
|
939
|
+
- `:replace` - this middleware will replace given middleware
|
|
940
|
+
|
|
941
|
+
The `:before` and `:after` keys are used to build a graph from the middlewares which is then
|
|
942
|
+
sorted down with
|
|
943
|
+
[topological sort](http://ruby-doc.org//stdlib-2.0/libdoc/tsort/rdoc/TSort.html)
|
|
944
|
+
to the chain of middleware execution.
|
|
945
|
+
|
|
946
|
+
### SubTasks TODO
|
|
947
|
+
|
|
948
|
+
- *when to use?*
|
|
949
|
+
- *how to use?*
|
|
950
|
+
|
|
951
|
+
## How it works TODO
|
|
952
|
+
|
|
953
|
+
### Action states TODO
|
|
954
|
+
|
|
955
|
+
- *normal phases and Present phase*
|
|
956
|
+
- *how to walk the execution plan*
|
|
957
|
+
|
|
958
|
+
### Inner-world communication and multi-executors TODO
|
|
959
|
+
|
|
960
|
+
### Thread-pools TODO
|
|
961
|
+
|
|
962
|
+
- *how it works now*
|
|
963
|
+
- *how it'll work*
|
|
964
|
+
- *gotchas*
|
|
965
|
+
- *worker pool sizing*
|
|
966
|
+
|
|
967
|
+
### Suspending -> events TODO
|
|
968
|
+
|
|
969
|
+
## Use cases TODO
|
|
970
|
+
|
|
971
|
+
- *Embedded without a DB, like inside CLI tool for a complex installation*
|
|
972
|
+
- *reserve resources in planning do not try to do `if`s in run phase*
|
|
973
|
+
- *Projects: katello, foreman, staypuft, fusor*
|
|
974
|
+
|
|
975
|
+
## Comments
|
|
976
|
+
|
|
977
|
+
**Comments are temporally turned on here for faster feedback.**
|