@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+219 -66
+63 -63
resources/celerity/map.php
··· 8 8 return array( 9 9 'names' => array( 10 10 'core.pkg.css' => '8d1c0f87', 11 - 'core.pkg.js' => 'd60876a7', 11 + 'core.pkg.js' => '7923c2e6', 12 12 'darkconsole.pkg.js' => '8ab24e01', 13 13 'differential.pkg.css' => '8af45893', 14 - 'differential.pkg.js' => 'f437e70e', 14 + 'differential.pkg.js' => 'dad3622f', 15 15 'diffusion.pkg.css' => '591664fa', 16 16 'diffusion.pkg.js' => 'bfc0737b', 17 17 'maniphest.pkg.css' => 'e34dfbec', ··· 166 166 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 167 167 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 168 168 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', 169 - 'rsrc/externals/javelin/core/init.js' => '8c4e8f8b', 170 - 'rsrc/externals/javelin/core/init_node.js' => 'c234aded', 169 + 'rsrc/externals/javelin/core/init.js' => '76e1fd61', 170 + 'rsrc/externals/javelin/core/init_node.js' => '77350e4d', 171 171 'rsrc/externals/javelin/core/install.js' => '05270951', 172 - 'rsrc/externals/javelin/core/util.js' => '93cc50d6', 172 + 'rsrc/externals/javelin/core/util.js' => 'bdcfee9e', 173 173 'rsrc/externals/javelin/docs/Base.js' => '74676256', 174 174 'rsrc/externals/javelin/docs/onload.js' => 'e819c479', 175 175 'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a', ··· 186 186 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2', 187 187 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472', 188 188 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb', 189 - 'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b', 189 + 'rsrc/externals/javelin/ext/view/__tests__/View.js' => 'bda69c40', 190 190 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5', 191 191 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9', 192 - 'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03', 193 - 'rsrc/externals/javelin/lib/DOM.js' => 'c8fd8db2', 192 + 'rsrc/externals/javelin/lib/Cookie.js' => '6b3dcf44', 193 + 'rsrc/externals/javelin/lib/DOM.js' => 'c4569c05', 194 194 'rsrc/externals/javelin/lib/History.js' => 'c60f4327', 195 195 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', 196 196 'rsrc/externals/javelin/lib/Leader.js' => '9330f91b', ··· 215 215 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd', 216 216 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187', 217 217 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', 218 - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce', 218 + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'e3b841c8', 219 219 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '316b8fa1', 220 220 'rsrc/externals/raphael/g.raphael.js' => '40dde778', 221 221 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', ··· 362 362 'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d', 363 363 'rsrc/js/application/differential/behavior-comment-preview.js' => '6932def3', 364 364 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 365 - 'rsrc/js/application/differential/behavior-dropdown-menus.js' => 'e33d4bc5', 365 + 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '3bc14668', 366 366 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '00861799', 367 367 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492', 368 368 'rsrc/js/application/differential/behavior-populate.js' => 'bdb3e4d0', ··· 410 410 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 411 411 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 412 412 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 413 - 'rsrc/js/application/releeph/releeph-request-state-change.js' => '3a1a4060', 413 + 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'ab836011', 414 414 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', 415 415 'rsrc/js/application/repository/repository-crossreference.js' => 'f9539603', 416 416 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', ··· 444 444 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 445 445 'rsrc/js/core/TextAreaUtils.js' => '5c93c52c', 446 446 'rsrc/js/core/Title.js' => '5c1c758c', 447 - 'rsrc/js/core/ToolTip.js' => '1d298e3a', 447 + 'rsrc/js/core/ToolTip.js' => '031d4411', 448 448 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 449 449 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 450 450 'rsrc/js/core/behavior-autofocus.js' => '7319e029', ··· 564 564 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 565 565 'javelin-behavior-differential-comment-jump' => '4fdb476d', 566 566 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', 567 - 'javelin-behavior-differential-dropdown-menus' => 'e33d4bc5', 567 + 'javelin-behavior-differential-dropdown-menus' => '3bc14668', 568 568 'javelin-behavior-differential-edit-inline-comments' => '00861799', 569 569 'javelin-behavior-differential-feedback-preview' => '6932def3', 570 570 'javelin-behavior-differential-keyboard-navigation' => '2c426492', ··· 635 635 'javelin-behavior-project-create' => '065227cc', 636 636 'javelin-behavior-refresh-csrf' => '7814b593', 637 637 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 638 - 'javelin-behavior-releeph-request-state-change' => '3a1a4060', 638 + 'javelin-behavior-releeph-request-state-change' => 'ab836011', 639 639 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 640 640 'javelin-behavior-remarkup-preview' => 'f7379f45', 641 641 'javelin-behavior-reorder-applications' => '76b9fc3e', ··· 650 650 'javelin-behavior-view-placeholder' => '47830651', 651 651 'javelin-behavior-workflow' => '0a3f3021', 652 652 'javelin-color' => '7e41274a', 653 - 'javelin-cookie' => '62dfea03', 653 + 'javelin-cookie' => '6b3dcf44', 654 654 'javelin-diffusion-locate-file-source' => 'b42eddc7', 655 - 'javelin-dom' => 'c8fd8db2', 655 + 'javelin-dom' => 'c4569c05', 656 656 'javelin-dynval' => 'f6555212', 657 657 'javelin-event' => '85ea0626', 658 658 'javelin-fx' => '54b612ba', ··· 660 660 'javelin-install' => '05270951', 661 661 'javelin-json' => '69adf288', 662 662 'javelin-leader' => '9330f91b', 663 - 'javelin-magical-init' => '8c4e8f8b', 663 + 'javelin-magical-init' => '76e1fd61', 664 664 'javelin-mask' => '8a41885b', 665 665 'javelin-reactor' => '2b8de964', 666 666 'javelin-reactor-dom' => 'c90a04fc', ··· 677 677 'javelin-typeahead-normalizer' => '6f7a9da8', 678 678 'javelin-typeahead-ondemand-source' => '8b3fd187', 679 679 'javelin-typeahead-preloaded-source' => '54f314a0', 680 - 'javelin-typeahead-source' => '2818f5ce', 680 + 'javelin-typeahead-source' => 'e3b841c8', 681 681 'javelin-typeahead-static-source' => '316b8fa1', 682 682 'javelin-uri' => '6eff08aa', 683 - 'javelin-util' => '93cc50d6', 683 + 'javelin-util' => 'bdcfee9e', 684 684 'javelin-vector' => 'cc1bd0b0', 685 685 'javelin-view' => '0f764c35', 686 686 'javelin-view-html' => 'fe287620', ··· 738 738 'phabricator-standard-page-view' => '2c96cfb5', 739 739 'phabricator-textareautils' => '5c93c52c', 740 740 'phabricator-title' => '5c1c758c', 741 - 'phabricator-tooltip' => '1d298e3a', 741 + 'phabricator-tooltip' => '031d4411', 742 742 'phabricator-transaction-view-css' => '5d0cae25', 743 743 'phabricator-ui-example-css' => '528b19de', 744 744 'phabricator-uiexample-javelin-view' => 'd4a14807', ··· 834 834 '029a133d' => array( 835 835 'aphront-dialog-view-css', 836 836 ), 837 + '031d4411' => array( 838 + 'javelin-install', 839 + 'javelin-util', 840 + 'javelin-dom', 841 + 'javelin-vector', 842 + ), 837 843 '03d6ed07' => array( 838 844 'javelin-behavior', 839 845 'javelin-stratcom', ··· 938 944 'javelin-util', 939 945 'phabricator-keyboard-shortcut-manager', 940 946 ), 941 - '1d298e3a' => array( 942 - 'javelin-install', 943 - 'javelin-util', 944 - 'javelin-dom', 945 - 'javelin-vector', 946 - ), 947 947 '1def2711' => array( 948 948 'javelin-install', 949 949 'javelin-dom', ··· 955 955 'javelin-json', 956 956 'javelin-workflow', 957 957 'javelin-util', 958 - ), 959 - '2818f5ce' => array( 960 - 'javelin-install', 961 - 'javelin-util', 962 - 'javelin-dom', 963 - 'javelin-typeahead-normalizer', 964 958 ), 965 959 '2926fff2' => array( 966 960 'javelin-behavior', ··· 1029 1023 'javelin-json', 1030 1024 'phabricator-prefab', 1031 1025 ), 1032 - '3a1a4060' => array( 1033 - 'javelin-behavior', 1034 - 'javelin-dom', 1035 - 'javelin-stratcom', 1036 - 'javelin-workflow', 1037 - 'javelin-util', 1038 - 'phabricator-keyboard-shortcut', 1039 - ), 1040 1026 '3ab51e2c' => array( 1041 1027 'javelin-behavior', 1042 1028 'javelin-behavior-device', ··· 1044 1030 'javelin-vector', 1045 1031 'javelin-dom', 1046 1032 'javelin-magical-init', 1033 + ), 1034 + '3bc14668' => array( 1035 + 'javelin-behavior', 1036 + 'javelin-dom', 1037 + 'javelin-util', 1038 + 'javelin-stratcom', 1039 + 'javelin-workflow', 1040 + 'phuix-dropdown-menu', 1041 + 'phuix-action-list-view', 1042 + 'phuix-action-view', 1043 + 'phabricator-phtize', 1044 + 'changeset-view-manager', 1047 1045 ), 1048 1046 '3d51a746' => array( 1049 1047 'javelin-behavior', ··· 1228 1226 'javelin-magical-init', 1229 1227 'javelin-util', 1230 1228 ), 1231 - '62dfea03' => array( 1232 - 'javelin-install', 1233 - 'javelin-util', 1234 - ), 1235 1229 '6453c869' => array( 1236 1230 'javelin-install', 1237 1231 'javelin-dom', ··· 1250 1244 ), 1251 1245 '69adf288' => array( 1252 1246 'javelin-install', 1247 + ), 1248 + '6b3dcf44' => array( 1249 + 'javelin-install', 1250 + 'javelin-util', 1253 1251 ), 1254 1252 '6c2b09a2' => array( 1255 1253 'javelin-install', ··· 1562 1560 'javelin-util', 1563 1561 'phabricator-prefab', 1564 1562 ), 1563 + 'ab836011' => array( 1564 + 'javelin-behavior', 1565 + 'javelin-dom', 1566 + 'javelin-stratcom', 1567 + 'javelin-workflow', 1568 + 'javelin-util', 1569 + 'phabricator-keyboard-shortcut', 1570 + ), 1565 1571 'ad7a69ca' => array( 1566 1572 'javelin-install', 1567 1573 'javelin-util', ··· 1650 1656 'javelin-util', 1651 1657 'phabricator-shaped-request', 1652 1658 ), 1659 + 'c4569c05' => array( 1660 + 'javelin-magical-init', 1661 + 'javelin-install', 1662 + 'javelin-util', 1663 + 'javelin-vector', 1664 + 'javelin-stratcom', 1665 + ), 1653 1666 'c51ae228' => array( 1654 1667 'javelin-behavior', 1655 1668 'javelin-util', ··· 1662 1675 'javelin-install', 1663 1676 'javelin-uri', 1664 1677 'javelin-util', 1665 - ), 1666 - 'c8fd8db2' => array( 1667 - 'javelin-magical-init', 1668 - 'javelin-install', 1669 - 'javelin-util', 1670 - 'javelin-vector', 1671 - 'javelin-stratcom', 1672 1678 ), 1673 1679 'c90a04fc' => array( 1674 1680 'javelin-dom', ··· 1788 1794 'javelin-workflow', 1789 1795 'javelin-vector', 1790 1796 ), 1791 - 'e33d4bc5' => array( 1792 - 'javelin-behavior', 1793 - 'javelin-dom', 1794 - 'javelin-util', 1795 - 'javelin-stratcom', 1796 - 'javelin-workflow', 1797 - 'phuix-dropdown-menu', 1798 - 'phuix-action-list-view', 1799 - 'phuix-action-view', 1800 - 'phabricator-phtize', 1801 - 'changeset-view-manager', 1802 - ), 1803 1797 'e379b58e' => array( 1804 1798 'javelin-behavior', 1805 1799 'javelin-stratcom', 1806 1800 'javelin-vector', 1807 1801 'javelin-dom', 1808 1802 'javelin-uri', 1803 + ), 1804 + 'e3b841c8' => array( 1805 + 'javelin-install', 1806 + 'javelin-util', 1807 + 'javelin-dom', 1808 + 'javelin-typeahead-normalizer', 1809 1809 ), 1810 1810 'e4cc26b3' => array( 1811 1811 'javelin-behavior',
+2
src/__phutil_library_map__.php
··· 1596 1596 'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php', 1597 1597 'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php', 1598 1598 'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php', 1599 + 'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php', 1599 1600 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 1600 1601 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', 1601 1602 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', ··· 4801 4802 'PhabricatorDaemonTasksTableView' => 'AphrontView', 4802 4803 'PhabricatorDaemonsApplication' => 'PhabricatorApplication', 4803 4804 'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck', 4805 + 'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock', 4804 4806 'PhabricatorDashboard' => array( 4805 4807 'PhabricatorDashboardDAO', 4806 4808 'PhabricatorApplicationTransactionInterface',
+57
src/infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php
··· 1 + <?php 2 + 3 + /** 4 + * Triggers a daily routine, like server backups. 5 + * 6 + * This clock triggers events every 24 hours, using UTC. It does not use a 7 + * locale, and is intended for technical processes like backing up a server 8 + * every night. 9 + * 10 + * Because UTC does not have daylight savings, the local hour when this event 11 + * occurs will change over the course of the year. For example, from the 12 + * perspective of a user in California, it might run backups at 3AM in the 13 + * winter and 2AM in the summer. This is desirable for maintenance processes, 14 + * but problematic for some human processes. Use a different clock if you're 15 + * triggering a human-oriented event. 16 + * 17 + * The clock uses the time of day of the `start` epoch to calculate the time 18 + * of day of the next event, so you can change the time of day when the event 19 + * occurs by adjusting the `start` time of day. 20 + */ 21 + final class PhabricatorDailyRoutineTriggerClock 22 + extends PhabricatorTriggerClock { 23 + 24 + public function validateProperties(array $properties) { 25 + PhutilTypeSpec::checkMap( 26 + $properties, 27 + array( 28 + 'start' => 'int', 29 + )); 30 + } 31 + 32 + public function getNextEventEpoch($last_epoch, $is_reschedule) { 33 + $start_epoch = $this->getProperty('start'); 34 + if (!$last_epoch) { 35 + $last_epoch = $start_epoch; 36 + } 37 + 38 + $start = new DateTime('@'.$start_epoch); 39 + $last = new DateTime('@'.$last_epoch); 40 + 41 + // NOTE: We're choosing the date from the last event, but the time of day 42 + // from the start event. This allows callers to change when the event 43 + // occurs by updating the trigger's start parameter. 44 + $ymd = $last->format('Y-m-d'); 45 + $hms = $start->format('G:i:s'); 46 + 47 + $next = new DateTime("{$ymd} {$hms} UTC"); 48 + 49 + // Add a day. 50 + // NOTE: DateInterval doesn't exist until PHP 5.3.0, and we currently 51 + // target PHP 5.2.3. 52 + $next->modify('+1 day'); 53 + 54 + return (int)$next->format('U'); 55 + } 56 + 57 + }
+97 -3
src/infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php
··· 30 30 pht('Should never trigger.')); 31 31 } 32 32 33 + public function testDailyRoutineTriggerClockDaylightSavings() { 34 + // These dates are selected to cross daylight savings in PST; they should 35 + // be unaffected. 36 + $start = strtotime('2015-03-05 16:17:18 UTC'); 37 + 38 + $clock = new PhabricatorDailyRoutineTriggerClock( 39 + array( 40 + 'start' => $start, 41 + )); 42 + 43 + $expect_list = array( 44 + '2015-03-06 16:17:18', 45 + '2015-03-07 16:17:18', 46 + '2015-03-08 16:17:18', 47 + '2015-03-09 16:17:18', 48 + '2015-03-10 16:17:18', 49 + ); 50 + 51 + $this->expectClock($clock, $expect_list, pht('Daily Routine (PST)')); 52 + } 53 + 54 + public function testDailyRoutineTriggerClockLeapSecond() { 55 + // These dates cross the leap second on June 30, 2012. There has never 56 + // been a negative leap second, so we can't test that yet. 57 + $start = strtotime('2012-06-28 23:59:59 UTC'); 58 + 59 + $clock = new PhabricatorDailyRoutineTriggerClock( 60 + array( 61 + 'start' => $start, 62 + )); 63 + 64 + $expect_list = array( 65 + '2012-06-29 23:59:59', 66 + '2012-06-30 23:59:59', 67 + '2012-07-01 23:59:59', 68 + '2012-07-02 23:59:59', 69 + ); 70 + 71 + $this->expectClock($clock, $expect_list, pht('Daily Routine (Leap)')); 72 + } 73 + 74 + 75 + public function testCDailyRoutineTriggerClockAdjustTimeOfDay() { 76 + // In this case, we're going to update the time of day on the clock and 77 + // make sure it keeps track of the date but adjusts the time. 78 + $start = strtotime('2015-01-15 6:07:08 UTC'); 79 + 80 + $clock = new PhabricatorDailyRoutineTriggerClock( 81 + array( 82 + 'start' => $start, 83 + )); 84 + 85 + $expect_list = array( 86 + '2015-01-16 6:07:08', 87 + '2015-01-17 6:07:08', 88 + '2015-01-18 6:07:08', 89 + ); 90 + 91 + $last_epoch = $this->expectClock( 92 + $clock, 93 + $expect_list, 94 + pht('Daily Routine (Pre-Adjust)')); 95 + 96 + // Now, change the time of day. 97 + $new_start = strtotime('2015-01-08 1:23:45 UTC'); 98 + 99 + $clock = new PhabricatorDailyRoutineTriggerClock( 100 + array( 101 + 'start' => $new_start, 102 + )); 103 + 104 + $expect_list = array( 105 + '2015-01-19 1:23:45', 106 + '2015-01-20 1:23:45', 107 + '2015-01-21 1:23:45', 108 + ); 109 + 110 + $this->expectClock( 111 + $clock, 112 + $expect_list, 113 + pht('Daily Routine (Post-Adjust)'), 114 + $last_epoch); 115 + } 116 + 33 117 public function testSubscriptionTriggerClock() { 34 118 $start = strtotime('2014-01-31 2:34:56 UTC'); 35 119 ··· 76 160 '2016-03-31 2:34:56', 77 161 ); 78 162 79 - $last_epoch = null; 163 + $this->expectClock($clock, $expect_list, pht('Billing Cycle')); 164 + } 165 + 166 + private function expectClock( 167 + PhabricatorTriggerClock $clock, 168 + array $expect_list, 169 + $clock_name, 170 + $last_epoch = null) { 171 + 80 172 foreach ($expect_list as $cycle => $expect) { 81 173 $next_epoch = $clock->getNextEventEpoch( 82 174 $last_epoch, ··· 84 176 85 177 $this->assertEqual( 86 178 $expect, 87 - id(new DateTime('@'.$next_epoch))->format('Y-m-d g:i:s'), 88 - pht('Billing cycle %s.', $cycle)); 179 + id(new DateTime('@'.$next_epoch))->format('Y-m-d G:i:s'), 180 + pht('%s (%s)', $clock_name, $cycle)); 89 181 90 182 $last_epoch = $next_epoch; 91 183 } 184 + 185 + return $last_epoch; 92 186 } 93 187 94 188 }