svn commit: r1935526 - spamassassin/trunk/lib/Mail/SpamAssassin/Plugin

[email protected] Fri, 19 Jun 2026 15:26:50 -0000
Newsgroups gmane.mail.spam.spamassassin.cvs
Message-ID <178188281046.3339186.1081271884627970838@svn03-he-fi>
Author: gbechis
Date: Fri Jun 19 15:26:50 2026
New Revision: 1935526

Log:
speedup SQL queries

Modified:
   spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm	Fri Jun 19 14:27:56 2026	(r1935525)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm	Fri Jun 19 15:26:50 2026	(r1935526)
@@ -44,7 +44,7 @@ use strict;
 use warnings;
 use re 'taint';
 
-my $VERSION = 0.11.1;
+my $VERSION = 0.11.2;
 
 use AI::FANN qw(:all);
 use Storable qw(store retrieve);
@@ -847,7 +847,8 @@ sub learn_message {
   # Use the cleared buffer returned by a successful retrain,
   # or fall back to the current in-memory buffer.
   my $tbuf_to_save = $tbuf_after_retrain // $vocab_for_balance{_tbuf};
-  $self->_save_meta($conf, $nn_data_dir, $new_counter, $tbuf_to_save);
+  $self->_save_meta($conf, $nn_data_dir, $new_counter, $tbuf_to_save,
+      defined($tbuf_after_retrain));
 
   my $model_saved = $self->_save_model_atomic(
     $network, $dataset_path, $lock1_mtime,
@@ -2418,7 +2419,8 @@ sub _load_meta {
 
 # Persists counter and training buffer to the appropriate backend store.
 sub _save_meta {
-  my ($self, $conf, $nn_data_dir, $counter, $tbuf) = @_;
+  my ($self, $conf, $nn_data_dir, $counter, $tbuf, $buffer_dirty) = @_;
+  $buffer_dirty //= 1;
   $tbuf ||= { spam => [], ham => [] };
   if (defined $conf->{neuralnetwork_dsn} && $self->{dbh}) {
     my $username = lc($self->{main}->{username});
@@ -2442,25 +2444,27 @@ sub _save_meta {
           undef, $username, 'learns_since_retrain', "$counter"
         );
       }
-      $self->{dbh}->do(
-        "DELETE FROM neural_training_buffer WHERE username=?", undef, $username
-      );
-      my $ins = $self->{dbh}->prepare(
-        "INSERT INTO neural_training_buffer (username, class, slot, ts, token, count) VALUES (?,?,?,?,?,?)"
-      );
-      for my $class (qw(spam ham)) {
-        my $slots = $tbuf->{$class} || [];
-        for my $i (0 .. $#$slots) {
-          my $entry = $slots->[$i];
-          my %counts;
-          $counts{$_}++ for @{ $entry->{tokens} || [] };
-          for my $tok (keys %counts) {
-            $ins->execute($username, $class, $i, $entry->{ts} // 0, $tok, $counts{$tok});
+      if ($buffer_dirty) {
+        $self->{dbh}->do(
+          "DELETE FROM neural_training_buffer WHERE username=?", undef, $username
+        );
+        my $ins = $self->{dbh}->prepare(
+          "INSERT INTO neural_training_buffer (username, class, slot, ts, token, count) VALUES (?,?,?,?,?,?)"
+        );
+        for my $class (qw(spam ham)) {
+          my $slots = $tbuf->{$class} || [];
+          for my $i (0 .. $#$slots) {
+            my $entry = $slots->[$i];
+            my %counts;
+            $counts{$_}++ for @{ $entry->{tokens} || [] };
+            for my $tok (keys %counts) {
+              $ins->execute($username, $class, $i, $entry->{ts} // 0, $tok, $counts{$tok});
+            }
           }
         }
       }
       $self->{dbh}->commit();
-      dbg("SQL metadata persisted: learns_since_retrain=$counter " .
+      dbg("SQL metadata persisted: learns_since_retrain=$counter buffer_dirty=$buffer_dirty " .
           "tbuf_spam=" . scalar(@{ $tbuf->{spam} || [] }) .
           " tbuf_ham=" . scalar(@{ $tbuf->{ham} || [] }));
       1;
@@ -2845,6 +2849,46 @@ sub _get_or_create_network {
 sub _push_to_training_buffer {
   my ($self, $conf, $nn_data_dir, $email_token_lists, $labels) = @_;
 
+  if (defined $conf->{neuralnetwork_dsn} && $self->{dbh}) {
+    my $username = lc($self->{main}->{username});
+    for my $i (0 .. $#$email_token_lists) {
+      next unless ref($email_token_lists->[$i]) eq 'ARRAY' && @{$email_token_lists->[$i]};
+      my $class = $labels->[$i] ? 'spam' : 'ham';
+      eval {
+        my ($next_slot) = $self->{dbh}->selectrow_array(
+          "SELECT COALESCE(MAX(slot)+1, 0) FROM neural_training_buffer WHERE username=? AND class=?",
+          undef, $username, $class
+        );
+        my $ts = time();
+        my %counts;
+        $counts{$_}++ for @{$email_token_lists->[$i]};
+        my $ins = $self->{dbh}->prepare(
+          "INSERT INTO neural_training_buffer (username, class, slot, ts, token, count) VALUES (?,?,?,?,?,?)"
+        );
+        $self->{dbh}->begin_work();
+        for my $tok (keys %counts) {
+          $ins->execute($username, $class, $next_slot, $ts, $tok, $counts{$tok});
+        }
+        $self->{dbh}->commit();
+        1;
+      } or do {
+        my $err = $@ || 'unknown';
+        eval { $self->{dbh}->rollback() if !$self->{dbh}{AutoCommit} };
+        dbg("Failed to append training slot to SQL buffer: $err");
+      };
+    }
+    my ($spam_slots) = $self->{dbh}->selectrow_array(
+      "SELECT COUNT(DISTINCT slot) FROM neural_training_buffer WHERE username=? AND class='spam'",
+      undef, $username
+    );
+    my ($ham_slots) = $self->{dbh}->selectrow_array(
+      "SELECT COUNT(DISTINCT slot) FROM neural_training_buffer WHERE username=? AND class='ham'",
+      undef, $username
+    );
+    my $needs_retrain = $self->_training_buffer_flush_needed($conf, {});
+    return ($spam_slots // 0, $ham_slots // 0, $needs_retrain);
+  }
+
   my $existing         = $self->_load_meta($conf);
   my $existing_buf     = $existing->{_tbuf};
   my $existing_counter = $existing->{_learns_since_retrain};