From b914dee8eea253d894ef91f7655b15ad09a02bdb Mon Sep 17 00:00:00 2001 From: Giovanni Date: Tue, 21 Jul 2026 16:11:58 +0200 Subject: [PATCH 1/2] Replace meta-rule full-rescan retry with a dependency-driven work queue do_meta_tests() restarted its entire foreach over meta_check_ready (or meta_pending when finishing) via "goto RULE" every time any meta resolved, re-checking the dependency list of every still-pending meta each time, even ones unaffected by what just resolved. Replace it with an explicit work queue seeded the same way, where resolving a meta pushes only the specific metas listed in its meta_deprules reverse-map (the same map PerMsgStatus::rule_ready() already uses) instead of rescanning everything. Co-Authored-By: Claude Sonnet 5 --- lib/Mail/SpamAssassin/Plugin/Check.pm | 37 +++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/Mail/SpamAssassin/Plugin/Check.pm b/lib/Mail/SpamAssassin/Plugin/Check.pm index 58c160d524..6b7b00da58 100644 --- a/lib/Mail/SpamAssassin/Plugin/Check.pm +++ b/lib/Mail/SpamAssassin/Plugin/Check.pm @@ -298,9 +298,9 @@ sub do_meta_tests { my $mr = $pms->{meta_check_ready}; my $mp = $pms->{meta_pending}; my $md = $pms->{conf}->{meta_dependencies}; + my $mdr = $pms->{conf}->{meta_deprules}; my $mt = $pms->{conf}->{meta_tests}; my $h = $pms->{tests_already_hit}; - my $retry; # When finishing, first mark all unrun non-meta rules as finished, # it will enable the next loop to finish everything properly @@ -314,14 +314,31 @@ sub do_meta_tests { } } -RULE: - foreach my $rulename ($finish ? keys %$mp : keys %$mr) { + # Work queue of metas worth (re)checking, seeded from meta_check_ready + # (or all pending metas when finishing). Running a meta can make other + # metas newly ready (a meta depending on it); rather than rescanning + # every pending/ready meta again on each such change (as a "goto RULE" + # restart would), push only the specific metas that depend on whatever + # just resolved, via the same meta_deprules reverse-dependency map + # PerMsgStatus::rule_ready() uses. + my @queue = $finish ? keys %$mp : keys %$mr; + my %queued; @queued{@queue} = (); + + while (@queue) { + my $rulename = shift @queue; + delete $queued{$rulename}; + next if exists $h->{$rulename}; # already resolved + # Meta is not ready if some dependency has not run yet + my $ready = 1; foreach my $deprule (@{$md->{$rulename}||[]}) { if (!exists $h->{$deprule}) { - next RULE; + $ready = 0; + last; } } + next if !$ready; + # Metasubs look like ($_[1]->{$rulename}||0) ... my $result = $mt->{$rulename}->($pms, $h); if ($result) { @@ -333,11 +350,15 @@ RULE: } delete $mr->{$rulename}; delete $mp->{$rulename}; - # Reiterate all metas again, in case some meta depended on us - $retry = 1; - } - goto RULE if $retry--; + # $rulename just resolved (in %$h); queue only the metas that + # specifically depend on it, instead of every pending/ready meta. + foreach my $deprule (keys %{$mdr->{$rulename} || {}}) { + next if $queued{$deprule}; + push @queue, $deprule; + $queued{$deprule} = 1; + } + } delete $pms->{meta_check_ready}; } From cb3183365e95a9f70b796941ab989ad533aa9baa Mon Sep 17 00:00:00 2001 From: Giovanni Date: Wed, 22 Jul 2026 15:32:17 +0200 Subject: [PATCH 2/2] initialize queue --- lib/Mail/SpamAssassin/Plugin/Check.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Mail/SpamAssassin/Plugin/Check.pm b/lib/Mail/SpamAssassin/Plugin/Check.pm index 6b7b00da58..65812396c6 100644 --- a/lib/Mail/SpamAssassin/Plugin/Check.pm +++ b/lib/Mail/SpamAssassin/Plugin/Check.pm @@ -322,7 +322,7 @@ sub do_meta_tests { # just resolved, via the same meta_deprules reverse-dependency map # PerMsgStatus::rule_ready() uses. my @queue = $finish ? keys %$mp : keys %$mr; - my %queued; @queued{@queue} = (); + my %queued; @queued{@queue} = (1) x @queue; while (@queue) { my $rulename = shift @queue;