Action Scheduler Architecture

This document explains the core initialization flow and internal architecture of Action Scheduler (AS) based on the following source files:

  • action-scheduler.php (entry point)
  • classes/abstracts/ActionScheduler.php (core class)
  • classes/ActionScheduler_Versions.php (version management)
  • classes/ActionScheduler_DataController.php (data layer control)

The goal is to help developers understand how AS bootstraps, how it chooses a single active version when multiple plugins bundle it, how the singleton accessors work, and how the autoloader maps class names to files.

1. Bootstrap / Initialization Flow

1.1 Entry point (action-scheduler.php)

The plugin entry point is responsible for registering the current AS version and initializing the latest registered version at the correct time.

Key steps in order:

  1. Guard to avoid redefinition

    • The file only runs if action_scheduler_register_3_dot_9_dot_3() is not already defined and WordPress is loaded (i.e., add_action() exists).
  2. Load the version registry

    • If the ActionScheduler_Versions class does not yet exist, the file is required, and its initialize_latest_version() method is hooked on plugins_loaded with priority 1.
    • This ensures that after all plugins have had a chance to register their AS version, the highest version wins and is initialized.
  3. Register this version

    • On plugins_loaded with priority 0, the entry point calls action_scheduler_register_3_dot_9_dot_3().
    • That function registers the version string (3.9.3) with a callback action_scheduler_initialize_3_dot_9_dot_3() via ActionScheduler_Versions::instance()->register().
  4. Theme fallback (non-plugin use)

    • If plugins_loaded has already fired and no plugin has loaded AS yet, the entry point initializes this version immediately, fires action_scheduler_pre_theme_init, and then calls ActionScheduler_Versions::initialize_latest_version().
    • This allows AS to be bundled by themes while still participating in the “latest version wins” flow where possible.

1.2 Version initialization callback

Each version callback (for example action_scheduler_initialize_3_dot_9_dot_3()) is responsible for:

  1. Safety check: only initialize if ActionScheduler is not already defined.
  2. Loading the core class: require_once classes/abstracts/ActionScheduler.php.
  3. Starting the system: ActionScheduler::init( __FILE__ ).

This makes the entry point responsible only for version registration and delegating the actual setup to the core class.

1.3 Core ActionScheduler::init() flow

Once ActionScheduler::init() is called, the core bootstrapping happens in a single static method.

High-level flow:

  1. Store plugin file path

    • self::$plugin_file is set to the entry point file path.
  2. Register the autoloader

    • spl_autoload_register( array( __CLASS__, 'autoload' ) );
  3. Early hook

    • do_action( 'action_scheduler_pre_init' );
  4. Load procedural API & data controller

    • require_once functions.php.
    • ActionScheduler_DataController::init() sets up data-store selection and migration logic (details in Section 5).
  5. Instantiate core singletons / services

    • store(), logger(), runner(), admin_view() return singleton instances.
    • A new ActionScheduler_RecurringActionScheduler instance is created (not stored globally).
  6. Hook service init methods

    • If init has not run yet, AS attaches each service’s init() method to the WordPress init hook (with controlled priorities).
    • If WordPress init already ran, AS directly calls each service’s init() method immediately.
  7. Mark data store initialized and fire action_scheduler_init

    • After the store is initialized, AS sets self::$data_store_initialized = true and fires do_action( 'action_scheduler_init' ).
    • This action is the canonical signal that the procedural API is safe to use.
  8. Optional deprecated functions

    • deprecated/functions.php is loaded if action_scheduler_load_deprecated_functions filter returns true.
  9. WP-CLI command registration

    • If running under WP-CLI, AS registers commands and optional migration commands.
  10. Post-migration cleanup

    • If a DB logger is in use and migration is complete, the WP comment cleanup flow may be initialized.

2. Core Singleton Pattern (Factory, Store, Lock, Logger, Runner)

Action Scheduler centralizes access to its core services through static accessors on the ActionScheduler class.

2.1 ActionScheduler::factory()

  • Returns the singleton instance of ActionScheduler_ActionFactory.
  • Lazily instantiated; stored in self::$factory.

2.2 ActionScheduler::store()

  • Returns ActionScheduler_Store::instance().
  • The actual store class is filtered by action_scheduler_store_class, controlled in part by ActionScheduler_DataController.

2.3 ActionScheduler::lock()

  • Returns ActionScheduler_Lock::instance().
  • Lock implementation is based on the active lock class (an abstract base defines the contract).

2.4 ActionScheduler::logger()

  • Returns ActionScheduler_Logger::instance().
  • Logger class selection is also filterable; ActionScheduler_DataController sets defaults when migration is complete.

2.5 ActionScheduler::runner()

  • Returns ActionScheduler_QueueRunner::instance().
  • Responsible for executing scheduled actions.

2.6 ActionScheduler::admin_view()

  • Returns ActionScheduler_AdminView::instance().
  • Manages the admin UI.

2.7 Singleton enforcement

ActionScheduler itself is a static-only class:

  • __construct() is private.
  • __clone() and __wakeup() trigger errors to prevent duplication.

This ensures there is a single global entry point for all core services.

3. Autoloader Logic

The autoloader is defined by ActionScheduler::autoload() and relies on naming conventions to map class names to directories.

3.1 Namespace handling

  • If a class name contains a namespace separator (), only classes in the Action_Scheduler namespace are handled.
  • The autoloader strips the namespace and loads by the short class name.

3.2 Directory resolution rules

