summaryrefslogtreecommitdiffstats
path: root/mailer.pl
diff options
context:
space:
mode:
Diffstat (limited to 'mailer.pl')
-rw-r--r--mailer.pl49
1 files changed, 42 insertions, 7 deletions
diff --git a/mailer.pl b/mailer.pl
index 41bd06e..20fc412 100644
--- a/mailer.pl
+++ b/mailer.pl
@@ -16,6 +16,8 @@ my $subject = 'Daily word';
my $sendmail = '/usr/sbin/sendmail';
my $syslog_tag = 'lex-mail';
+my $max_interval = 365; # Upper bound limit (1 year)
+
# Connect to /dev/log before pledge/unveil so no late 'unix'
# socket creation occurs
openlog($syslog_tag, 'ndelay,pid', LOG_MAIL);
@@ -98,12 +100,34 @@ foreach my $file (@current_files) {
}
}
-# Select candidate file (spaced repetition)
+# Select candidate file (spaced repetition + fair scheduling)
my @due_files = grep { $state{$_}{next_due} le $today } @current_files;
my $selected_file;
-if (@due_files) {
- $selected_file = $due_files[rand @due_files];
+if (@due_files == 1) {
+ $selected_file = $due_files[0];
+} elsif (@due_files > 1) {
+ # Weighted random choice: files with fewer reviews get higher probability weights
+ my %weights;
+ my $total_weight = 0;
+
+ foreach my $file (@due_files) {
+ # Inversely proportional weight (1 / (reviews + 1))
+ my $w = 1.0 / ($state{$file}{reviews} + 1);
+ $weights{$file} = $w;
+ $total_weight += $w;
+ }
+
+ my $rand_point = rand($total_weight);
+ my $accum = 0;
+ foreach my $file (@due_files) {
+ $accum += $weights{$file};
+ if ($rand_point <= $accum) {
+ $selected_file = $file;
+ last;
+ }
+ }
+ $selected_file //= $due_files[-1]; # Fallback
} else {
@current_files = sort { $state{$a}{next_due} cmp $state{$b}{next_due} } @current_files;
$selected_file = $current_files[0];
@@ -150,6 +174,11 @@ if ($item->{reviews} == 0) {
$interval = int($interval * $factor);
}
+# Cap interval
+if ($interval > $max_interval) {
+ $interval = $max_interval;
+}
+
$item->{interval} = $interval;
$item->{reviews} += 1;
@@ -161,15 +190,21 @@ foreach my $file (keys %state) {
delete $state{$file} unless $active_files{$file};
}
+# Atomic write using a temporary state file
+my $tmp_file = "$state_file.tmp";
my $out_fh;
-if (!open($out_fh, '>', $state_file)) {
- log_die("Cannot write to $state_file: $!");
+if (!open($out_fh, '>', $tmp_file)) {
+ log_die("Cannot write to $tmp_file: $!");
}
foreach my $file (keys %state) {
my $i = $state{$file};
print $out_fh join("\t", $file, $i->{next_due}, $i->{interval}, $i->{reviews}, $i->{factor}), "\n";
}
-close($out_fh);
+close($out_fh) or log_die("Error closing $tmp_file: $!");
+
+if (!rename($tmp_file, $state_file)) {
+ log_die("Cannot atomic swap $tmp_file to $state_file: $!");
+}
syslog(LOG_INFO, "Mailed %s to %s (next due: %s)", $selected_file, $to, $item->{next_due});
-closelog();
+closelog(); \ No newline at end of file