Skip to content

OBS mis-pairs batch results under Parallel execution (upstream bug) #16

Description

@Agash

Under RequestBatchExecutionType.Parallel, OBS pairs each result's requestType/requestId with a different request's responseData/requestStatus. No client can correct this: the association is already lost when the response is built. Serial execution is unaffected.

This issue is for tracking on our side. The fix belongs in obs-websocket; the analysis below is written so it can be handed over as-is.

Root cause

src/requesthandler/RequestBatchHandler.cpp collects parallel results in completion order. Each thread-pool task appends as it finishes:

for (auto &request : requests) {
    threadPool.start(Utils::Compat::CreateFunctionRunnable([&parallelResults, &request]() {
        RequestResult requestResult = parallelResults.requestHandler.ProcessRequest(request);

        std::unique_lock<std::mutex> lock(parallelResults.conditionMutex);
        parallelResults.results.push_back(requestResult);   // completion order
        lock.unlock();
        parallelResults.condition.notify_one();
    }));
}

src/websocketserver/WebSocketServer_Protocol.cpp then zips that vector against the original submission-order request array, by index:

size_t i = 0;
for (auto &requestResult : resultsVector) {
    results.push_back(ConstructRequestResult(requestResult, requests[i]));
    i++;
}

ConstructRequestResult takes the identity from one and the outcome from the other:

ret["requestType"]   = requestJson["requestType"];      // submission order
ret["requestId"]     = requestJson["requestId"];        // submission order
ret["requestStatus"] = { ... requestResult ... };       // completion order
ret["responseData"]  = requestResult.ResponseData;      // completion order

While the two orders differ, every result is mislabelled. The serial paths build their vector in submission order, so they line up.

Note requestStatus is displaced along with responseData, so a parallel batch can report a failure against a request that actually succeeded.

Observed

Raw bytes off the wire for a parallel batch of GetVersion, GetSceneItemList, GetStats:

"requestType":"GetVersion",       "responseData":{"sceneItems"...
"requestType":"GetSceneItemList", "responseData":{"activeFps"...
"requestType":"GetStats",         "responseData":{"availableRequests"...

Every serial batch in the same run pairs correctly.

Suggested fix

Preserve submission order in the parallel branch: size the results vector up front and have each task write to its own slot, rather than appending. The existing requests[i] zip in WebSocketServer_Protocol.cpp then needs no change, and requestStatus is fixed at the same time.

parallelResults.results.resize(requests.size());

for (size_t index = 0; index < requests.size(); index++) {
    auto &request = requests[index];
    threadPool.start(Utils::Compat::CreateFunctionRunnable([&parallelResults, &request, index]() {
        RequestResult requestResult = parallelResults.requestHandler.ProcessRequest(request);

        std::unique_lock<std::mutex> lock(parallelResults.conditionMutex);
        parallelResults.results[index] = requestResult;   // submission order
        parallelResults.completed++;
        lock.unlock();
        parallelResults.condition.notify_one();
    }));
}

The wait condition currently uses results.size() == requestCount, which no longer works once the vector is pre-sized, so it needs a separate completion counter.

Why this cannot be worked around client-side

  • Nothing in the response reveals the completion order, so the permutation is not recoverable.
  • inputVariables/outputVariables would allow a marker, but are rejected in parallel mode.
  • responseData is built by each request handler and has no passthrough field to stamp an id into.
  • Matching payloads by shape fails: 75 of 147 requests return no payload at all, and five groups of requests share identical response shapes (seven of them return just outputActive). It also cannot recover requestStatus.

Client behaviour meanwhile

BatchResults.Get and TryGet refuse to resolve a reference on a parallel batch and explain why, rather than returning another request's data. Raw remains available.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions