Open kang-sw opened 2 years ago
Additionally, what do you think about replacing websocket::connection::userdata_
field as shared_ptr<void>
with custom destructor?
I think userdata assignment logic can be changed into shared_ptr<void>
implementation without breaking current user code. This lets user to notice connection instance expiration immediately, and make memory management much more simple. I believe there are a lot more advantages with it.
void userdata(void* u) { userdata_ = shared_ptr<void>(u, [](auto) {}); }
void userdata(shared_ptr<void> u) { userdata_ = move(u); }
void* userdata() { return userdata_.get(); }
std::shared_ptr<void> get_userdata() { return userdata_; }
private:
std::shared_ptr<void> userdata_;
Accept registration logic can be changed into:
std::function<bool(const crow::request&, shared_ptr<void>)> accept_handler_;
template<typename Func, class = std::enable_if_t<std::is_invocable_v<Func, crow::request, shared_ptr<void>*>>>
self_t& onaccept(Func f)
{
accept_handler_ = f;
return *this;
}
template<typename Func, class = std::enable_if_t<std::is_invocable_v<Func, crow::request, void**>>>
self_t& onaccept(Func f)
{
accept_handler_ = [f_org = std::move(f)](auto&& req, shared_ptr<void>* o) {
void* p = nullptr;
f_org(req, &o);
*o = shared_ptr<void>(p, [](auto){});
};
return *this;
}
I do basically the same. In my app I use a collection of std::unique_ptrs holding my allocated user_data. This way my instances are destroyed when the application shuts down without relying on onclose being called. In onclose I remove the instances from the collection, freeing associated memory in the process.
@kang-sw @MichaelSB Sorry for the wait. The updates somehow got lost in my inbox 😰
There are indeed numerous bugs in the websocket code.
First of all, as you pointed out, the connections don't get registered. Second, websocket::connection->close() doesn't really run the close handler when called manually, because has_recv_close_
is not set. Furthermore, App->stop()
is not called when the application is terminated via a signal.
I haven't been working with the websockets code at all during my short time with Crow. However, I can imagine those things are not that difficult to fix. I'll tackle this in the coming week or two.
If you want, you can open a PR yourself (I'll promise to look at it in a timely manner). Make sure to cover the points above and notify beforehand if you start working.
As to the userdata replacement. I'm not sure, this seems convenient if some cases, but a little more difficult to use in others. If I'm actually storing a pointer to some static data, I would have to use some nonetheless.
I agree on the shared_ptr. For me it would be worse. Also, when using it with some C-Api a shared_ptr would most likely be hard to implement.
@dranikpg Thanks to reply. Yet I'm not fully aware of all HTTP upgrade process, it would take some time to find appropriate fix myself.
@MichaelSB
And actually above userdata replacement won't affect existing user code who uses C-data API.
The code above I wrote exists for compatibility with plain pointer code.
void userdata(void* u) { userdata_ = shared_ptr<void>(u, [](auto) {}); }
This creates new shared pointer using custom desturctor which does nothing on destruction. You can freely use raw pointers without any extra work. (Even APIs will be compatible)
Of course it may introduce a bit overhead by creating redundant shared counter object internally, however, it would be trivial as much as keeping single std::string
instance.
I see... still I'm not convinced. shared_ptr gives the notion of something that is shared. In many cases nothing is shared. My userdata is unique, that's why I use unique_ptrs and give them as raw ptrs to userdata. shared_ptr have their use, but to me this looks like poor-mans-gc. And of course shared_ptr costs more than storing a string. It always allocates. std::string does not. And though it may not bother you, I try to avoid allocations and useless ref-counts whenever possible. I'm writing high-performance apps, that's why I use C++ and not some platform better suited for webdev. However, I have no voice here, just a guy giving his opinion on the matter :D
@MichaelSB I totally agree with your view, as this introduces unnecessary overhead in every use cases currently. However, the reason I prefer using shared_ptr
everywhere is not only for implement poor-mans-gc, but also tracking lifetime of the object using weak_ptr
.
In current model of websocket implementation, on every callback, websocket instance is delivered as raw c++ reference, and userdata is raw-c-pointer either. And if you hope to implement any server-side notification, you have to store connection's reference somewhere, and must manage its lifetime not to call send_data()
on already expired websocket instance. Of course it can be implemented in many clever ways using current userdata()
raw pointer, however, it is easy to become error-prone.
Using shared_ptr inherently, we can pretty easily track lifetime of that instance using weak_ptr.
// Main thread: onclose()
auto wp = std::weak_ptr<void>{conn.userdata_shared()};
conn.userdata_shared(nullptr); // Try to expire user data object field
// Wait until all concurrent access to userdata object to be finished.
while(not wp.expired()) { std::this_thread::yield(); }
// User thread: who utilizes userdata
weak_ptr<my_userdata_t> w_userdata;
...
if(auto p_userdata = w_userdata.lock()) {
// Okay, connection seems valid at this point.
p_userdata->get_conn().send_data("hello, world!");
// As above onclose() logic, it waits for all userdata usage finishes, thus it's always safe to access
// websocket connection instance under this scope. (as long as onclose() callback called in correct manner)
// (or, above userdata expiration join logic may be embedded inside websocket connection instance destructor)
} else {
// We can detect if connection is already expired.
}
Firsly, I really appreciate your amazing work. It literally changed my life :D.
And apologies for my short English skills as I don't natively use it.
The text below is logging from my application which is under development currently.
It seems active websockets does not receive
onclose()
callback call on application disposal.It causes problem as I usually dynamically allocate userdata object on
onaccept()
routine, to implement notification without explicit client request. (Holding connection references outside ofon*()
callback routines will make callingsend_text|send_binary()
at random timing available)Currently I'm releasing allocated memory from
onclose()
callback routine. However, if it's not properly called oncrow::App
's stop sequence, it results in memory leak.It seems there's already logic to handle it, however, it seems there are a few problems with it.
Crow::::websockets_
field automatically. Thus, user have to make call toadd_websocket()
andremove_websocket()
on their own websocket handler routine.websocket_
field is not properly protected with any mutex or critical section. Link toadd_websocket, remove_websocket
websocket_
access routines are made atomic, as they are invoked prior to actualhttp_server::stop()
call, new websocket connection can be made during existing websocket cleanup routines, which will makeadd_websocket
call, after copyingwebsocket_to_close
content.