| @@ -5,8 +5,9 @@ | ||
| 5 | 5 | namespace Yatra\Upgrades; |
| 6 | 6 | |
| 7 | 7 | use Yatra\Core\Database; |
| 8 | 8 | use Yatra\Services\InstallerService; |
| 9 | +use Yatra\Upgrades\Versions\Upgrade_3_0_5; | |
| 9 | 10 | |
| 10 | 11 | /** |
| 11 | 12 | * Orchestrates Yatra Free upgrades: {@see Database::createTables()} for schema sync, then version-gated |
| 12 | 13 | * {@see Versions\* } steps (ALTER / DROP / data) that do not run on a fresh install at the current version. |
| @@ -21,13 +22,90 @@ | ||
| 21 | 22 | final class FreeUpgradeRunner |
| 22 | 23 | { |
| 23 | 24 | public const VERSION_OPTION = 'yatra_version'; |
| 24 | 25 | |
| 26 | + /** Request-level short-circuit so we never re-enter the rename path in | |
| 27 | + * the same PHP process once it has been settled (either completed, | |
| 28 | + * skipped because the option flag is set, or back-off-throttled). */ | |
| 29 | + private static bool $renameSettledThisRequest = false; | |
| 30 | + | |
| 25 | 31 | public static function register(): void |
| 26 | 32 | { |
| 27 | 33 | add_action('admin_init', [self::class, 'runAdminUpgrades'], 5); |
| 34 | + | |
| 35 | + // Frontend + admin: triggers the one-shot `wp_yatra_new_*` -> | |
| 36 | + // `wp_yatra_*` rename without waiting for an admin pageview. | |
| 37 | + // Heavily short-circuited — see runEarlyRenameHeal() — so the | |
| 38 | + // steady-state cost per request is one boolean check. | |
| 39 | + add_action('init', [self::class, 'runEarlyRenameHeal'], 0); | |
| 28 | 40 | } |
| 29 | 41 | |
| 42 | + /** | |
| 43 | + * Cheap-path entry for the one-shot table-rename. Layered guards keep | |
| 44 | + * the cost minimal on the common case where the rename is already done: | |
| 45 | + * | |
| 46 | + * 1. Request-level static — first call sets it, subsequent calls in | |
| 47 | + * the same PHP process exit immediately. | |
| 48 | + * 2. Autoloaded option flag — WordPress preloads all autoload=yes | |
| 49 | + * options on boot, so `get_option()` is an array lookup, not a | |
| 50 | + * query. | |
| 51 | + * 3. Failure-backoff transient — if a prior attempt failed (e.g. | |
| 52 | + * DB user lacks RENAME privilege), we don't retry for 15 min so | |
| 53 | + * we don't slow every pageview indefinitely. | |
| 54 | + * 4. wp_cache_add concurrency lock — only one request at a time | |
| 55 | + * tries the rename. Others see the lock and skip. | |
| 56 | + * | |
| 57 | + * Anything thrown by the rename step is caught here so a partial | |
| 58 | + * migration / DB hiccup can never fatal the page. | |
| 59 | + */ | |
| 60 | + public static function runEarlyRenameHeal(): void | |
| 61 | + { | |
| 62 | + if (self::$renameSettledThisRequest) { | |
| 63 | + return; | |
| 64 | + } | |
| 65 | + | |
| 66 | + if (get_option(Upgrade_3_0_5::RENAME_DONE_OPTION)) { | |
| 67 | + self::$renameSettledThisRequest = true; | |
| 68 | + return; | |
| 69 | + } | |
| 70 | + | |
| 71 | + // Backoff: if a recent attempt failed, skip until the transient | |
| 72 | + // expires. Prevents persistent failures (permission denied, | |
| 73 | + // disk full) from running on every pageview forever. | |
| 74 | + if (get_transient('yatra_table_rename_backoff_v1')) { | |
| 75 | + self::$renameSettledThisRequest = true; | |
| 76 | + return; | |
| 77 | + } | |
| 78 | + | |
| 79 | + // Concurrency lock: ensure only one process attempts the rename | |
| 80 | + // at a time. 30s TTL is the upper bound for a slow rename; | |
| 81 | + // wp_cache_add returns false if the key already exists. | |
| 82 | + if (!wp_cache_add('yatra_table_rename_lock_v1', 1, 'yatra', 30)) { | |
| 83 | + // Another request is renaming — let it finish. | |
| 84 | + self::$renameSettledThisRequest = true; | |
| 85 | + return; | |
| 86 | + } | |
| 87 | + | |
| 88 | + try { | |
| 89 | + $ok = Upgrade_3_0_5::runTableRenameOnce(); | |
| 90 | + if (!$ok) { | |
| 91 | + // Don't retry on every request — back off for 15 minutes | |
| 92 | + // so a persistent failure doesn't trash site responsiveness. | |
| 93 | + set_transient('yatra_table_rename_backoff_v1', 1, 15 * MINUTE_IN_SECONDS); | |
| 94 | + } | |
| 95 | + } catch (\Throwable $e) { | |
| 96 | + // Defensive: nothing the rename does should reach here, but | |
| 97 | + // never let a migration exception take down a public page. | |
| 98 | + if (function_exists('error_log')) { | |
| 99 | + error_log('[Yatra rename] uncaught: ' . $e->getMessage()); | |
| 100 | + } | |
| 101 | + set_transient('yatra_table_rename_backoff_v1', 1, 15 * MINUTE_IN_SECONDS); | |
| 102 | + } finally { | |
| 103 | + wp_cache_delete('yatra_table_rename_lock_v1', 'yatra'); | |
| 104 | + self::$renameSettledThisRequest = true; | |
| 105 | + } | |
| 106 | + } | |
| 107 | + | |
| 30 | 108 | public static function runAdminUpgrades(): void |
| 31 | 109 | { |
| 32 | 110 | if (!defined('YATRA_VERSION')) { |
| 33 | 111 | return; |
| @@ -32,8 +110,18 @@ | ||
| 32 | 110 | if (!defined('YATRA_VERSION')) { |
| 33 | 111 | return; |
| 34 | 112 | } |
| 35 | 113 | |
| 114 | + // *** Rename BEFORE createTables *** | |
| 115 | + // Database::createTables() runs dbDelta, which would create empty | |
| 116 | + // `wp_yatra_*` placeholders alongside the live `wp_yatra_new_*` | |
| 117 | + // tables — triggering Upgrade_3_0_5's both-exist branch on the | |
| 118 | + // very next call. Doing the rename first means dbDelta sees the | |
| 119 | + // canonical names already in place and is a no-op for them. | |
| 120 | + // runEarlyRenameHeal() is itself heavily short-circuited and | |
| 121 | + // safe to call from both init and admin_init. | |
| 122 | + self::runEarlyRenameHeal(); | |
| 123 | + | |
| 36 | 124 | $to = YATRA_VERSION; |
| 37 | 125 | $stored = get_option(self::VERSION_OPTION, false); |
| 38 | 126 | |
| 39 | 127 | if ($stored === false) { |
| @@ -71,6 +159,31 @@ | ||
| 71 | 159 | private static function runIdempotentMaintenance(): void |
| 72 | 160 | { |
| 73 | 161 | InstallerService::maybeBackfillEmailTemplateDefaults(); |
| 74 | 162 | InstallerService::maybeNormalizeMigratedCouponDiscountStatuses(); |
| 163 | + // Heal recurring availability rules whose new-schema columns | |
| 164 | + // (rule_type / seats_total / interval_days / interval_start_date) | |
| 165 | + // were never written by sample data or pre-3.x writers. Without this | |
| 166 | + // the new admin UI showed phantom "1 on All & Active" badges for | |
| 167 | + // legacy rows that rendered as broken weekly rules. | |
| 168 | + InstallerService::maybeNormalizeAvailabilityRulesLegacyData(); | |
| 169 | + // Widen bookings.status enum to accept 'pending_verification' and | |
| 170 | + // restore rows that earlier inserts coerced to '' under the old | |
| 171 | + // enum. Version-independent (runs on every admin pageview, gated | |
| 172 | + // by its own one-shot option) so installs whose stored version | |
| 173 | + // already moved past 3.0.5 by a failed upgrade attempt still heal. | |
| 174 | + InstallerService::maybeAddPendingVerificationBookingStatus(); | |
| 175 | + | |
| 176 | + // Add the nullable `duration_hours` column to trips (hour-based tours). | |
| 177 | + // Additive + idempotent; existing trips get NULL and behave unchanged. | |
| 178 | + InstallerService::maybeAddTripDurationHoursColumn(); | |
| 179 | + | |
| 180 | + // Widen reviews.status enum to accept 'spam' / 'trash' (and | |
| 181 | + // recover rows previously coerced to ''). Called directly here | |
| 182 | + // rather than relying on the version-chain in runAdminUpgrades() | |
| 183 | + // because the bug ships IN 3.0.5 itself — installs whose stored | |
| 184 | + // yatra_version already equals the code version short-circuit | |
| 185 | + // past the chain. The upgrade step's own one-shot option flag | |
| 186 | + // gates the work so this is cheap on subsequent pageviews. | |
| 187 | + Upgrade_3_0_5::run(YATRA_VERSION, YATRA_VERSION); | |
| 75 | 188 | } |
| 76 | 189 | } |