dynflow 0.7.6 → 0.7.7
Sign up to get free protection for your applications and to get access to all the features.
- 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.**
|