Skip to content

serviceLocator: allow replacing an already-registered service (storage client / event manager / configuration) #3710

@B4nan

Description

@B4nan

Problem

ServiceLocator throws a ServiceConflictError whenever a service that is already registered is set again to a different instance:

  • setConfiguration(config)
  • setStorageClient(client)
  • setEventManager(manager)

The only way to clear a registered service is serviceLocator.reset(), which drops all services at once (configuration, storage client, event manager, logger, and the storage-instance-manager cache). There is no way to replace a single service.

The conflict guard usefully prevents accidental double-registration, but it also makes several legitimate replacement scenarios impossible without nuking everything.

Concrete impact (Apify SDK v4 migration)

  1. Actor.init() cannot run when a storage client is already registered. On the platform, Actor.init() registers an ApifyStorageClient. If anything has already set a storage client — a test harness using @crawlee/memory-storage, a second Actor.init(), or a user who configured their own client before init — setStorageClient throws.

  2. Tests can't swap the storage client. A common pattern is: let Actor.init() register the real client, then swap in an in-memory storage to assert against pushed data. Under v4 this is impossible — reset() would also discard the configuration and event manager that init() just established.

In crawlee v3 the equivalent (Configuration.useStorageClient) allowed replacement, so this is a behavioural regression for these flows.

Repro (sketch)

import { serviceLocator } from '@crawlee/core';
import { MemoryStorage } from '@crawlee/memory-storage';

serviceLocator.setStorageClient(new MemoryStorage());
serviceLocator.setStorageClient(new MemoryStorage());
// throws: Service StorageClient is already in use.

Possible solutions

  1. Last-write-wins — make setX() overwrite an existing service (optionally with a log.warning on replacement). Simplest, but loses the accidental-double-set guard.
  2. Explicit override flagsetStorageClient(client, { override: true }); keeps the guard by default, allows intentional replacement.
  3. Granular per-service reset — add resetStorageClient() / resetEventManager() / resetConfiguration() so a caller can clear one service without dropping the rest.
  4. Allow replacement until first read — track whether a service has been consumed (e.g. getStorageClient() called) and allow replacement until then, throwing only on replace-after-use.

Preference is (2) or (3): they keep the conflict guard for the common accidental case while making intentional replacement an explicit, first-class operation.

Happy to open a PR once we agree on the shape.

Metadata

Metadata

Assignees

No one assigned

    Labels

    debtCode quality improvement or decrease of technical debt.t-toolingIssues with this label are in the ownership of the tooling team.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions