{"record":{"id":"8e4d3f75cb1a2103","repo":"chroma-core/chroma","slug":"conditional-transaction-is-closed-8e4d3f","errorCode":null,"errorMessage":"conditional transaction is closed","messagePattern":"conditional transaction is closed","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"chromadb/api/conditional_http.py","lineNumber":219,"sourceCode":"            \"operations\": self._operations.copy(),\n        }\n\n    def prepare_commit_payload(\n        self,\n    ) -> Optional[Tuple[ConditionalHttpScope, ConditionalHttpJsonPayload]]:\n        prepared_commit = self.prepare_commit()\n        if prepared_commit is None:\n            return None\n        scope = self._require_scope()\n        return (scope, prepared_commit)\n\n    def close(self, first_inserted_record_offset: Optional[int] = None) -> None:\n        self._ensure_open()\n        self._closed = True\n\n    def _ensure_open(self) -> None:\n        if self._closed:\n            raise ValueError(\"conditional transaction is closed\")\n\n    def _record_scope(\n        self, collection_id: UUID, tenant: str, database: str\n    ) -> ConditionalHttpScope:\n        scope = ConditionalHttpScope(str(collection_id), tenant, database)\n        if self._scope is None:\n            self._scope = scope\n        elif self._scope != scope:\n            raise ValueError(\"conditional transaction cannot span collections\")\n        return scope\n\n    def _require_scope(self) -> ConditionalHttpScope:\n        if self._scope is None:\n            raise ValueError(\"conditional transaction has no collection scope\")\n        return self._scope\n\n    def _validate_get_request(self, request_payload: ConditionalHttpGetPayload) -> None:\n        ids = request_payload.get(\"ids\")","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/chroma-core/chroma/blob/aecdd12c8a891610db8653630b066b32ceb678b5/chromadb/api/conditional_http.py#L201-L237","documentation":"This ValueError comes from ConditionalHttpTransaction._ensure_open in chromadb/api/conditional_http.py: the conditional-transaction object has already been closed (its close() was called, typically after commit) and any further operation — prepare_get, record_get, buffer_add/update/delete/upsert, prepare_commit — raises 'conditional transaction is closed'. It is the Chroma equivalent of using a DB transaction after commit/rollback: the transaction is single-use.","triggerScenarios":"Using the transaction API (client-style conditional transactions over HTTP) and calling any buffered operation after the transaction has been committed and closed, or after an explicit close(first_inserted_record_offset=...). Typical code: reuse of a stored transaction object across requests, or a loop that commits inside the body and keeps writing afterwards.","commonSituations":"Long-lived application objects holding a transaction handle across requests; retry wrappers that replay operations onto an already-committed transaction; frameworks where a per-client singleton transaction is accidentally shared; misunderstanding that close() finalizes the transaction permanently.","solutions":["Create a NEW ConditionalHttpTransaction (via the client's transaction entry point) for each unit of work instead of reusing the closed one","Track committed/closed state in your wrapper and refuse or reopen (new transaction) on subsequent writes","Ensure retry logic acquires a fresh transaction after a commit failure or success, rather than replaying onto the same object"],"exampleFix":"# before\ntx = client.create_conditional_transaction()\ntx_add(tx, ids=[...])\nclient.commit(tx)  # closes tx\ntx_add(tx, ids=['more'])  # ValueError: conditional transaction is closed\n\n# after\ntx = client.create_conditional_transaction()\ntx_add(tx, ids=[...])\nclient.commit(tx)  # tx closed here\n\ntx = client.create_conditional_transaction()  # fresh transaction per unit of work\ntx_add(tx, ids=['more'])\nclient.commit(tx)","handlingStrategy":"validation","validationCode":"class TxnHandle:\n    def __init__(self, client):\n        self.client = client\n        self._tx = None\n\n    def begin(self):\n        self._tx = self.client.create_conditional_transaction()  # or equivalent entry point\n        return self._tx\n\n    @property\n    def tx(self):\n        if self._tx is None or self._tx._closed:\n            self.begin()  # fresh transaction; never reuse a closed one\n        return self._tx","typeGuard":"def txn_is_open(tx) -> bool:\n    return getattr(tx, '_closed', False) is False","tryCatchPattern":"try:\n    buffered_op(tx, ...)\nexcept ValueError as e:\n    if 'conditional transaction is closed' in str(e):\n        tx = client.create_conditional_transaction()  # start a new unit of work\n        buffered_op(tx, ...)\n    else:\n        raise","preventionTips":["Treat a conditional transaction as single-use: one begin -> ops -> commit cycle","Wrap the transaction in a context manager that nulls the handle on commit/close","Make retry logic acquire a fresh transaction instead of replaying onto the old object","Do not cache transaction handles in long-lived singletons"],"tags":["chroma","transaction","conditional-http","state-management","client-api"],"backgroundTag":"transaction-already-closed","analyzedSha":"aecdd12c8a891610db8653630b066b32ceb678b5","analyzedAt":"2026-08-16T21:53:27.228Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}