WeblateOrg / WeblateOrg/weblate

Units created via the API immediately after component creation are silently deleted

Open
#20,995 0 comments 0 reactions 0 assignees View on GitHub
Area: API Area: Processing Waiting for: Demand
Dominant language
Python
Stars
6.1k
Forks
1.4k
Avg merge
9h 53m
Merged PRs (30d)
395

Description

### Describe the issue

Creating a translation unit through `POST /api/translations/{project}/{component}/{language}/units/` immediately after creating its parent component succeeds (`HTTP 200`, full unit body returned, confirmed by a follow-up `GET`), but the unit is then **silently deleted a few hundred milliseconds later** with no error surfaced anywhere.

The parent component's own post-creation initialization task chain (`component_after_save` → `perform_load`) reconciles the database against the on-disk files shortly after creation. Because the newly-created component's files are still empty at that point, and the unit created via the API is a pending row that was never serialized into a file, the reconciliation pass treats it as "stale" and deletes it. This is **not** a lost-update race between two writers — `add_unit()` does hold the component lock while writing — it's a race between that write and the component's own later, separately-locked initialization pass, which runs its reconciliation with `preserve_pending_units=False` by default.

No explicit `repository`/`commit` call is needed to trigger the deletion — it happens automatically as part of component setup, typically within ~100–200ms of component creation. Waiting roughly 15 seconds before creating the unit avoids the issue entirely.

### I already tried
- [x] I've read and searched the documentation.
- [x] I've searched for similar filed issues in this repository.

(Closest related issue I found: #13623, "Redesign component level locking," whose own checklist names "creating new strings (add, parsing files)" as a scenario not yet covered by that locking work.)

### Steps to reproduce the behavior

1. Create a project: `POST /api/projects/` with a `slug`.
2. Immediately create a component under it, e.g. `file_format: json`, `repo: "local:"`, `vcs: "local"`, `manage_units: true`.
3. Immediately (no delay) `POST` a unit: `POST /api/translations/{project}/{component}/{source_language}/units/` with a `key` and `value`.
4. Observe: `HTTP 200`, full unit body returned. A follow-up `GET` on the same units endpoint shows `count: 1` with the new unit present.
5. Poll the same units list every 1–2 seconds for 10–20 seconds, without calling any other endpoint.
6. Observe: within roughly 100ms–a few seconds, the unit disappears — `count: 0` — with no error response, webhook, or log entry visible to the API caller at any point.

Additional scope notes from testing:
- Inserting a ~15 second delay before step 3 makes the unit persist correctly.
- Bulk upload (`POST .../file/`) done immediately after component creation is **not** affected — units created that way land reliably even with zero delay.
- The same loss reproduces against a real git-backed component (not just `vcs: "local"`), though the vulnerable window is narrower and less deterministic there, since the clone/checkout step itself can consume most of the window before an external client's next request lands.

### Expected behavior

Either:
- Reject or defer unit creation while the component is still initializing (e.g. `409 Conflict` or `503` with `Retry-After`), or
- Expose a readiness indicator on the component resource so a client can poll before writing, or
- Have the reconciliation pass preserve pending units created during initialization instead of discarding them.

Silently returning `200` for a write that is then discarded with no trace is the part that needs fixing regardless of which direction is chosen.

### Screenshots

N/A — this is a pure API/backend timing issue with no UI surface. See "Additional context" for a server-log excerpt showing the deletion.

### Exception traceback

N/A — no exception is raised; the unit is deleted cleanly by normal reconciliation logic, which is exactly the problem (nothing signals that anything went wrong).

### How do you run Weblate?

Docker container

### Weblate version

2026.7.1 (also confirmed against the current `latest` Docker tag, which at the time of testing resolved to the same image)

### Weblate deploy checks

N/A — reproduced on a minimal, disposable single-node instance for isolation; not a production deployment.

### Additional context

Server log for the exact run above (component `bugdemo/c`, `vcs: local`), timestamps included — the unit was created at `07:32:25,875` (per the API response's own `timestamp` field, `2026-08-02T07:32:25.878448Z`) and deleted 130ms later, entirely on its own, before the first poll even landed:

```
[2026-08-02 07:32:25,804: INFO] bugdemo/c: Component created (admin)
[2026-08-02 07:32:25,844: INFO] Task weblate.trans.tasks.component_after_save[...] received
200, "pending": true, timestamp 07:32:25.878448Z>
[2026-08-02 07:32:25,976: INFO] bugdemo/c: checking l10n/en.json (en) [1/1]
[2026-08-02 07:32:25,995: INFO] bugdemo/c/en: processing l10n/en.json, new file, 0 strings
[2026-08-02 07:32:26,005: INFO] bugdemo/c/en: deleting 1 stale strings
[2026-08-02 07:32:26,013: INFO] bugdemo/c/en: updating completed

```

Same result against a real git-backed component (`vcs: "git"`, cloned from a live repo rather than `local:`) — narrower window, but reproduced on the first attempt:

```
[2026-08-02 07:33:52,446: INFO] bugdemo/cgit1: Component created (admin)
200, timestamp 07:33:52.482971Z>
[2026-08-02 07:33:52,568: INFO] bugdemo/cgit1: checking l10n/en.json (en) [1/1]
[2026-08-02 07:33:52,573: INFO] bugdemo/cgit1/en: processing l10n/en.json, new file, 0 strings
[2026-08-02 07:33:52,582: INFO] bugdemo/cgit1/en: deleting 1 stale strings
```

Both runs used `json` file format; I've also seen the same behavior with `properties`.

Related: #13623 covers component-level locking generally and its checklist names this exact "creating new strings" gap as unresolved work; none of the PRs that came out of that effort (#15248, #15250, #15277, #18747, #18759, #19671) add a guard between API-driven unit creation and the initialization reconciliation pass. Discussion #15594 (pending translations lost during repository sync) is a related but distinct report worth cross-referencing — different alleged trigger, same general "destructive reconciliation of pending work" pattern.

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the immediate POST /api/translations/{project}/{component}/{language}/units/ sequence and trace the component_after_save → perform_load task chain. Inspect add_unit() and the preserve_pending_units=False reconciliation path; done means a unit created during initialization is not silently deleted, with regression coverage for the reported timing and initialization behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.