torproject / tor Public
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ticket25573 v2 #270
Ticket25573 v2 #270
Conversation
We allow their CONNECTEDs, RESOLVEDs, ENDs, SENDMEs, and DATA cells to not count as dropped until the windows are empty, or we get an END. This commit does not change behavior. It only changes CIRC_BW event field values.
This commit does not change behavior. It only changes CIRC_BW field values.
Avoid data corrupton by avoiding mixing up old stream ids with new ones. This commit changes client behavior.
TRUNCATED cells were ignored while in path bias. Now they are obeyed, and cause us to tear down the circuit. The actual impact is minimal, since we would just wait around for a probe that would never arrive before. This commit changes client behavior.
This is my review of "Ticket #25573: Track half-closed stream ids". (There is a lot of code here, so I'll do a review for each group of similar commits.)
In general, I think this code would be simpler and more performant if it used a hash table, rather than a smartlist. Using a hash table would make it easier to check correctness, too.
src/or/connection_edge.c
Outdated
| half_conn->connected_pending = conn->base_.state == | ||
| AP_CONN_STATE_CONNECT_WAIT; | ||
|
|
||
| /* Data should only arrive if we're not waiting on a resolved. |
src/or/connection_edge.c
Outdated
| if (!half_conns) | ||
| return 0; | ||
|
|
||
| SMARTLIST_FOREACH_BEGIN(half_conns, half_edge_t *, half_conn) { |
How large do we expect half_conns to get?
Would it be better implemented as a hash table?
The half_conns list should be quite small, and is checked infrequently. Connections only get added when a client hangs up on a TCP connection early, before getting the END cell from the other side. This typically only happens in cases where the user interrupts client action by a cancel/stop/kill at the application layer.
It would take a very broken client to fill the half_closed list, and even if the half_closed connection table manages to get full, it will be at most 65535 uint16_t equality comparisons to go through it. And it will only be checked when using that same circuit for a new stream, or when getting stray data back with stale stream ids on that circuit.
I actually think that the additional memory allocation, indirection, indirect calls, and hashing overhead involved in a hash table will make it considerably more expensive to use for these small sizes, especially since we're only comparing int16_t values in the search.
All of this made me opt for the simple list for this (which is also what we use for our opened streams). The simple list also felt easier to check for lifetime and correctness issues. (Though the refactoring you suggested does make this even easier).
| /** | ||
| * Check if this stream_id is in a half-closed state. If so, | ||
| * check if it still has data cells pending, and decrement that | ||
| * window if so. |
Conceptually, these seem like separate functions:
- find the half-closed state for this stream
- check if it still has data cells pending
- decrement that window
You perform step 1 in all the connection_half_edge_is_valid_*() functions, and duplicate all that code. Consider using a macro or function pointer?
Maybe it's ok to duplicate the code as an optimisation. But using a hash table, the search code would be simpler.
Also, this function should be named after an action, not a question. Here's one possible name:
- connection_half_edge_reduce_data_window
I like the idea of factoring out the search into a separate function. I did this in 8257aab.
It seems safer to me for callers to use explicit named functions for each of the window checks, rather than making them pass in a pointer to an int or something. This way, caller misuse is more easily spotted, and we don't have to worry about dangling pointers.
In my mind, it makes sense to have these calls be questions rather than action statements. This is consistent with the philosophy I used for the patch. None of these functions perform any actions that alter behavior. They only tell the caller if the cell counts as valid data for the control port statistics, so that the caller can count it (or not).
(e08e36f has an additional fixup to make the search function take a const list).
src/or/connection_edge.c
Outdated
| */ | ||
| static void | ||
| connection_half_edge_add(edge_connection_t *conn, | ||
| origin_circuit_t *circ) |
This comment has been hidden.
This comment has been hidden.
Sorry, something went wrong.
src/or/connection_edge.c
Outdated
| */ | ||
| int | ||
| connection_half_edge_is_valid_data(smartlist_t *half_conns, | ||
| streamid_t stream_id) |
I don't know if you'll want to const smartlist_t *half_conns.
Technically, we don't modify it, but we do modify one of the items' attributes.
I guess this is consistent with the "these are verfication functions that don't alter behavior" notion. I made the containers const in b75fcb5
src/or/relay.c
Outdated
| if (connection_half_edge_is_valid_data(ocirc->half_streams, | ||
| rh.stream_id)) { | ||
| circuit_read_valid_data(TO_ORIGIN_CIRCUIT(circ), rh.length); | ||
| log_info(domain,"data cell valid on half-closed stream."); |
Ok logged both stream id and circ id in 85f86d0 for all of your comments below.
src/or/relay.c
Outdated
| rh.stream_id)) { | ||
|
|
||
| circuit_read_valid_data(TO_ORIGIN_CIRCUIT(circ), rh.length); | ||
| log_info(domain,"end cell (%s) valid on half-closed stream.", |
src/or/relay.c
Outdated
| if (connection_half_edge_is_valid_connected(ocirc->half_streams, | ||
| rh.stream_id)) { | ||
| circuit_read_valid_data(TO_ORIGIN_CIRCUIT(circ), rh.length); | ||
| log_info(domain,"connected cell valid on half-closed stream."); |
src/or/relay.c
Outdated
| if (connection_half_edge_is_valid_sendme(ocirc->half_streams, | ||
| rh.stream_id)) { | ||
| circuit_read_valid_data(TO_ORIGIN_CIRCUIT(circ), rh.length); | ||
| log_info(domain,"sendme cell valid on half-closed stream."); |
src/or/relay.c
Outdated
| if (connection_half_edge_is_valid_resolved(ocirc->half_streams, | ||
| rh.stream_id)) { | ||
| circuit_read_valid_data(TO_ORIGIN_CIRCUIT(circ), rh.length); | ||
| log_info(domain,"resolved cell valid on half-closed stream."); |
| @@ -154,18 +180,299 @@ test_circbw_relay(void *arg) | |||
| tt_int_op(circ->n_overhead_read_circ_bw, OP_EQ, overhead); \ | |||
| } while (0) | |||
|
|
|||
| static int | |||
| subtest_circbw_halfclosed(origin_circuit_t *circ, streamid_t init_id) | |||
This comment has been hidden.
This comment has been hidden.
Sorry, something went wrong.
Consider testing what happens when the stream ids wrap:
#270 (comment)
The duplicate issue is resolved by 7afb6f0.
@mikeperry-tor, do you test what happens when stream ids wrap? Do you think that's important now with all the other changes?
src/or/connection_edge.c
Outdated
| @@ -2767,6 +2767,15 @@ get_unique_stream_id_by_circ(origin_circuit_t *circ) | |||
| for (tmpconn = circ->p_streams; tmpconn; tmpconn=tmpconn->next_stream) | |||
| if (tmpconn->stream_id == test_stream_id) | |||
| goto again; | |||
|
|
|||
| if (circ->half_streams) { | |||
| SMARTLIST_FOREACH_BEGIN(circ->half_streams, half_edge_t *, half_conn) { | |||
This comment has been hidden.
This comment has been hidden.
Sorry, something went wrong.
This comment has been hidden.
This comment has been hidden.
Sorry, something went wrong.
This loop will only run when we open a new stream on a circuit that had lots of half-closed streams. I think this is unlikely, and even worst case is only 65535 uint16_t compares, as per above. (Also we chose to use a O(N) search on the opened connections linked list for the same reasons).
Ok, I decided to sort the list, so we can bsearch them on the common operations of cell arrival, and also guard against duplicate stream ids. I added tests for duplicates and list management correctness as well. I still think this is better than a hashtable, because for small list sizes, the memory allocation, hashing, and fragmentation overhead of a hashtable strikes me as not worth it.
Even on a full list of 65k half-closed connections (which should never happen), the slow operation (adding a new half-closed id, which triggers a sort), takes under 10ms on my 2015-era laptop. Since add only happens on EOF, even in this worst case, this will not directly affect user experience or page load.
Oh duh, we don't have a smartlist insert function to insert in order, but we have a bsearch to search for the index, and we can insert and delete at that index. I should be using that instead of sorting (and also instead of remove_keeporder). I will have a new commit for this in a bit.
Resolved cell typo.
Refactor search code out of functions.
Group window-related functions together, and move the two half-closed termination cases after them.
Header file typo/minor type fixes.
Log stream and circ ids of half-closed cells.
Make half-conn search take a const.
More consts. Technically I guess the container is const, even though we modify its elements' attributes.
It does not modify the actual list.
It should not be the case that this list gets large enough to matter, but sorting it also makes it easier to ensure against duplicate elements. A sorted list is preferable to a hashtable because this set should be very small. There should only be a handful of half-closed connections on a circuit, if that. This makes the memory allocation, memory fragmentation, and hashing overhead of the hash table not worth it.
Also use bsearch_idx to get the index to add/delete at.
Bah. Check for empty list in end.
…w one Use search function in get_unique_stream_id_by_circ().
…w one Test various types of streamid wrapping.
|
Closing this pull request in favor of #296. |
Broken into functional commits.
The text was updated successfully, but these errors were encountered: