feat: implement crawlee v4 RequestQueueClient over apify-client - #643
Conversation
janbuchar
left a comment
There was a problem hiding this comment.
The description seems outdated, e.g.:
ApifyStorageClient.setStatusMessage— Crawlee v4 calls this on the storage client to surface crawl progress (it was warningsetStatusMessage is not a function).
is no longer true since apify/crawlee#3818
| export interface OpenStorageContext { | ||
| config: Configuration; | ||
| client?: StorageClient; | ||
| client?: StorageBackend; |
There was a problem hiding this comment.
I guess the field deserves a rename too?
| // Crawlee v4's RequestQueueBackend is a stateful, pull-based interface that | ||
| // can't be satisfied by name-remapping; it's implemented on apify-client. |
There was a problem hiding this comment.
| // Crawlee v4's RequestQueueBackend is a stateful, pull-based interface that | |
| // can't be satisfied by name-remapping; it's implemented on apify-client. |
ok thx bye
| * ``` | ||
| */ | ||
| export class ApifyStorageClient implements StorageClient { | ||
| export class ApifyStorageClient implements StorageBackend { |
There was a problem hiding this comment.
Rename the class and file please
| } | ||
|
|
||
| async createRequestQueueClient(options?: CreateRequestQueueClientOptions): Promise<RequestQueueClient> { | ||
| async createRequestQueueBackend(options?: CreateRequestQueueBackendOptions): Promise<RequestQueueBackend> { |
There was a problem hiding this comment.
The options don't make it possible to toggle between single and shared modes (no locking vs locking) — is this tracked in an issue? Or shall we fix it right here right now?
janbuchar
left a comment
There was a problem hiding this comment.
No sense in blocking this, let's try it out in the wild
| async createKeyValueStoreBackend(options?: StorageIdentifier): Promise<KeyValueStoreBackend> { | ||
| const id = await this.resolveId(options, 'KeyValueStore'); | ||
| const client = this.client.keyValueStore(id); | ||
| return adapt( |
There was a problem hiding this comment.
I don't know, it would be easier to understand if we just made two dummy classes, DatasetBackend and KeyValueStoreBackend, like for the request queue
Adapts to the changed upstream APIs: storage backend factory methods now take a StorageIdentifier (create*Backend), setStatusMessage moved off the storage backend to the run API (apify/crawlee#3818), moved types (QueueOperationInfo, Constructor, Dictionary), removed helpers (snakeCaseToCamelCase is now inlined), privatized ProxyConfiguration internals, and the KeyValueStore frontend field changes. Also adds @apify/datastructures, used by the upcoming request queue backends.
Configuration.getGlobalConfig() is now getGlobalConfiguration(), and the public config properties are now called configuration: Actor.configuration (static getter and instance property), ProxyConfiguration.configuration and PlatformEventManager.configuration. Follows the same renames in Crawlee v4.
| * dataset client (pay-per-event on `Actor.pushData()`), provided a charging | ||
| * manager is supplied and a default-dataset-item price is configured. | ||
| * | ||
| * `storageExists()` lets `Dataset.open(idOrName)` resolve a string to an id |
There was a problem hiding this comment.
Implementation details leaking in comment?
|
|
||
| describe('ApifyStorageBackend', () => { | ||
| test('creates a single-consumer request queue backend by default', async () => { | ||
| const client = createMockApifyClient(); |
There was a problem hiding this comment.
This doesn't seem to test anything typescript wouldn't catch
d1b71f6 to
82af3d3
Compare
Follows crawlee v4's StorageClient -> StorageBackend interface rename.
The constructor now takes an options object ({ configuration }) instead
of positional arguments, and OpenStorageContext.client is now
OpenStorageContext.backend. (ApifyStorageBackend is new in v4, so this
is not a breaking change against v3.)
Crawlee v4 passes StorageIdentifier aliases down to the storage backend (including the reserved __default__ alias used for every default storage open). The backend now resolves them itself: __default__ maps to the run's default storage id, other aliases resolve via the Actor schema storages (ACTOR_STORAGES_JSON), undeclared aliases on the platform get a clear error, and outside the platform an unnamed storage is created per alias and process. Also implements getStorageBackendCacheKey, which partitions crawlee's storage-instance cache by API base URL and token.
Implements crawlee v4's stateful, pull-based RequestQueueBackend for the
Apify platform, modeled on the Python SDK's request queue clients:
- single (default): assumes one consumer; no request locking, local head
estimation and full-request caching, so most requests are processed
without per-request read API calls. A one-time prefetch of existing
queue contents lets resurrected runs deduplicate re-added requests
locally instead of paying for platform writes.
- shared (requestQueueAccess: 'shared'): safe for any number of
concurrent consumers via server-side locking (listAndLockHead); the
lock duration follows the crawler's expected request processing time.
The mode is selected via Actor.init({ requestQueueAccess }) or the
ApifyStorageBackend constructor option. Request queue clients send a
stable per-run clientKey (the run id), so a migrated or resurrected run
re-acquires the locks of its previous incarnation.
82af3d3 to
5e83735
Compare
Crawlee v4's storage backends are byte transports: the KeyValueStore frontend parses record values according to their content type. The backend was handing over values already parsed by apify-client, so JSON records got parsed twice. Records are now fetched with buffer: true and returned unparsed. Also returns the paginated listKeys result shape the interface expects instead of a bare items array.
…classes Adds ApifyDatasetBackend and ApifyKeyValueStoreBackend, mirroring the request queue backends, instead of wrapping apify-client resource clients in a generic renaming Proxy. The pay-per-event marker now lives on the dataset backend instance, and the Apify-store detection in KeyValueStore.getPublicUrl() is a plain instanceof check.
5e83735 to
16cb202
Compare
Crawlee v4 redesigned the request queue storage layer into a stateful, pull-based
RequestQueueBackendinterface, modeled on the Python SDK. The SDK's old name-remapping proxy can't satisfy it, so this PR implements it for the Apify platform, with two access modes. The Python SDK's request queue clients were the starting point, but the design follows the JS API: the crawlee JS frontend already keeps large dedup caches, so the backends only manage what the frontend can't.Request queue access modes
single(default,ApifyRequestQueueSingleBackend) assumes the run is the only consumer of the queue. There is no request locking; the queue head is estimated locally, and requests added by this client are served from a local cache, so a typical request costs no read API calls. On the first add, up to 10k existing requests are prefetched, letting resurrected runs deduplicate re-added requests locally instead of paying a platform write per request. Multiple producers may still add requests concurrently.shared(ApifyRequestQueueSharedBackend) is safe for any number of concurrent consumers. Fetched requests are locked server-side (listAndLockHead), and the lock duration follows the crawler's expected request processing time (setExpectedRequestProcessingTimeSecs, i.e. handler timeout plus padding). Reclaims release the lock so other consumers can pick the request up immediately, andisFinishedconsultsqueueHasLockedRequestsso no consumer exits while another still holds work.The mode is selected per storage backend, as in the Python SDK:
Actor.init({ requestQueueAccess: 'shared' })ornew ApifyStorageBackend(client, { requestQueueAccess: 'shared' }). Opening the same queue in both modes at once is not supported; whichever backend opens it first wins (documented ongetStorageBackendCacheKey).Request queue clients now send a stable per-run
clientKey(the run id, same as the Python SDK), sohadMultipleClientsstays meaningful and a migrated or resurrected run re-acquires the locks of its previous incarnation.Both backends honor the beta.105 contract:
markRequestAsHandled/reclaimRequestof a request that does not exist returnundefinedwithout upserting it, andpurge()throws, since the platform has no truncate endpoint (Python behaves the same).Review feedback addressed
ApifyStorageClientrenamed toApifyStorageBackend(class and file),OpenStorageContext.clientrenamed tobackend, stray comment removed.single/sharedtoggle from the review is implemented here rather than tracked in an issue.Crawlee beta.105 adaptation (was beta.71)
create*Backendnow receives crawlee'sStorageIdentifier({id} | {name} | {alias}). The backend resolves aliases itself:__default__maps to the run's default storage, other aliases resolve viaACTOR_STORAGES_JSON, undeclared aliases on the platform get a clear error, and anywhere else an unnamed storage is created per alias and process. A KVS-persisted alias mapping like Python'sAliasResolver, which would let undeclared aliases survive migrations, is left as a follow-up.getValuereads buffers and leaves parsing to crawlee's frontend (previously the value got parsed twice),listKeysreturns the new paginated shape, andrecordExistspasses through.getStorageBackendCacheKeypartitions crawlee's storage cache by API base URL and token.getGlobalConfigrenamed togetGlobalConfiguration;QueueOperationInfo/Constructor/Dictionarymoved to@crawlee/types;snakeCaseToCamelCaseinlined (removed upstream);ProxyConfigurationinternals were privatized upstream (ownlog, no more base-class field reads);KeyValueStore.config/clientfields replaced byserviceLocator/backend.Validation
Unit tests cover both backends against a mocked
apify-client(head estimation, dedup, prefetch, in-progress tracking, forefront ordering, lock duration raising,queueHasLockedRequests, the no-upsert contract), plus the storage backend's alias resolution and access mode wiring. Build, lint, and the full suite (148 tests) pass. Thesinglemode is the successor of the flow validated end-to-end on the platform earlier in this PR; re-validating both modes on the platform against beta.105 is the remaining step.