added on WebSTLs (webstls.com) at 2026-07-01 22:27:11
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # WebSTLs -- hourly storage worker. | |
| 4 | # | |
| 5 | # Three jobs per pass: | |
| 6 | # 1. Soft-alert sweep -- find users at or above | |
| 7 | # platform_settings.storage_soft_alert_pct (default 80) whose | |
| 8 | # last_storage_alert_at is NULL or > 7 days ago. Stamps | |
| 9 | # last_storage_alert_at NOW() and writes a per-user note row in | |
| 10 | # email_sends_log so the email-blast worker (if present) can | |
| 11 | # pick them up. | |
| 12 | # | |
| 13 | # 2. Drift recompute -- for users whose storage_used_at_recompute | |
| 14 | # is NULL or > 24 hours ago, run Storage::recompute_usage to | |
| 15 | # reconcile the denormalized counter against the authoritative | |
| 16 | # model_files sum. | |
| 17 | # | |
| 18 | # 3. Stats line at the end summarizes counts for the cron log. | |
| 19 | # | |
| 20 | # Cron suggestion (server crontab for the vhost user): | |
| 21 | # 17 * * * * /usr/bin/perl \ | |
| 22 | # /var/www/vhosts/webstls.com/httpdocs/_storage_alert_worker.pl \ | |
| 23 | # >> /var/www/vhosts/webstls.com/httpdocs/_storage_alert_worker.log 2>&1 | |
| 24 | #====================================================================== | |
| 25 | use strict; | |
| 26 | use warnings; | |
| 27 | use lib '/var/www/vhosts/webstls.com/httpdocs'; | |
| 28 | ||
| 29 | use MODS::DBConnect; | |
| 30 | use MODS::WebSTLs::Config; | |
| 31 | use MODS::WebSTLs::Storage; | |
| 32 | use MODS::WebSTLs::Mailer; | |
| 33 | ||
| 34 | my $RECOMPUTE_AGE_HOURS = 24; | |
| 35 | my $ALERT_REPEAT_DAYS = 7; | |
| 36 | ||
| 37 | my $cfg = MODS::WebSTLs::Config->new; | |
| 38 | my $db = MODS::DBConnect->new; | |
| 39 | my $DB = $cfg->settings('database_name'); | |
| 40 | my $dbh = $db->db_connect(); | |
| 41 | die "could not connect to $DB\n" unless $dbh; | |
| 42 | ||
| 43 | my $started = scalar localtime; | |
| 44 | my $storage = MODS::WebSTLs::Storage->new; | |
| 45 | ||
| 46 | # ---- 1) Drift recompute ---------------------------------------------- | |
| 47 | # Pick every user who hasn't been recomputed in 24 hours. Each call to | |
| 48 | # recompute_usage runs one COUNT/SUM on model_files so the workload is | |
| 49 | # bounded -- even at 10k users that's well under 5 minutes / pass. | |
| 50 | my @stale = $db->db_readwrite_multiple($dbh, qq~ | |
| 51 | SELECT id FROM `${DB}`.users | |
| 52 | WHERE storage_used_at_recompute IS NULL | |
| 53 | OR storage_used_at_recompute < DATE_SUB(NOW(), INTERVAL $RECOMPUTE_AGE_HOURS HOUR) | |
| 54 | ORDER BY storage_used_at_recompute IS NULL DESC, id ASC | |
| 55 | LIMIT 5000 | |
| 56 | ~, $0, __LINE__); | |
| 57 | ||
| 58 | my $recomputed = 0; | |
| 59 | foreach my $u (@stale) { | |
| 60 | $storage->recompute_usage($db, $dbh, $DB, $u->{id}); | |
| 61 | $recomputed++; | |
| 62 | } | |
| 63 | ||
| 64 | # ---- 2) Soft-alert sweep --------------------------------------------- | |
| 65 | my $needs_alert = $storage->users_needing_soft_alert($db, $dbh, $DB); | |
| 66 | my $alerted = 0; | |
| 67 | my $send_failed = 0; | |
| 68 | my $mailer = MODS::WebSTLs::Mailer->new; | |
| 69 | ||
| 70 | # Pre-compute next-tier upgrade label per tier slug so vars come out | |
| 71 | # right per recipient. | |
| 72 | my %upgrade = (free => 'Starter', starter => 'Pro', pro => 'Studio', studio => 'Studio'); | |
| 73 | ||
| 74 | foreach my $row (@$needs_alert) { | |
| 75 | my $uid = $row->{user_id}; | |
| 76 | my $pct = $row->{pct}; | |
| 77 | my $used_b = $row->{used_bytes}; | |
| 78 | my $used_h = MODS::WebSTLs::Storage::human_size($used_b); | |
| 79 | ||
| 80 | # Pull cap + tier for the template vars. | |
| 81 | my $L = $storage->limits_for_user($db, $dbh, $DB, $uid); | |
| 82 | my $cap_h = MODS::WebSTLs::Storage::human_size($L->{hard_cap_bytes}); | |
| 83 | my $upgrade_to = $upgrade{ $L->{tier} } || 'Studio'; | |
| 84 | ||
| 85 | # Send the email via Mailer::send_template -- pulls the | |
| 86 | # storage_soft_alert row from email_templates, fills vars, | |
| 87 | # appends the unsubscribe footer, respects user preferences, | |
| 88 | # logs to email_sends_log. Returns 1 on send, 0 on suppressed/fail. | |
| 89 | my $ok = eval { $mailer->send_template( | |
| 90 | slug => 'storage_soft_alert', | |
| 91 | user_id => $uid, | |
| 92 | vars => { | |
| 93 | pct => $pct, | |
| 94 | used => $used_h, | |
| 95 | cap => $cap_h, | |
| 96 | upgrade_to => $upgrade_to, | |
| 97 | }, | |
| 98 | ) }; | |
| 99 | if ($ok) { | |
| 100 | # Only stamp last_storage_alert_at when we actually SENT (or | |
| 101 | # the template suppressed it via prefs -- either way we won't | |
| 102 | # re-process). Failures stay flagged for the next pass. | |
| 103 | $db->db_readwrite($dbh, qq~ | |
| 104 | UPDATE `${DB}`.users | |
| 105 | SET last_storage_alert_at = NOW() | |
| 106 | WHERE id='$uid' | |
| 107 | ~, $0, __LINE__); | |
| 108 | $alerted++; | |
| 109 | } else { | |
| 110 | warn "[storage worker] send_template failed for uid=$uid\n"; | |
| 111 | $send_failed++; | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | $db->db_disconnect($dbh); | |
| 116 | ||
| 117 | print "[$started] storage worker: recomputed=$recomputed soft_alerts_sent=$alerted send_failed=$send_failed\n"; | |
| 118 | exit 0; |