From a988a1a043543495b64f5001346a1704ecad9774 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 19 Jan 2015 16:54:23 -0800 Subject: [PATCH] Add a "daily routine" trigger clock for backups, etc. Summary: Ref T6881. Before implementing subscriptions, I'm going to vet triggers by using them to do backups. Each instance will get a daily trigger for backups, and that should give us a smaller-scale test to catch issues and limitations, with more opportunities for something to go wrong since it fires more often. Test Plan: Added unit tests. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T6881 Differential Revision: https://secure.phabricator.com/D11427 --- resources/celerity/map.php | 126 +++++++++--------- src/__phutil_library_map__.php | 2 + .../PhabricatorDailyRoutineTriggerClock.php | 57 ++++++++ .../PhabricatorTriggerClockTestCase.php | 100 +++++++++++++- 4 files changed, 219 insertions(+), 66 deletions(-) create mode 100644 src/infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 8eb30571e7..bb82ed06f4 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,10 +8,10 @@ return array( 'names' => array( 'core.pkg.css' => '8d1c0f87', - 'core.pkg.js' => 'd60876a7', + 'core.pkg.js' => '7923c2e6', 'darkconsole.pkg.js' => '8ab24e01', 'differential.pkg.css' => '8af45893', - 'differential.pkg.js' => 'f437e70e', + 'differential.pkg.js' => 'dad3622f', 'diffusion.pkg.css' => '591664fa', 'diffusion.pkg.js' => 'bfc0737b', 'maniphest.pkg.css' => 'e34dfbec', @@ -166,10 +166,10 @@ return array( 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', - 'rsrc/externals/javelin/core/init.js' => '8c4e8f8b', - 'rsrc/externals/javelin/core/init_node.js' => 'c234aded', + 'rsrc/externals/javelin/core/init.js' => '76e1fd61', + 'rsrc/externals/javelin/core/init_node.js' => '77350e4d', 'rsrc/externals/javelin/core/install.js' => '05270951', - 'rsrc/externals/javelin/core/util.js' => '93cc50d6', + 'rsrc/externals/javelin/core/util.js' => 'bdcfee9e', 'rsrc/externals/javelin/docs/Base.js' => '74676256', 'rsrc/externals/javelin/docs/onload.js' => 'e819c479', 'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a', @@ -186,11 +186,11 @@ return array( 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2', 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472', 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb', - 'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b', + 'rsrc/externals/javelin/ext/view/__tests__/View.js' => 'bda69c40', 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9', - 'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03', - 'rsrc/externals/javelin/lib/DOM.js' => 'c8fd8db2', + 'rsrc/externals/javelin/lib/Cookie.js' => '6b3dcf44', + 'rsrc/externals/javelin/lib/DOM.js' => 'c4569c05', 'rsrc/externals/javelin/lib/History.js' => 'c60f4327', 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', 'rsrc/externals/javelin/lib/Leader.js' => '9330f91b', @@ -215,7 +215,7 @@ return array( 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'e3b841c8', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '316b8fa1', 'rsrc/externals/raphael/g.raphael.js' => '40dde778', 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', @@ -362,7 +362,7 @@ return array( 'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d', 'rsrc/js/application/differential/behavior-comment-preview.js' => '6932def3', 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', - 'rsrc/js/application/differential/behavior-dropdown-menus.js' => 'e33d4bc5', + 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '3bc14668', 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '00861799', 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492', 'rsrc/js/application/differential/behavior-populate.js' => 'bdb3e4d0', @@ -410,7 +410,7 @@ return array( 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', - 'rsrc/js/application/releeph/releeph-request-state-change.js' => '3a1a4060', + 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'ab836011', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', 'rsrc/js/application/repository/repository-crossreference.js' => 'f9539603', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', @@ -444,7 +444,7 @@ return array( 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '5c93c52c', 'rsrc/js/core/Title.js' => '5c1c758c', - 'rsrc/js/core/ToolTip.js' => '1d298e3a', + 'rsrc/js/core/ToolTip.js' => '031d4411', 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', @@ -564,7 +564,7 @@ return array( 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 'javelin-behavior-differential-comment-jump' => '4fdb476d', 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', - 'javelin-behavior-differential-dropdown-menus' => 'e33d4bc5', + 'javelin-behavior-differential-dropdown-menus' => '3bc14668', 'javelin-behavior-differential-edit-inline-comments' => '00861799', 'javelin-behavior-differential-feedback-preview' => '6932def3', 'javelin-behavior-differential-keyboard-navigation' => '2c426492', @@ -635,7 +635,7 @@ return array( 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-refresh-csrf' => '7814b593', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', - 'javelin-behavior-releeph-request-state-change' => '3a1a4060', + 'javelin-behavior-releeph-request-state-change' => 'ab836011', 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 'javelin-behavior-remarkup-preview' => 'f7379f45', 'javelin-behavior-reorder-applications' => '76b9fc3e', @@ -650,9 +650,9 @@ return array( 'javelin-behavior-view-placeholder' => '47830651', 'javelin-behavior-workflow' => '0a3f3021', 'javelin-color' => '7e41274a', - 'javelin-cookie' => '62dfea03', + 'javelin-cookie' => '6b3dcf44', 'javelin-diffusion-locate-file-source' => 'b42eddc7', - 'javelin-dom' => 'c8fd8db2', + 'javelin-dom' => 'c4569c05', 'javelin-dynval' => 'f6555212', 'javelin-event' => '85ea0626', 'javelin-fx' => '54b612ba', @@ -660,7 +660,7 @@ return array( 'javelin-install' => '05270951', 'javelin-json' => '69adf288', 'javelin-leader' => '9330f91b', - 'javelin-magical-init' => '8c4e8f8b', + 'javelin-magical-init' => '76e1fd61', 'javelin-mask' => '8a41885b', 'javelin-reactor' => '2b8de964', 'javelin-reactor-dom' => 'c90a04fc', @@ -677,10 +677,10 @@ return array( 'javelin-typeahead-normalizer' => '6f7a9da8', 'javelin-typeahead-ondemand-source' => '8b3fd187', 'javelin-typeahead-preloaded-source' => '54f314a0', - 'javelin-typeahead-source' => '2818f5ce', + 'javelin-typeahead-source' => 'e3b841c8', 'javelin-typeahead-static-source' => '316b8fa1', 'javelin-uri' => '6eff08aa', - 'javelin-util' => '93cc50d6', + 'javelin-util' => 'bdcfee9e', 'javelin-vector' => 'cc1bd0b0', 'javelin-view' => '0f764c35', 'javelin-view-html' => 'fe287620', @@ -738,7 +738,7 @@ return array( 'phabricator-standard-page-view' => '2c96cfb5', 'phabricator-textareautils' => '5c93c52c', 'phabricator-title' => '5c1c758c', - 'phabricator-tooltip' => '1d298e3a', + 'phabricator-tooltip' => '031d4411', 'phabricator-transaction-view-css' => '5d0cae25', 'phabricator-ui-example-css' => '528b19de', 'phabricator-uiexample-javelin-view' => 'd4a14807', @@ -834,6 +834,12 @@ return array( '029a133d' => array( 'aphront-dialog-view-css', ), + '031d4411' => array( + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-vector', + ), '03d6ed07' => array( 'javelin-behavior', 'javelin-stratcom', @@ -938,12 +944,6 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), - '1d298e3a' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-vector', - ), '1def2711' => array( 'javelin-install', 'javelin-dom', @@ -956,12 +956,6 @@ return array( 'javelin-workflow', 'javelin-util', ), - '2818f5ce' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-typeahead-normalizer', - ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', @@ -1029,14 +1023,6 @@ return array( 'javelin-json', 'phabricator-prefab', ), - '3a1a4060' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-util', - 'phabricator-keyboard-shortcut', - ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1045,6 +1031,18 @@ return array( 'javelin-dom', 'javelin-magical-init', ), + '3bc14668' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phuix-dropdown-menu', + 'phuix-action-list-view', + 'phuix-action-view', + 'phabricator-phtize', + 'changeset-view-manager', + ), '3d51a746' => array( 'javelin-behavior', 'javelin-dom', @@ -1228,10 +1226,6 @@ return array( 'javelin-magical-init', 'javelin-util', ), - '62dfea03' => array( - 'javelin-install', - 'javelin-util', - ), '6453c869' => array( 'javelin-install', 'javelin-dom', @@ -1251,6 +1245,10 @@ return array( '69adf288' => array( 'javelin-install', ), + '6b3dcf44' => array( + 'javelin-install', + 'javelin-util', + ), '6c2b09a2' => array( 'javelin-install', 'javelin-util', @@ -1562,6 +1560,14 @@ return array( 'javelin-util', 'phabricator-prefab', ), + 'ab836011' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-util', + 'phabricator-keyboard-shortcut', + ), 'ad7a69ca' => array( 'javelin-install', 'javelin-util', @@ -1650,6 +1656,13 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), + 'c4569c05' => array( + 'javelin-magical-init', + 'javelin-install', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + ), 'c51ae228' => array( 'javelin-behavior', 'javelin-util', @@ -1663,13 +1676,6 @@ return array( 'javelin-uri', 'javelin-util', ), - 'c8fd8db2' => array( - 'javelin-magical-init', - 'javelin-install', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', @@ -1788,18 +1794,6 @@ return array( 'javelin-workflow', 'javelin-vector', ), - 'e33d4bc5' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phuix-dropdown-menu', - 'phuix-action-list-view', - 'phuix-action-view', - 'phabricator-phtize', - 'changeset-view-manager', - ), 'e379b58e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1807,6 +1801,12 @@ return array( 'javelin-dom', 'javelin-uri', ), + 'e3b841c8' => array( + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-typeahead-normalizer', + ), 'e4cc26b3' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 55a1870bd7..780bf11799 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1596,6 +1596,7 @@ phutil_register_library_map(array( 'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php', 'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php', 'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php', + 'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php', 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', @@ -4801,6 +4802,7 @@ phutil_register_library_map(array( 'PhabricatorDaemonTasksTableView' => 'AphrontView', 'PhabricatorDaemonsApplication' => 'PhabricatorApplication', 'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck', + 'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorDashboard' => array( 'PhabricatorDashboardDAO', 'PhabricatorApplicationTransactionInterface', diff --git a/src/infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php b/src/infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php new file mode 100644 index 0000000000..7b1041cf1b --- /dev/null +++ b/src/infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php @@ -0,0 +1,57 @@ + 'int', + )); + } + + public function getNextEventEpoch($last_epoch, $is_reschedule) { + $start_epoch = $this->getProperty('start'); + if (!$last_epoch) { + $last_epoch = $start_epoch; + } + + $start = new DateTime('@'.$start_epoch); + $last = new DateTime('@'.$last_epoch); + + // NOTE: We're choosing the date from the last event, but the time of day + // from the start event. This allows callers to change when the event + // occurs by updating the trigger's start parameter. + $ymd = $last->format('Y-m-d'); + $hms = $start->format('G:i:s'); + + $next = new DateTime("{$ymd} {$hms} UTC"); + + // Add a day. + // NOTE: DateInterval doesn't exist until PHP 5.3.0, and we currently + // target PHP 5.2.3. + $next->modify('+1 day'); + + return (int)$next->format('U'); + } + +} diff --git a/src/infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php b/src/infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php index ad4ec73bee..a494f2612f 100644 --- a/src/infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php +++ b/src/infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php @@ -30,6 +30,90 @@ final class PhabricatorTriggerClockTestCase extends PhabricatorTestCase { pht('Should never trigger.')); } + public function testDailyRoutineTriggerClockDaylightSavings() { + // These dates are selected to cross daylight savings in PST; they should + // be unaffected. + $start = strtotime('2015-03-05 16:17:18 UTC'); + + $clock = new PhabricatorDailyRoutineTriggerClock( + array( + 'start' => $start, + )); + + $expect_list = array( + '2015-03-06 16:17:18', + '2015-03-07 16:17:18', + '2015-03-08 16:17:18', + '2015-03-09 16:17:18', + '2015-03-10 16:17:18', + ); + + $this->expectClock($clock, $expect_list, pht('Daily Routine (PST)')); + } + + public function testDailyRoutineTriggerClockLeapSecond() { + // These dates cross the leap second on June 30, 2012. There has never + // been a negative leap second, so we can't test that yet. + $start = strtotime('2012-06-28 23:59:59 UTC'); + + $clock = new PhabricatorDailyRoutineTriggerClock( + array( + 'start' => $start, + )); + + $expect_list = array( + '2012-06-29 23:59:59', + '2012-06-30 23:59:59', + '2012-07-01 23:59:59', + '2012-07-02 23:59:59', + ); + + $this->expectClock($clock, $expect_list, pht('Daily Routine (Leap)')); + } + + + public function testCDailyRoutineTriggerClockAdjustTimeOfDay() { + // In this case, we're going to update the time of day on the clock and + // make sure it keeps track of the date but adjusts the time. + $start = strtotime('2015-01-15 6:07:08 UTC'); + + $clock = new PhabricatorDailyRoutineTriggerClock( + array( + 'start' => $start, + )); + + $expect_list = array( + '2015-01-16 6:07:08', + '2015-01-17 6:07:08', + '2015-01-18 6:07:08', + ); + + $last_epoch = $this->expectClock( + $clock, + $expect_list, + pht('Daily Routine (Pre-Adjust)')); + + // Now, change the time of day. + $new_start = strtotime('2015-01-08 1:23:45 UTC'); + + $clock = new PhabricatorDailyRoutineTriggerClock( + array( + 'start' => $new_start, + )); + + $expect_list = array( + '2015-01-19 1:23:45', + '2015-01-20 1:23:45', + '2015-01-21 1:23:45', + ); + + $this->expectClock( + $clock, + $expect_list, + pht('Daily Routine (Post-Adjust)'), + $last_epoch); + } + public function testSubscriptionTriggerClock() { $start = strtotime('2014-01-31 2:34:56 UTC'); @@ -76,7 +160,15 @@ final class PhabricatorTriggerClockTestCase extends PhabricatorTestCase { '2016-03-31 2:34:56', ); - $last_epoch = null; + $this->expectClock($clock, $expect_list, pht('Billing Cycle')); + } + + private function expectClock( + PhabricatorTriggerClock $clock, + array $expect_list, + $clock_name, + $last_epoch = null) { + foreach ($expect_list as $cycle => $expect) { $next_epoch = $clock->getNextEventEpoch( $last_epoch, @@ -84,11 +176,13 @@ final class PhabricatorTriggerClockTestCase extends PhabricatorTestCase { $this->assertEqual( $expect, - id(new DateTime('@'.$next_epoch))->format('Y-m-d g:i:s'), - pht('Billing cycle %s.', $cycle)); + id(new DateTime('@'.$next_epoch))->format('Y-m-d G:i:s'), + pht('%s (%s)', $clock_name, $cycle)); $last_epoch = $next_epoch; } + + return $last_epoch; } }