Skip to content

Lifecycle

Install, uninstall, TTL, delete cascades — how state gets created, maintained, and torn down.

Owner projection

Owner modules (wow_character, wow_guild, wow_user) project reference fields onto entities owned by OTHER modules.

The flow has two paths that produce the same end state:

Path A — owner installed FIRST

wow_character install
wow_character_install() runs — iterates _wow_character_owned_entity_types()
for each entity type that exists: installFieldStorageDefinition('character', ...)
(later) wow_achievement install
wow_achievement_progress schema is built
hook_entity_base_field_info fires → wow_character contributes the 'character' base field
column added automatically during schema creation

Path B — owner installed LATER

wow_achievement install
wow_achievement_progress schema built, no 'character' column
(later) wow_character install
wow_character_install() iterates owned types, finds wow_achievement_progress
installFieldStorageDefinition('character', 'wow_achievement_progress', ...)
column added to the existing schema

Both paths are tested in CharacterFieldInjectionOrderingTest, UserFieldInjectionOrderingTest, and GuildFieldInjectionOrderingTest.

Uninstall

Uninstalling an owner module drops its projection cleanly:

  • Schema columns are removed from every target entity type.
  • Rows with the projected owner populated are purged first (so the projection column has nothing to migrate through Drupal's field-storage delete hooks).
  • Orphan rows (projection column NULL) survive.
  • Catalog entities owned by other modules are untouched.

Uninstalling a catalog module goes further: its own base tables, state keys, and user.data namespace (where applicable) are also removed.

The 30-day TTL

Blizzard's Terms of Use require API-derived data to be refreshed or deleted within 30 days. The wow.ttl_sweeper service enforces this:

  • Runs daily under hook_cron, throttled by the wow.ttl_sweep_last state key.
  • On demand: drush wow:sweep-ttl.
  • Sweeps every content entity type that declares a last_fetched base field — catalogs AND junction entities.
  • Taxonomy reference data (which uses a wow_last_fetched configurable field because taxonomy_term can't host arbitrary base fields) is NOT swept; operators keep it fresh via the scheduled sync commands.

Junction entities have their own last_fetched independent of their owning character/guild. Two scenarios make this matter:

  1. Blizzard's character profile API can return a partial collection on big accounts — untouched rows go stale while the character itself is still fresh.
  2. When a player un-collects a mount/pet/title, Blizzard stops returning that row; the local junction row sits frozen until swept.

A regression test (TtlSweepCoverageTest) scans every entity class for last_fetched and asserts each is registered in TtlSweeper::SWEEP_ENTITY_TYPES. Adding a new catalog entity without wiring the sweep fails CI.

Delete cascades

Three cascades, all using hook_entity_predelete:

Character delete → CharacterDataProvider purges

Character::delete()
wow_character_entity_predelete()
for each CharacterDataProvider:
  try { deleteCharacterData($character); } catch log & continue

Every registered provider purges its per-character rows. One provider throwing doesn't block the rest; failures are logged with @id, @cid, @name, @msg context.

Guild delete → GuildDataProvider purges

Same shape, via wow_guild_entity_predelete() and GuildDataProvider plugins.

Two entry points that converge on wow_user_purge_account_data($uid):

  1. Account cancellationwow_user_entity_predelete() on the user entity.
  2. Battle.net de-link — form_alter + submit handler on openid_connect_accounts_form, scoped to the battlenet client only.

Both call the same purge function which: - deletes every wow_character row owned by the user (which in turn cascades to all CharacterDataProvider plugins), - clears user.data under wow_user and wow_battlenet_login.

Rate limiting

Two windows, both in Drupal State so all PHP processes share one counter:

  • wow.api_rate_limit — hourly (36,000 requests)
  • wow.api_rate_limit_second — per-second (100 requests)

Check + increment is serialized under the Drupal lock service (wow:api_rate_limit). If the lock can't be acquired after a few retries, the request is throttled — the client refuses to mutate counters with no guard held.

This closes the last concurrent-process race. See BattleNetClient::reserveRateSlot() and RateLimitIntegrationTest / BattleNetClientRateLimitTest.