Skip to content

Commit bd41338

Browse files
authored
Thread race fix (#11)
- Guards for the client side (`WsStreamingDevice`); - Using `Device::removedNoLock()` to stop `boost::asio` executor thread (otherwise `Device::removed()` will create deadlock); Move data publishing from acquisition thread to `boost::asio executor` so `wss::detail::peer` processing will be handled only on one thread (`wss::detail::peer is not thread-save`); - own `local_signal` via `shared_ptr` (async `publish_data/set_metadata` posts hold it alive);
1 parent f0f2e58 commit bd41338

6 files changed

Lines changed: 70 additions & 31 deletions

File tree

shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming_device.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class WsStreamingDevice : public Device
8585
static PropertyObjectPtr createDefaultConfig();
8686

8787
void removed() override;
88+
void removedNoLock() override;
8889

8990
DeviceInfoPtr onGetInfo() override;
9091

shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming_listener.h

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
#pragma once
1818

19+
#include <memory>
20+
21+
#include <boost/asio/any_io_executor.hpp>
22+
1923
#include <opendaq/opendaq.h>
2024

2125
#include <ws-streaming/local_signal.hpp>
@@ -32,7 +36,8 @@ class WsStreamingListener
3236
WsStreamingListener(
3337
IContext *context,
3438
ISignal *signal,
35-
wss::local_signal *localSignal);
39+
std::shared_ptr<wss::local_signal> localSignal,
40+
boost::asio::any_io_executor executor);
3641

3742
~WsStreamingListener() override;
3843

@@ -50,11 +55,12 @@ class WsStreamingListener
5055

5156
private:
5257

53-
SignalPtr _signal;
54-
InputPortConfigPtr _port;
55-
DataDescriptorPtr _lastDescriptor;
56-
wss::local_signal& _localSignal;
57-
bool _ruleType;
58+
SignalPtr _signal;
59+
InputPortConfigPtr _port;
60+
DataDescriptorPtr _lastDescriptor;
61+
std::shared_ptr<wss::local_signal> _localSignal;
62+
boost::asio::any_io_executor _executor;
63+
bool _ruleType;
5864
};
5965

6066
END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING

shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming_server.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,12 @@ class WsStreamingServer : public Server
115115
const std::string& name,
116116
const wss::metadata& metadata,
117117
SignalPtr openDaqSignal)
118-
: localSignal(name, metadata)
118+
: localSignal(std::make_shared<wss::local_signal>(name, metadata))
119119
, openDaqSignal(openDaqSignal)
120120
{
121121
}
122122

123-
wss::local_signal localSignal;
123+
std::shared_ptr<wss::local_signal> localSignal;
124124
SignalPtr openDaqSignal;
125125
InputPortNotificationsPtr listener;
126126
SignalPtr domainSignal;

shared/libraries/websocket_streaming/src/ws_streaming_device.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,12 @@ PropertyObjectPtr WsStreamingDevice::createDefaultConfig()
8383
void WsStreamingDevice::removed()
8484
{
8585
streamingEvents.clear();
86+
}
8687

88+
void WsStreamingDevice::removedNoLock()
89+
{
8790
streaming.release();
88-
91+
auto lock = getRecursiveConfigLock2();
8992
Device::removed();
9093
}
9194