The autoloader uses suffixes and prefixes to decide which directory to search:

  1. Deprecated classes

    • Class ends with Deprecateddeprecated/ directory.
  2. Abstract classes

    • Determined by is_class_abstract() list → classes/abstracts/.
  3. Migration classes

    • Determined by is_class_migration() segment list → classes/migration/.
  4. Schedules

    • Class ends with Scheduleclasses/schedules/.
  5. Actions

    • Class ends with Actionclasses/actions/.
  6. Schema

    • Class ends with Schemaclasses/schema/.
  7. ActionScheduler core classes*

    • Class begins with ActionSchedulerclasses/ by default.
    • If the second underscore segment matches one of:
      • WPCLIclasses/WP_CLI/
      • DBLogger, DBStore, HybridStore, wpPostStore, wpCommentLoggerclasses/data-stores/
  8. CLI helper classes

    • Determined by is_class_cli() list → classes/WP_CLI/.
  9. Bundled libraries

    • CronExpression*lib/cron-expression/
    • WP_Async_Request*lib/

If the resolved file exists, it is included; otherwise the autoloader returns without error.

4. The action_scheduler_init Hook

The action_scheduler_init action is the official signal that Action Scheduler is fully initialized and the procedural API can be safely used.

4.1 When it fires

  • It fires after the data store is initialized.
  • If WordPress init has not yet fired:
    • AS hooks a closure to init at priority 1.
    • That closure sets self::$data_store_initialized = true and fires action_scheduler_init.
  • If WordPress init has already fired:
    • AS initializes services immediately and fires action_scheduler_init right away.

4.2 Why it matters

  • ActionScheduler::is_initialized() will emit a _doing_it_wrong notice if the procedural API is used before this hook.
  • Core services (store, logger, runner) are not guaranteed to be available before it.
  • Recurring action scheduling uses this hook (e.g., ActionScheduler_RecurringActionScheduler adds itself to action_scheduler_init).

5. Version Competition: Multiple Plugins Bundling Action Scheduler

Action Scheduler is commonly bundled in multiple plugins. The design ensures that only one version initializes, and that it is the highest registered version.

5.1 How versions register

Each bundled copy registers its version on plugins_loaded priority 0:

  • ActionScheduler_Versions::instance()->register( $version, $callback )

The registry stores:

  • A map of version strings → initialization callbacks.
  • A map of source file paths → version strings (via debug_backtrace() to record the registering file).

5.2 How the “winner” is chosen

On plugins_loaded priority 1, ActionScheduler_Versions::initialize_latest_version():

  1. Determines the latest version via version_compare on all registered versions.
  2. Calls the callback associated with the latest version.

This ensures that only the highest version initializes and all lower versions remain inert.

5.3 Theme fallback behavior

If AS is bundled by a theme, plugin hooks might already be done. In that case:

  • The entry point initializes the version immediately.
  • It then fires action_scheduler_pre_theme_init and still calls ActionScheduler_Versions::initialize_latest_version() to allow plugins to register their versions.

5.4 Practical implication for plugin authors

  • Bundling AS is safe because version registration is deterministic.
  • Only the highest registered version initializes, preventing multiple instances.
  • ActionScheduler_Versions keeps source tracking that can be used (in newer versions) to determine which plugin/theme supplied the active copy.

6. Data Layer Control (Migration & Store Selection)

ActionScheduler_DataController is responsible for deciding which storage backend and handling migration between storage systems.

6.1 Core responsibilities

  • Expose the canonical store class name (ActionScheduler_DBStore) and logger class name (ActionScheduler_DBLogger).
  • Decide whether migration is complete or should run.
  • Register store/logger filters when migration is complete.
  • Hook action_scheduler/progress_tick to free memory during long-running CLI jobs.

6.2 Migration state

  • A site option action_scheduler_migration_status tracks completion.
  • is_migration_complete() checks if it equals 'complete'.
  • mark_migration_complete() and mark_migration_incomplete() manage the flag.

6.3 Initialization logic

During ActionScheduler_DataController::init():

  1. If migration is complete:

    • Adds filters to force the DB store and DB logger classes.
    • Hooks deactivate_plugin to reset migration state (to prevent reverting to a 2.x store when a plugin is deactivated).
  2. If migration is not complete but dependencies are met:

    • Starts the migration controller (Action_SchedulerMigrationController::init()).
  3. Hooks action_scheduler/progress_tick to maybe_free_memory() for memory management during bulk processing.

6.4 Memory management for CLI

  • maybe_free_memory() frees memory after a configurable number of ticks.
  • It resets $wpdb->queries and clears caches on WP_Object_Cache where available.
  • Optional sleep time can be set for large operations.

7. Summary Flow Diagram (Textual)

plugins_loaded (priority 0)
  └─ action_scheduler_register_X() → ActionScheduler_Versions::register()

plugins_loaded (priority 1)
  └─ ActionScheduler_Versions::initialize_latest_version()
       └─ action_scheduler_initialize_X()
            └─ ActionScheduler::init( plugin file )
                 ├─ register autoloader
                 ├─ do_action('action_scheduler_pre_init')
                 ├─ require functions.php
                 ├─ ActionScheduler_DataController::init()
                 ├─ instantiate store/logger/runner/admin/recurring scheduler
                 ├─ init via WP `init` (or immediate if already fired)
                 └─ do_action('action_scheduler_init')

This is the canonical boot flow for Action Scheduler. Every other internal component depends on the guarantees established by this sequence.