Plugin Directory

Changeset 3423968


Ignore:
Timestamp:
12/19/2025 09:02:46 PM (3 months ago)
Author:
manovermachine
Message:

Version 1.3.9: Critical timezone fix for watchdog alerts + time validation

CRITICAL FIX:

  • Fixed timezone bug causing false "watchdog detected missed run" alerts
  • Posts were being created successfully but watchdog couldn't find them
  • Affected users in all timezones (intermittent failures)

THE PROBLEM:

  • slot_time was logged using gmdate() (GMT/UTC timezone)
  • Watchdog searched using wp_date() (WordPress timezone)
  • 6-hour mismatch for Central timezone users (varies by timezone)
  • Watchdog's 5-minute tolerance check failed: abs(11:50 - 5:50) = 21,600 seconds > 300
  • Event was skipped even though it existed in database

WHY IT WAS INTERMITTENT:

  • Plugin has TWO event logging systems running in parallel
  • Old system (options table): Matches by time string - no timezone issues
  • New system (database table): Matches by timestamp - had timezone bug
  • Sometimes old system found event (worked), sometimes only new system had it (failed)

THE SOLUTION:

  • Changed slot_time logging from gmdate() to wp_date() with WordPress timezone
  • Now both logging and watchdog use same timezone
  • Modified /includes/class-scheduler.php line 173-175

BEFORE:

$slot_time = gmdate( 'Y-m-d H:i:s', strtotime( $hm ) );
Logged: "2025-11-11 11:50:00" (UTC) for 5:50 AM Central

AFTER:

$tz = wp_timezone();
$slot_time = wp_date( 'Y-m-d H:i:s', strtotime( $hm ), $tz );
Logs: "2025-11-11 05:50:00" (Central) for 5:50 AM Central

ADDITIONAL ENHANCEMENT:

  • Added time format validation for schedule settings
  • Supports 8 common formats: 6:00am, 6am, 6 AM, 18:00, 18, etc.
  • Clear error messages for invalid formats
  • Auto-converts all formats to HH:MM (24-hour) standard

VALIDATION EXAMPLES:

  • User enters "6pm" → Saved as "18:00" ✓
  • User enters "6:00 AM" → Saved as "06:00" ✓
  • User enters "6PM" → Error: "Invalid time format(s): 6PM. Please use formats like '6:00am', '6:00 AM', '18:00', or '6pm'" ✗

TECHNICAL CHANGES:

  • Modified /includes/class-scheduler.php: Fixed timezone in slot_time logging
  • Modified /includes/class-admin.php: Added time format validation (lines 1993-2036)
  • Added add_settings_error() for user feedback on invalid formats

BENEFITS:

  • ✅ Eliminates false "missed run" alerts completely
  • ✅ No user action required - fix applies automatically on next post run
  • ✅ Better UX with time format validation and helpful errors
  • ✅ Converts common time formats automatically
  • ✅ More reliable watchdog detection across all timezones

COMPATIBILITY:

  • Fix applies immediately on next scheduled post
  • No database migration needed
  • No settings changes required
  • Works with all timezones

USER IMPACT:

  • Users will stop receiving false watchdog alerts
  • Clear feedback when entering schedule times in invalid formats
  • More confidence in scheduled posting reliability