@@ -101,6 +104,9 @@ void WsStreamingDevice::onSignalAvailable(
101104
wss::remote_signal_ptr domainSignal,
102105
const DataDescriptorPtr& descriptor)
103106
{
107+
auto lock = getRecursiveConfigLock2();
108+
if (this->objPtr.template asPtr<IRemovable>(true).isRemoved() || !streaming.assigned())
109+
return;
104110
daq::MirroredSignalConfigPtr openDaqDomainSignal;
105111

106112
if (domainSignal)
@@ -134,6 +140,7 @@ void WsStreamingDevice::onSignalAvailable(
134140

135141
void WsStreamingDevice::onSignalUnavailable(wss::remote_signal_ptr signal)
136142
{
143+
auto lock = getRecursiveConfigLock2();
137144
auto it = streamingSignals.find(signal->id());
138145
if (it == streamingSignals.end())
139146
return;

shared/libraries/websocket_streaming/src/ws_streaming_listener.cpp

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616

1717
#include <cstdint>
18+
#include <utility>
19+
20+
#include <boost/asio/post.hpp>
1821

1922
#include <opendaq/opendaq.h>
2023

@@ -34,17 +37,22 @@ BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING
3437
WsStreamingListener::WsStreamingListener(
3538
IContext *context,
3639
ISignal *signal,
37-
wss::local_signal *localSignal)
40+
std::shared_ptr<wss::local_signal> localSignal,
41+
boost::asio::any_io_executor executor)
3842
: _signal(signal)
3943
, _port(
4044
InputPort(
4145
context,
4246
nullptr,
4347
String("ws-streaming")))
4448
, _lastDescriptor(_signal.getDescriptor())
45-
, _localSignal(*localSignal)
49+
, _localSignal(std::move(localSignal))
50+
, _executor(std::move(executor))
4651
{
47-
_localSignal.set_metadata(
52+
// The listener is constructed on the streaming endpoint's strand (inside the on_subscribed handler)
53+
// so touching _localSignal directly here is safe. All later access happens from
54+
// packetReceived() on the acquisition thread and is send to _executor instead.
55+
_localSignal->set_metadata(
4856
descriptorToMetadata(
4957
signal,
5058
_lastDescriptor));
@@ -109,22 +117,34 @@ void WsStreamingListener::onDataPacketReceived(DataPacketPtr packet)
109117

110118
auto descriptor = packet.getDataDescriptor();
111119

120+
// wss::local_signal/connection/peer are not thread-safe
121+
// packetReceived() runs on the acquisition thread
122+
// so marshal every local_signal call onto _executor
112123
if (descriptor != _lastDescriptor)
113124
{
114-
_localSignal.set_metadata(
115-
descriptorToMetadata(
116-
_signal,
117-
descriptor));
125+
auto metadata = descriptorToMetadata(_signal, descriptor);
126+
boost::asio::post(
127+
_executor,
128+
[localSignal = _localSignal, metadata = std::move(metadata)]()
129+
{
130+
localSignal->set_metadata(metadata);
131+
});
118132

119133
_lastDescriptor = descriptor;
120134
}
121135

122136
if (packet.getRawDataSize())
123-
_localSignal.publish_data(
124-
offset,
125-
packet.getSampleCount(),
126-
packet.getRawData(),
127-
packet.getRawDataSize());
137+
// Capture the packet by value so its raw buffer stays alive until the handler runs
138+
boost::asio::post(
139+
_executor,
140+
[localSignal = _localSignal, packet, offset]()
141+
{
142+
localSignal->publish_data(
143+
offset,
144+
packet.getSampleCount(),
145+
packet.getRawData(),
146+
packet.getRawDataSize());
147+
});
128148
}
129149

130150
void WsStreamingListener::onEventPacketReceived(EventPacketPtr packet)
@@ -138,10 +158,14 @@ void WsStreamingListener::onEventPacketReceived(EventPacketPtr packet)
138158

139159
if (valueDescriptorChanged && newValueDescriptor != _lastDescriptor)
140160
{
141-
_localSignal.set_metadata(
142-
descriptorToMetadata(
143-
_signal,
144-
newValueDescriptor));
161+
// Marshal onto the streaming endpoint's strand (see onDataPacketReceived).
162+
auto metadata = descriptorToMetadata(_signal, newValueDescriptor);
163+
boost::asio::post(
164+
_executor,
165+
[localSignal = _localSignal, metadata = std::move(metadata)]()
166+
{
167+
localSignal->set_metadata(metadata);
168+
});
145169

146170
_lastDescriptor = newValueDescriptor;
147171
}

shared/libraries/websocket_streaming/src/ws_streaming_server.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ void WsStreamingServer::createListener(const SignalPtr& signal)
208208

209209
if (domainSignal != it->second.domainSignal)
210210
{
211-
_server.remove_local_signal(it->second.localSignal);
211+
_server.remove_local_signal(*it->second.localSignal);
212212
_localSignals.erase(it);
213213
}
214214

@@ -230,7 +230,7 @@ void WsStreamingServer::createListener(const SignalPtr& signal)
230230

231231
streamableSignal.domainSignal = domainSignal;
232232

233-
streamableSignal.localSignal.on_subscribed.connect([
233+
streamableSignal.localSignal->on_subscribed.connect([
234234
=,
235235
&streamableSignal,
236236
signal_id = signal.getGlobalId().toStdString()
@@ -239,11 +239,12 @@ void WsStreamingServer::createListener(const SignalPtr& signal)
239239
streamableSignal.listener = createWithImplementation<IInputPortNotifications, WsStreamingListener>(
240240
this->template thisPtr<ComponentPtr>().getContext(),
241241
signal,
242-
&streamableSignal.localSignal);
242+
streamableSignal.localSignal,
243+
_server.executor());
243244
reinterpret_cast<WsStreamingListener *>(streamableSignal.listener.getObject())->start();
244245
});
245246

246-
streamableSignal.localSignal.on_unsubscribed.connect([
247+
streamableSignal.localSignal->on_unsubscribed.connect([
247248
=,
248249
&streamableSignal,
249250
signal_id = signal.getGlobalId().toStdString()
@@ -252,7 +253,7 @@ void WsStreamingServer::createListener(const SignalPtr& signal)
252253
streamableSignal.listener.release();
253254
});
254255

255-
_server.add_local_signal(streamableSignal.localSignal);
256+
_server.add_local_signal(*streamableSignal.localSignal);
256257
}
257258

258259
void WsStreamingServer::onClientConnected(
@@ -344,7 +345,7 @@ void WsStreamingServer::rescan()
344345
if (it->second.openDaqSignal.isRemoved())
345346
{
346347
auto jt = it++;
347-
_server.remove_local_signal(jt->second.localSignal);
348+
_server.remove_local_signal(*jt->second.localSignal);
348349
_localSignals.erase(jt);
349350
}
350351

0 commit comments

Comments
 (0)