@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.

Implement an "only if the rule did not match last time" policy for Herald rules

Summary: Depends on D18927. Ref T13048. This implements a new policy which allows Herald rules to fire on some kinds of state changes.

Test Plan:
Wrote and tested rules with the new policy:

{F5394971}

{F5394972}

Also wrote and tested rules with the old policies:

{F5394973}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13048

Differential Revision: https://secure.phabricator.com/D18930

+102 -24
+9 -5
src/applications/herald/adapter/HeraldAdapter.php
··· 774 774 775 775 if (!$this->isSingleEventAdapter()) { 776 776 $options[] = HeraldRule::REPEAT_FIRST; 777 + $options[] = HeraldRule::REPEAT_CHANGE; 777 778 } 778 779 779 780 return $options; ··· 897 898 )); 898 899 } 899 900 900 - if ($rule->isRepeatEvery()) { 901 - $action_text = 902 - pht('Take these actions every time this rule matches:'); 901 + if ($rule->isRepeatFirst()) { 902 + $action_text = pht( 903 + 'Take these actions the first time this rule matches:'); 904 + } else if ($rule->isRepeatOnChange()) { 905 + $action_text = pht( 906 + 'Take these actions if this rule did not match the last time:'); 903 907 } else { 904 - $action_text = 905 - pht('Take these actions the first time this rule matches:'); 908 + $action_text = pht( 909 + 'Take these actions every time this rule matches:'); 906 910 } 907 911 908 912 $action_title = phutil_tag(
+1 -1
src/applications/herald/controller/HeraldRuleController.php
··· 218 218 ), 219 219 pht('New Action'))) 220 220 ->setDescription(pht( 221 - 'Take these actions %s this rule matches:', 221 + 'Take these actions %s', 222 222 $repetition_selector)) 223 223 ->setContent(javelin_tag( 224 224 'table',
+82 -16
src/applications/herald/engine/HeraldEngine.php
··· 14 14 15 15 private $forbiddenFields = array(); 16 16 private $forbiddenActions = array(); 17 + private $skipEffects = array(); 17 18 18 19 public function setDryRun($dry_run) { 19 20 $this->dryRun = $dry_run; ··· 171 172 return; 172 173 } 173 174 174 - $rules = mpull($rules, null, 'getID'); 175 - $applied_ids = array(); 175 + // Update the "applied" state table. How this table works depends on the 176 + // repetition policy for the rule. 177 + // 178 + // REPEAT_EVERY: We delete existing rows for the rule, then write nothing. 179 + // This policy doesn't use any state. 180 + // 181 + // REPEAT_FIRST: We keep existing rows, then write additional rows for 182 + // rules which fired. This policy accumulates state over the life of the 183 + // object. 184 + // 185 + // REPEAT_CHANGE: We delete existing rows, then write all the rows which 186 + // matched. This policy only uses the state from the previous run. 176 187 177 - // Mark all the rules that have had their effects applied as having been 178 - // executed for the current object. 188 + $rules = mpull($rules, null, 'getID'); 179 189 $rule_ids = mpull($xscripts, 'getRuleID'); 180 190 191 + $delete_ids = array(); 192 + foreach ($rules as $rule_id => $rule) { 193 + if ($rule->isRepeatFirst()) { 194 + continue; 195 + } 196 + $delete_ids[] = $rule_id; 197 + } 198 + 199 + $applied_ids = array(); 181 200 foreach ($rule_ids as $rule_id) { 182 201 if (!$rule_id) { 183 202 // Some apply transcripts are purely informational and not associated ··· 190 209 continue; 191 210 } 192 211 193 - if ($rule->isRepeatFirst()) { 212 + if ($rule->isRepeatFirst() || $rule->isRepeatOnChange()) { 194 213 $applied_ids[] = $rule_id; 195 214 } 196 215 } 197 216 198 - if ($applied_ids) { 217 + // Also include "only if this rule did not match the last time" rules 218 + // which matched but were skipped in the "applied" list. 219 + foreach ($this->skipEffects as $rule_id => $ignored) { 220 + $applied_ids[] = $rule_id; 221 + } 222 + 223 + if ($delete_ids || $applied_ids) { 199 224 $conn_w = id(new HeraldRule())->establishConnection('w'); 200 - $sql = array(); 201 - foreach ($applied_ids as $id) { 202 - $sql[] = qsprintf( 225 + 226 + if ($delete_ids) { 227 + queryfx( 203 228 $conn_w, 204 - '(%s, %d)', 229 + 'DELETE FROM %T WHERE phid = %s AND ruleID IN (%Ld)', 230 + HeraldRule::TABLE_RULE_APPLIED, 205 231 $adapter->getPHID(), 206 - $id); 232 + $delete_ids); 207 233 } 208 - queryfx( 209 - $conn_w, 210 - 'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q', 211 - HeraldRule::TABLE_RULE_APPLIED, 212 - implode(', ', $sql)); 234 + 235 + if ($applied_ids) { 236 + $sql = array(); 237 + foreach ($applied_ids as $id) { 238 + $sql[] = qsprintf( 239 + $conn_w, 240 + '(%s, %d)', 241 + $adapter->getPHID(), 242 + $id); 243 + } 244 + queryfx( 245 + $conn_w, 246 + 'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q', 247 + HeraldRule::TABLE_RULE_APPLIED, 248 + implode(', ', $sql)); 249 + } 213 250 } 214 251 } 215 252 ··· 311 348 } 312 349 } 313 350 351 + // If this rule matched, and is set to run "if it did not match the last 352 + // time", and we matched the last time, we're going to return a match in 353 + // the transcript but set a flag so we don't actually apply any effects. 354 + 355 + // We need the rule to match so that storage gets updated properly. If we 356 + // just pretend the rule didn't match it won't cause any effects (which 357 + // is correct), but it also won't set the "it matched" flag in storage, 358 + // so the next run after this one would incorrectly trigger again. 359 + 360 + $is_dry_run = $this->getDryRun(); 361 + if ($result && !$is_dry_run) { 362 + $is_on_change = $rule->isRepeatOnChange(); 363 + if ($is_on_change) { 364 + $did_apply = $rule->getRuleApplied($object->getPHID()); 365 + if ($did_apply) { 366 + $reason = pht( 367 + 'This rule matched, but did not take any actions because it '. 368 + 'is configured to act only if it did not match the last time.'); 369 + 370 + $this->skipEffects[$rule->getID()] = true; 371 + } 372 + } 373 + } 374 + 314 375 $this->newRuleTranscript($rule) 315 376 ->setResult($result) 316 377 ->setReason($reason); ··· 362 423 protected function getRuleEffects( 363 424 HeraldRule $rule, 364 425 HeraldAdapter $object) { 426 + 427 + $rule_id = $rule->getID(); 428 + if (isset($this->skipEffects[$rule_id])) { 429 + return array(); 430 + } 365 431 366 432 $effects = array(); 367 433 foreach ($rule->getActions() as $action) {
+10 -2
src/applications/herald/storage/HeraldRule.php
··· 32 32 33 33 const REPEAT_EVERY = 'every'; 34 34 const REPEAT_FIRST = 'first'; 35 + const REPEAT_CHANGE = 'change'; 35 36 36 37 protected function getConfiguration() { 37 38 return array( ··· 282 283 return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST); 283 284 } 284 285 286 + public function isRepeatOnChange() { 287 + return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE); 288 + } 289 + 285 290 public static function getRepetitionPolicySelectOptionMap() { 286 291 $map = self::getRepetitionPolicyMap(); 287 292 return ipull($map, 'select'); ··· 290 295 private static function getRepetitionPolicyMap() { 291 296 return array( 292 297 self::REPEAT_EVERY => array( 293 - 'select' => pht('every time'), 298 + 'select' => pht('every time this rule matches:'), 294 299 ), 295 300 self::REPEAT_FIRST => array( 296 - 'select' => pht('only the first time'), 301 + 'select' => pht('only the first time this rule matches:'), 302 + ), 303 + self::REPEAT_CHANGE => array( 304 + 'select' => pht('if this rule did not match the last time:'), 297 305 ), 298 306 ); 299 307 }