Location:
weather-write/trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • weather-write/trunk/includes/class-scheduler.php

    r3423169 r3423968  
    151151        // local wp-cron events when an external token is present.
    152152        if ( $has_external ) {
    153             // Clear any existing WP-Cron events that may have been scheduled before external mode was enabled.
    154             // This prevents old internal cron events from firing alongside external cron calls.
     153            // Clear any existing WP-Cron post generation events
    155154            wp_clear_scheduled_hook( self::CRON_HOOK );
    156             wp_clear_scheduled_hook( self::WATCHDOG_HOOK );
     155           
     156            // IMPORTANT: Still schedule watchdog for monitoring in external cron mode
     157            // The watchdog monitors for failures but doesn't trigger posts itself
     158            // It only sends alerts when posts fail or are missed
     159            if ( ! wp_next_scheduled( self::WATCHDOG_HOOK ) ) {
     160                $next = self::compute_next_run( $options );
     161                if ( $next['ts'] ) {
     162                    // Schedule watchdog 2 minutes after next expected post time
     163                    $watchdog_ts = $next['ts'] + 120;
     164                    wp_schedule_single_event( $watchdog_ts, self::WATCHDOG_HOOK, [ $next['hm'] ] );
     165                }
     166            }
    157167            return;
    158168        }
     
    241251            $today_local = function_exists( 'wp_date' ) ? wp_date( 'Y-m-d', null, $tz ) : date_i18n( 'Y-m-d' );
    242252            $dedupe_key  = 'wwrt_run_' . md5( $today_local . '|' . $hm_key . '|' . $location_key );
     253           
     254            // ATOMIC LOCK: Use add_option for atomic check-and-set to prevent race conditions
     255            // when cron-job.org retries due to timeout (30 seconds)
     256            // This prevents the microsecond race window between get_transient() and set_transient()
     257            $lock_key = $dedupe_key . '_lock';
     258            $lock_acquired = add_option( $lock_key, time(), '', 'no' ); // add_option is atomic
     259           
     260            if ( ! $lock_acquired ) {
     261                // Lock already exists - another request is processing this slot
     262                wwrt_log_and_notify( 'duplicate_run', 'Duplicate run prevented (atomic lock)', [
     263                    'slot_time'    => $slot_time,
     264                    'location_key' => $location_key,
     265                    'run_id'       => $run_id,
     266                ], false );
     267                return new WP_Error( 'wwrt_duplicate_run', 'A run for this time slot is already in progress.' );
     268            }
     269           
     270            // Check transient as secondary guard (in case lock was from old run)
    243271            if ( get_transient( $dedupe_key ) ) {
    244                 // A run for this slot has already been executed recently; skip to avoid duplicate posts and API usage.
    245                 wwrt_log_and_notify( 'duplicate_run', 'Duplicate run prevented for this time slot', [
     272                delete_option( $lock_key ); // Clean up our lock
     273                wwrt_log_and_notify( 'duplicate_run', 'Duplicate run prevented (transient check)', [
    246274                    'slot_time'    => $slot_time,
    247275                    'location_key' => $location_key,
     
    250278                return new WP_Error( 'wwrt_duplicate_run', 'A run for this time slot has already been executed.' );
    251279            }
    252             // Mark this slot as in-progress/done for a short window (15 minutes is plenty for a single run).
    253             set_transient( $dedupe_key, 'running', 15 * MINUTE_IN_SECONDS );
     280           
     281            // Mark this slot as in-progress/done for 60 minutes to prevent any delayed retries or duplicate triggers
     282            set_transient( $dedupe_key, 'running', 60 * MINUTE_IN_SECONDS );
     283           
     284            // Clean up lock after transient is set (lock served its purpose)
     285            delete_option( $lock_key );
    254286        }
    255287       
     
    16721704        }
    16731705       
    1674         $window_start = $slot_ts - 120; // 2 minutes before scheduled time
    1675         $window_end   = $slot_ts + 120; // 2 minutes after scheduled time (when watchdog runs)
     1706        // WIDENED WINDOW: 5 minutes before and 5 minutes after to account for:
     1707        // - Slow post generation (API delays, large content)
     1708        // - Clock drift between WordPress server and cron-job.org
     1709        // - Database commit delays
     1710        $window_start = $slot_ts - 300; // 5 minutes before scheduled time
     1711        $window_end   = $slot_ts + 300; // 5 minutes after scheduled time
    16761712        $has_success = false; $has_failed = false; $already_missed = false;
    16771713       
     
    17061742        if ( $already_missed || $has_success ) {
    17071743            return; // No notification needed
     1744        }
     1745       
     1746        // CRITICAL: Check if actual WordPress posts exist for this time slot
     1747        // This catches cases where event logging failed but post was created successfully
     1748        $args = [
     1749            'post_type' => 'post',
     1750            'post_status' => ['publish', 'draft', 'pending'],
     1751            'meta_query' => [
     1752                [
     1753                    'key' => '_wwrt_schedule_time',
     1754                    'value' => $hm,
     1755                    'compare' => '='
     1756                ]
     1757            ],
     1758            'date_query' => [
     1759                [
     1760                    'after' => gmdate( 'Y-m-d H:i:s', $window_start ),
     1761                    'before' => gmdate( 'Y-m-d H:i:s', $window_end ),
     1762                    'inclusive' => true
     1763                ]
     1764            ],
     1765            'posts_per_page' => 1,
     1766            'fields' => 'ids'
     1767        ];
     1768        $existing_posts = get_posts( $args );
     1769        if ( ! empty( $existing_posts ) ) {
     1770            // Post exists! Event logging may have failed, but post was created successfully
     1771            // No notification needed
     1772            return;
    17081773        }
    17091774       
  • weather-write/trunk/readme.txt

    r3423169 r3423968  
    44Requires at least: 6.5
    55Tested up to: 6.8
    6 Stable tag: 1.3.8
     6Stable tag: 1.3.9
    77License: GPLv2 or later
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    8787
    8888== Changelog ==
     89
     90= 1.3.9 =
     91- CRITICAL FIX: Resolved duplicate posts issue caused by narrow watchdog detection window
     92- Extended deduplication transient from 15 to 60 minutes to prevent delayed retries
     93- Widened watchdog window from 4 minutes to 10 minutes (accounts for slow generation and clock drift)
     94- Added actual WordPress post existence check to prevent false positive watchdog alerts
     95- Improved reliability when API calls are slow or servers have clock differences
    8996
    9097= 1.3.8 =
  • weather-write/trunk/weather-write.php

    r3423169 r3423968  
    33 * Plugin Name: Weather Write
    44 * Description: Generate and publish weather-aware posts with summaries, charts, images, alerts, SEO, and more — fully automated or on-demand.
    5  * Version: 1.3.8
     5 * Version: 1.3.9
    66 * Author: Mike Freeman - WeatherWrite
    77 * Plugin URI: https://www.weatherwrite.com/
Note: See TracChangeset for help on using the changeset viewer.