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

Guard to avoid redefinition

  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__ ).

Load the version registry

1.3 Core ActionScheduler::init() flow

Register this version

Theme fallback (non-plugin use)

  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)

Guard to avoid redefinition

2.1 ActionScheduler::factory()

  • 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.2 ActionScheduler::store()

  • 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.

2.3 ActionScheduler::lock()

  • 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().

2.4 ActionScheduler::logger()

  • 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.

2.5 ActionScheduler::runner()

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

2.6 ActionScheduler::admin_view()

  • spl_autoload_register( array( __CLASS__, 'autoload' ) );

2.7 Singleton enforcement

Load the version registry

  • do_action( 'action_scheduler_pre_init' );

Register this version

3. Autoloader Logic

Theme fallback (non-plugin use)

3.1 Namespace handling

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

3.2 Directory resolution rules

Store plugin file path

  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.
  8. CLI helper classes

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

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

Register the autoloader

4. The action_scheduler_init Hook

Early hook

4.1 When it fires

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

4.2 Why it matters

  • 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.

5. Version Competition: Multiple Plugins Bundling Action Scheduler

Load procedural API & data controller

5.1 How versions register

Instantiate core singletons / services

  • 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.

Hook service init methods

  • deprecated/functions.php is loaded if action_scheduler_load_deprecated_functions filter returns true.

5.2 How the “winner” is chosen

Mark data store initialized and fire action_scheduler_init

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

Optional deprecated functions

5.3 Theme fallback behavior

WP-CLI command registration

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

5.4 Practical implication for plugin authors

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

6. Data Layer Control (Migration & Store Selection)

Post-migration cleanup

6.1 Core responsibilities

  • 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).

6.2 Migration state

  • 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.

6.3 Initialization logic

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

  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

  • 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().

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 makes the entry point responsible only for version registration and delegating the actual setup to the core class.