Interactions with other components — placement — branch stable/2026.2
Branch
stable/2026.2@f4a89d3803cb— repository https://opendev.org/openstack/placement — analysed on 2026-10-28
Placement exchanges with only two systems:
- its HTTP clients (Nova, Neutron, Cyborg, administrators), which call it;
- its SQL database.
Keystone token validation is done by the auth_token middleware. There is no RabbitMQ, no notification, no Redis or memcached, and no outbound REST call in the code:
oslo_messaging,redis,pymemcacheandkeystoneauth1are not imported outside tests;requestsonly appears inplacement/direct.py, an in-process client used by tests and by tools that embed Placement.
Inbound HTTP (REST)
| Item | Value | Source |
|---|---|---|
| Transport | HTTP/1.1 through an external WSGI server (uwsgi or mod_wsgi). Port 8778 is the usual one [inferred: devstack and deployment convention], often behind the /placement prefix | placement/wsgi/api.py |
| Authentication | X-Auth-Token, validated by keystonemiddleware, except GET / | placement/auth.py:93-116 |
| Version | OpenStack-API-Version: placement 1.x in the request and the response; the response also carries Vary: OpenStack-API-Version [inferred: microversion-parse behaviour] | microversion-parse, placement/deploy.py:58-60 |
| Correlation | X-Openstack-Request-Id, generated by oslo_middleware.request_id and returned in errors (request_id); an ID supplied by the client is reused [inferred] | placement/util.py (json_error_formatter) |
| Content | application/json, enforced on writes (415 otherwise) and on reads through Accept (406 otherwise) | placement/util.py |
| Caching | Last-Modified and Cache-Control: no-cache from 1.15 | handlers |
| Access log | one line per request: method, path, status, size, duration, microversion | placement/requestlog.py |
Sample frame [reconstructed]: an allocation made by Nova after scheduling.
PUT /placement/allocations/8d3b2c6e-0f7e-4b5c-9a3e-2f1c6c1a7b10 HTTP/1.1
Host: controller:8778
X-Auth-Token: gAAAAAB…
OpenStack-API-Version: placement 1.39
Content-Type: application/json
X-Openstack-Request-Id: req-2c9…
{"allocations": {"4e8e5957-…": {"resources": {"VCPU": 2, "MEMORY_MB": 4096, "DISK_GB": 20}}},
"project_id": "c1f…", "user_id": "a9e…", "consumer_generation": null, "consumer_type": "INSTANCE"}
HTTP/1.1 204 No Content
OpenStack-API-Version: placement 1.39
Vary: OpenStack-API-Version
X-Openstack-Request-Id: req-2c9…
Keystone (indirect)
keystonemiddleware.auth_token validates each token against Keystone, with GET /v3/auth/tokens using Placement's service token, then caches it. The cache is in memory by default, or in memcached when [keystone_authtoken]/memcached_servers is set. All of this is configured in the [keystone_authtoken] section of placement.conf. It is the only outbound traffic of the process. It does not appear in Placement's code, only in the middleware wiring (placement/auth.py:93-116).
The token's scope (project or system) is checked against each policy's scope_types. On this branch, oslo.policy ≥ 6.0.0 is required (requirements.txt:19), and that version no longer lets an operator disable scope checks (see change history, change 6).
Database (SQLAlchemy, oslo.db)
- Connection:
[placement_database]/connection, for examplemysql+pymysql://placement:…@db/placement. Also tunable:slave_connection(read-only), the pool (max_pool_size,max_overflow,pool_timeout), connection retries (max_retries= 10,retry_interval= 10 s) andmysql_sql_mode=TRADITIONAL(placement/conf/database.py:59-109). - Transactions:
enginefacade.transaction_context(), through theplacement_context_manager.readerand.writerdecorators (placement/db_api.py:24-40). Nested transactions join the enclosing one.reader.independentopens a separate transaction, used to re-read providers during allocation retries (placement/objects/allocation.py:579-582). - Schema: Alembic migrations in
placement/db/sqlalchemy/alembic/versions/(5 files), applied byplacement-manage db sync. There are two online data migrations:set_root_provider_idsandcreate_incomplete_consumers(placement/cmd/manage.py:31-34). No migration was added on this branch since15.0.0(see 06).
Tables (placement/db/sqlalchemy/models.py)
| Table | Role | Notable keys and constraints | Written by | Read by |
|---|---|---|---|---|
resource_providers | providers, tree (parent_provider_id, root_provider_id), **generation** | unique uuid and name (placement/db/sqlalchemy/models.py:50-73) | /resource_providers, and every write related to the provider (generation) | almost everything |
inventories | capacity per (provider, class): total, reserved, min_unit, max_unit, step_size, allocation_ratio | unique (provider, class) (placement/db/sqlalchemy/models.py:76-101) | /inventories, reshaper | allocations, candidates, usages |
allocations | reservations: (provider, class, consumer_id = UUID, used) | indexes (provider, class, used) and consumer_id (placement/db/sqlalchemy/models.py:104-126) | /allocations, reshaper | candidates (capacity), usages |
consumers | consumer: uuid, project_id, user_id, **generation**, consumer_type_id | unique uuid (placement/db/sqlalchemy/models.py:212-230) | /allocations | /allocations, /usages |
projects, users | mapping between the Keystone ID (external_id) and the internal ID | unique external_id (placement/db/sqlalchemy/models.py:182-209) | created on the fly by /allocations | /usages |
consumer_types | INSTANCE, MIGRATION… | unique name (placement/db/sqlalchemy/models.py:233-239) | created on the fly (1.38+) | /usages |
resource_classes | custom classes, and standard ones copied from os-resource-classes | unique name (placement/db/sqlalchemy/models.py:38-47) | /resource_classes, startup | everywhere, through the per-request cache |
traits, resource_provider_traits | traits and their association with providers | unique traits.name (placement/db/sqlalchemy/models.py:153-179) | /traits, startup | trait filters |
placement_aggregates, resource_provider_aggregates | aggregates and their association | unique uuid (placement/db/sqlalchemy/models.py:129-150) | /aggregates | member_of filters, sharing providers |
Optimistic locking: the generation [reconstructed from the ORM]
Every write that touches a provider ends with a "compare-and-swap" on its generation (placement/objects/resource_provider.py:669-688):
UPDATE resource_providers SET generation = :n_plus_1
WHERE id = :rp_id AND generation = :n;
-- 0 rows modified → ResourceProviderConcurrentUpdateDetected → rollback of the whole transaction
The consumer follows the same model (placement/objects/consumer.py:221-240). No SELECT … FOR UPDATE is used: consistency rests entirely on these conditional updates and on uniqueness constraints.
Writing allocations [reconstructed] (placement/objects/allocation.py:372-461)
-- in one transaction (automatic retry on deadlock, up to 5 times)
SELECT resource_provider_id, resource_class_id, SUM(used) FROM allocations
WHERE resource_provider_id IN (:rps) AND resource_class_id IN (:rcs) GROUP BY 1,2; -- usage before the operation
DELETE FROM allocations WHERE consumer_id = :consumer_uuid; -- full replacement
SELECT rp.id, rp.uuid, rp.generation, inv.resource_class_id, inv.total, inv.reserved,
inv.allocation_ratio, inv.min_unit, inv.max_unit, inv.step_size, usage.used
FROM resource_providers rp
JOIN inventories inv ON rp.id = inv.resource_provider_id AND inv.resource_class_id IN (:rcs)
LEFT JOIN (SELECT resource_provider_id, resource_class_id, SUM(used) AS used FROM allocations
WHERE resource_provider_id IN (:rps) AND resource_class_id IN (:rcs) GROUP BY 1,2) usage
ON usage.resource_provider_id = inv.resource_provider_id
AND usage.resource_class_id = inv.resource_class_id
WHERE rp.id IN (:rps); -- capacity check
INSERT INTO allocations (resource_provider_id, resource_class_id, consumer_id, used) VALUES (…);
UPDATE resource_providers SET generation = generation + 1 WHERE id = :rp AND generation = :g; -- for each provider
UPDATE consumers SET generation = generation + 1 WHERE id = :c AND generation = :cg; -- for each consumer
DELETE FROM consumers WHERE uuid IN (:consumers_without_allocations) …; -- cleanup
COMMIT;
On a provider generation conflict, replace_all re-reads the providers in an independent transaction, then replays _set_allocations, up to [placement]/allocation_conflict_retry_count times (placement/objects/allocation.py:544-596). When the write itself runs inside the handler's enclosing transaction (placement/handlers/allocation.py:454-461), the retry happens within that same transaction [inferred, not verified at run time].
Capacity search for candidates [reconstructed] (placement/objects/research_context.py:421-501)
For each requested class:
SELECT rp.id, rp.root_provider_id FROM resource_providers rp
JOIN inventories inv ON inv.resource_provider_id = rp.id AND inv.resource_class_id = :rc
LEFT JOIN (SELECT resource_provider_id, SUM(used) AS used FROM allocations
WHERE resource_class_id = :rc GROUP BY resource_provider_id) u
ON u.resource_provider_id = rp.id
WHERE COALESCE(u.used, 0) + :amount <= (inv.total - inv.reserved) * inv.allocation_ratio
AND inv.min_unit <= :amount AND inv.max_unit >= :amount AND MOD(:amount, inv.step_size) = 0;
These queries are followed by trait filters (resource_provider_traits) and aggregate filters (resource_provider_aggregates), then by the computation of usages per tree (get_usages_by_provider_trees, placement/objects/research_context.py:1319). The per-tree overall-capacity pre-check added by the fix for bug 2160721 works in memory on those usages; it adds no SQL query [inferred from placement/objects/allocation_candidate.py:794-844].
Cost
- One allocation: 4 to 6 queries and one transaction, whatever the number of providers.
- **
GET /allocation_candidates: one query per class and per group, plus the filters, plus the **in-memory Cartesian product. This is the hot spot of the service. - Per-request cache:
ResourceClassCacheandConsumerTypeCachelive in each request's context (placement/context.py:26-27). They reload the wholeresource_classestable on first use, then on each unknown name (placement/attribute_cache.py). The cost is small but repeated on every request: there is no cache shared between requests.
What does not exist
| Kind of exchange | Present? | Note |
|---|---|---|
| RabbitMQ / RPC / notifications | no | clients must poll the API; no event is published when a capacity changes |
| Redis / memcached (application code) | no | only keystonemiddleware may use memcached for its token cache |
| Outbound REST | no | Placement knows neither Nova nor Neutron |
| Files, external processes | no | apart from configuration and logs |
Source: reverse-spec-placement/03-interactions_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.