Processing outside the API — glance — branch stable/2026.2
Branch
stable/2026.2@a3e5f490b7a2— repository https://opendev.org/openstack/glance — analysed on 2026-10-28
Glance has no RPC server, no bus consumer and no notification listener: it only emits notifications (see 03). The three "notification listener" triggers found by the inventory (glance/notifier.py:107, :110, :113) are false positives: they are the warn/info/error methods of Glance's own notification emitter.
Everything that happens after a 202 Accepted runs inside the glance-api worker that received the request, in a thread pool, as a taskflow flow. The other non-HTTP processing comes from timers in separate commands (the scrubber, the deprecated cache commands) and from one-shot startup work.
Summary
| Process | Trigger | Type | Purpose | Reads | Writes / emits | Defined at |
|---|---|---|---|---|---|---|
| glance-api | api_image_import task | in-process thread pool tasks_pool, started by POST /v2/images/{id}/import | import the image data into one or more stores (glance-direct, web-download, copy-image, glance-download) | staging file, remote URL, remote Glance, existing location | store write, images, image_locations, image_properties, tasks, notifications | glance/async_/flows/api_image_import.py:930 |
| glance-api | location_import task | thread pool, started by POST /v2/images/{id}/locations | attach an external location, compute or check the hash, activate the image | location URL (store get) | images, image_locations, tasks | glance/async_/flows/location_import.py:299 |
| glance-api | legacy import task | thread pool, started by POST /v2/tasks (admin only by default) | create an image from import_from (HTTP URL), optionally convert or introspect | remote URL | new image, store write, tasks | glance/async_/flows/base_import.py:431 |
| glance-api | CacheWorker thread | in-process queue, fed by PUT /v2/cache/{image_id} | download an active image into the local image cache | store get | cache files, cached_images (centralized_db driver) | glance/api/v2/cached_images.py:303 |
| glance-api | cache middleware tee | every GET /v2/images/{id}/file served from the store when the cache is enabled | fill the cache while streaming to the client | response stream | cache files, cache metadata | glance/api/middleware/cache.py:268 |
| glance-api | staging cleanup | once per worker at startup (daemon thread) | delete staging files whose image is gone | staging directory, images | files deleted | glance/common/wsgi_app.py:102, glance/housekeeping.py:85 |
| glance-api | import lock expiry | on the next import or add-location request on the same image | steal a lock left by a dead or stuck task | tasks, image_properties.os_glance_import_task | task failure, lock property deleted | glance/api/v2/images.py:171 |
| glance-api | task expiry | on every task list / GET /v2/images/{id}/tasks | soft-delete tasks past expires_at | tasks | tasks.deleted | glance/db/sqlalchemy/api.py:1884 |
| glance-scrubber | Daemon._run | timer, every wakeup_time s (default 300), only with --daemon | delete the data of pending_delete images after scrub_time | images, image_locations | store delete, image_locations.status, images.status = deleted | glance/scrubber.py:309 |
| glance-cache-prefetcher / -pruner / -cleaner | cron or manual run | one-shot commands, deprecated since Epoxy (replaced by the /v2/cache API) | prefetch queued images, prune to image_cache_max_size, clean stalled entries | cache metadata | cache files | glance/cmd/cache_prefetcher.py:49, glance/cmd/cache_pruner.py:46, glance/cmd/cache_cleaner.py:54 |
Task execution model (all three task types)
- Trigger: the API handler creates the task row (
status = pending), thencommon.get_thread_pool("tasks_pool").spawn(task.run, executor)(glance/api/v2/images.py:496-497,glance/api/v2/tasks.py:91-92). The pool size is[wsgi]/task_pool_threads(default 16,glance/common/config.py:652), set at startup (glance/common/wsgi_app.py:135). - Emitter: the glance-api worker that accepted the request. There is no queue between workers: if that worker dies, the task stays in
processing(orpending) until a later request steals its lock (see Import lock expiry). - Executor:
TaskExecutor._run(glance/async_/taskflow_executor.py:174) moves the task toprocessing, loads the flow by name from theglance.flowsstevedore namespace (glance/async_/taskflow_executor.py:134;pyproject.toml:49-52mapsapi_image_import,import,location_import), and runs it with a taskflow engine (glance/async_/taskflow_executor.py:201) in[taskflow_executor]/engine_mode(parallelby default) withmax_workersthreads. - End: the last task of the flow calls
task.succeed()ortask.fail(). Both setexpires_at = now + [task]/task_time_to_livehours (default 48,glance/domain/__init__.py:437-446,glance/common/config.py:148). If the flow raises,_fail_task(glance/async_/taskflow_executor.py:149) records the message. - Shutdown: at process exit,
drain_workerswaits for the running tasks oftasks_pooland stops the cache worker (glance/common/wsgi_app.py:86).
api_image_import (interoperable image import)
- Trigger:
POST /v2/images/{image_id}/import(202), see 01. - Purpose: move the image data from wherever the import method says into the target stores, then activate the image.
- Flow (
glance/async_/flows/api_image_import.py:930-1069). The flow is linear, withretry.AlwaysRevert(): any failure reverts the tasks already run, in reverse order.- *(before the flow starts, still synchronous in
get_flow)* Unless the method iscopy-image, the image goes toimporting. The target stores are written toos_glance_importing_to_storesand removed fromos_glance_failed_import, andos_glance_stage_hostis dropped (glance/async_/flows/api_image_import.py:1041-1047). Then quotas are checked:image_size_totalforglance-direct;image_stage_totalandimage_count_uploadingfor the other methods (glance/async_/flows/api_image_import.py:1049-1067,glance/async_/flows/api_image_import.py:891). A quota failure fails the task and reverts the image. _ImageLockchecks that the image still carries this task's lock (glance/async_/flows/api_image_import.py:487).glance-downloadonly:_ImportMetadatafetches the remote image (GET {remote}/v2/images/{id}), refuses it unless it isactive, and copiesdisk_format,container_formatand the properties listed in[glance_download_properties]/extra_properties(glance/async_/flows/api_image_import.py:804).web-download,copy-image,glance-download: an internal plugin writes the data to the staging store.web-downloaddownloads the URL (glance/async_/flows/_internal_plugins/web_download.py:39);copy-imagereads an existing location (glance/async_/flows/_internal_plugins/copy_image.py:48);glance-downloaddownloads{remote}/v2/images/{id}/fileand checks that the size matches (glance/async_/flows/_internal_plugins/glance_download.py:46)._VerifyStagingchecks that the staging file exists (glance/async_/flows/api_image_import.py:522).- Except for
copy-image, the configured processing plugins run in order ([image_import_opts]/image_import_plugins, default none):image_decompression(zip, gzip, lha;glance/async_/flows/plugins/image_decompression.py:144),image_conversion(qemu-img infoandqemu-img convertto[image_conversion]/output_format, defaultraw, after format inspection and safety checks;glance/async_/flows/plugins/image_conversion.py:114),inject_image_metadata(glance/async_/flows/plugins/inject_image_metadata.py:66),no_op. - One
_ImportToStoreper store, in order (glance/async_/flows/api_image_import.py:596). With[image_import_opts]/max_parallel_stores > 1and several stores, the parallel variant writes them concurrently (glance/async_/flows/parallel_api_image_import.py:60,glance/async_/flows/parallel_api_image_import.py:76). Each write updates the task message every chunk (Copied N MiB) and aborts if the task is no longerprocessing(glance/async_/flows/api_image_import.py:687-707). The image becomesactiveafter the last store, or after the first one whenall_stores_must_succeedis false; never forcopy-image. _DeleteFromFSdeletes the staging file (glance/async_/flows/api_image_import.py:444)._VerifyImageStaterequiresactive, otherwise raises_NoStoresSucceeded("None of the uploads finished!"); its revert puts the image back toqueued(exceptcopy-image) (glance/async_/flows/api_image_import.py:723)._CompleteTaskmarks the tasksuccesswith{"image_id": ...}and drops the lock (glance/async_/flows/api_image_import.py:748).
- *(before the flow starts, still synchronous in
- Error behaviour: with
all_stores_must_succeed = true(the default), a store failure reverts everything: the locations already added are removed, the stores are moved toos_glance_failed_import, the image goes back toqueuedand the task tofailure. Withfalse, a failed store is only recorded inos_glance_failed_import(glance/async_/flows/api_image_import.py:673-682). The client sees the outcome only throughGET /v2/images/{id}(status andos_glance_*properties) orGET /v2/images/{id}/tasks. - Concurrency: one import per image, enforced by the atomic property
os_glance_import_task(insert that fails on duplicate;glance/db/sqlalchemy/api.py:891). Two workers can run imports of different images in parallel. - Cost: the whole image is read once per store, plus once by each processing plugin.
copy-imageand the download methods first copy the image into staging (local disk of the worker:node_staging_uri, or the reservedos_glance_staging_store).
location_import (asynchronous add-location)
- Trigger:
POST /v2/images/{image_id}/locations(202) on aqueuedimage. - Flow (
glance/async_/flows/location_import.py:299):_ImageLock, then_UpdateLocationTaskadds the location after validating it against the stores (glance/async_/flows/location_import.py:224).- With
do_secure_hash = true(default),_CalculateHashreads the data through the store and computesos_hash_valuewithhttp_retriesattempts. It runs before activation when validation data was given, after activation otherwise (glance/async_/flows/location_import.py:54). It publishesos_glance_hash_op_hostso that other workers proxy a delete to this one, and it registers with the cancellation tracker so thatDELETE /v2/images/{id}can stop it (glance/task_cancellation_tracker.py:53,glance/task_cancellation_tracker.py:73). _VerifyValidationDatacompares the computed hash with the one given by the client; a mismatch fails the task and puts the image back toqueued(glance/async_/flows/location_import.py:152)._SetImageToActiveTaskactivates the image (glance/async_/flows/location_import.py:261), and_CompleteTaskends the task.
- Error behaviour: an invalid location raises
_InvalidLocation; a hash failure afterhttp_retriesraises_HashCalculationFailedand reverts toqueuedonly if the image is not yet active (glance/async_/flows/location_import.py:85-103).
Legacy import task (POST /v2/tasks, type import)
- Trigger:
POST /v2/taskswith{"type": "import", "input": {"import_from": "http://…", "image_properties": {…}}}. Access needstasks_api_access(admin by default,glance/policies/tasks.py:105). - Flow (
glance/async_/flows/base_import.py:431):_CreateImagecreates the image fromimage_properties(glance/async_/flows/base_import.py:44);_ImportToFSdownloadsimport_frominto[task]/work_dir(glance/async_/flows/base_import.py:84); the optionalconvert/introspect/ovf_processsub-flows run (pyproject.toml:54-57);_ImportToStorewrites to the default store (glance/async_/flows/base_import.py:227);_DeleteFromFScleans up. - The tasks API is deprecated (
glance/api/v2/tasks.py:106) but still routed.
Import lock expiry
- Trigger: a new
importor add-location request findsos_glance_import_taskalready set (glance/api/v2/images.py:389-392,glance/api/v2/images.py:1203-1206). - Rule (
glance/api/v2/images.py:171-220): the lock is stolen if the task row no longer exists, or if its status ispending,processing,successorfailureand it was last updated more than 60 minutes ago (120 minutes forpending). The old task is markedfailure("Expired lock preempted"), its in-progress store markers are cleaned, and the new request proceeds. Otherwise the request gets 409 "Image has active task". - Concurrency: the property is removed with a compare-and-delete (
glance/db/sqlalchemy/api.py:946); losing the race gives 409.
Task expiry
_task_soft_deletemarks as deleted every task whoseexpires_atis past, each time tasks are listed or read by image (glance/db/sqlalchemy/api.py:1884, called atglance/db/sqlalchemy/api.py:1842andglance/db/sqlalchemy/api.py:1930). There is no timer;glance-manage db purgeremoves the soft-deleted rows later.
Staging cleanup at startup
- Trigger:
init_appof each glance-api worker starts a daemon thread (glance/common/wsgi_app.py:102,glance/common/wsgi_app.py:164). - Purpose: delete leftovers of staged uploads whose image was deleted while this worker was down.
- Data read: the file names of the staging directory (
file://only,glance/housekeeping.py:31), each parsed as an image UUID, thenimage_getper file. - Effects: files of missing or deleted images are removed; others are kept (
glance/housekeeping.py:85-126). Errors are logged and counted, never raised.
Image cache worker
- Trigger:
PUT /v2/cache/{image_id}queues the image in the cache metadata and submits it to the in-processCacheWorkerqueue (glance/api/v2/cached_images.py:276). The worker is started by the firstCacheControllerinstance whenimage_cache_diris set (glance/api/v2/cached_images.py:67-71). - Flow:
Prefetcher.fetch_image_into_cachereads the image as an admin, refuses non-active images, and tees the first readable location into the cache, verifying the checksum (glance/image_cache/prefetcher.py:43-73). - Concurrency: one worker thread per glance-api process; cache files are local to the node (
image_cache_dir). With the defaultcentralized_dbdriver, the cache metadata of all nodes lives in thenode_referenceandcached_imagestables, keyed byworker_self_reference_url(glance/image_cache/drivers/centralized_db.py:65-69).
glance-scrubber (delayed delete)
- Trigger:
glance-scrubber --daemonloops everywakeup_timeseconds (default 300,glance/scrubber.py:105,glance/scrubber.py:309); without--daemonit runs once (cron). - Purpose: with
delayed_delete = true(default false,glance/scrubber.py:74),DELETE /v2/images/{id}only moves the image topending_delete(glance/domain/__init__.py:249-255); the scrubber deletes the data later. - Data read: pages of
imageswithstatus = pending_delete(glance/scrubber.py:200-220), keeping those whosedeleted_at + scrub_timeis past (glance/scrubber.py:222-262). - Effects:
delete_from_backendper location,image_locations.status = deleted, thenimages.status = deleted(glance/scrubber.py:358-414), withscrub_pool_sizethreads (default 1). - Error behaviour: a store error is logged and the location is retried on the next run.
glance-scrubber --restore <id>puts apending_deleteimage back toactive(glance/scrubber.py:416); it refuses to run together with--daemon.
Source: reverse-spec-glance/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.