Skip to content

Hi. I want to ignore a participant.  #78

Description

@qqqshengwei

Hi. I want to ignore a participant. I wonder where I can create an ignoreList and update it during the interaction between pubs and subs? By reading DDS Spec. (2.2.2.2.1.14). I found it as an irreversible operation. I hope that you can give me some implementation ideas.

Activity

  1. eboasson commented on Dec 25, 2018

    @eboasson
    Contributor

    Hi @qqqshengwei, in my reading of the specification, the specification doesn't have any conception of something larger than a participant and therefore all participants are 100% independent of each other. (Maybe I'm confusing you here — sorry! — just trying to explain how I look at this because I think it is relevant. The "larger" thing in Cyclone is the idea that you can have a single DDSI stack that handles multiple participants simultaneously while doing discovery only once.)

    In the spec's conception of this, it is therefore a very simple operation: when you call the ignore operation, you do a bunch of things:

    • you add the (remote) participant GUID to some ignore list in the participant;
    • you delete the proxy for the remote participant, which automatically gets rid of all readers/writers;
    • you ignore any SPDP message that arrives from a participant in the list.
      I think you are also allowed to ignore yourself. The above would work even in that case if you implement communication between readers and writers in a single participant on top of the network stack (there are some decent arguments for this and Cyclone has done it that way for some time — that was before the code was opened up — but no longer does so because I don't think the trade-off is the best one. That detail even came up in an earlier discussion: Userdata Qos usecase #54 (comment).)

    I think the irreversibility of the operation is either to give implementors more freedom, or it is caused by the use of an instance handle for the remote participant instead of a GUID. The spec doesn't guarantee that instance handles can't be reused, and so after ignoring the participant there is no longer a way to refer to it anymore. Clearly, in the above implementation, undoing it would be trivial if the GUID is known: just remove the GUID from the ignore list and you're done.

    You would only run into one problem, and that is that the participant being ignored doesn't "know" it is being ignored, and so it would expect responses from the readers and writers that it had already discovered. Cyclone is not really good at handling that kind of problem at the moment ... (It is actually one of the easier things to do deal with, because it means deciding that some remote entity has lost liveliness if it hasn't responded for too long.)

    But ... Cyclone doesn't do local communications via the network and is conceived to handle multiple participants simultaneously ...

    The easy option, and a quite reasonable one at that, is to not care about the multiple participants bit, and have a "global" ignore list. In that case the above works for remote participants. For local readers/writers you would have to check whether a match is allowed for new entities (connect_writer_with_reader) and for the initial dropping of the connections you'd have to run over them and do something along the lines of the loop in gc_delete_reader.

    That said: I have a question in return. There is this old issue: #8, which is specifically about filtering out the data the participant sent itself. It goes without saying that filtering after reading is rather inefficient ... but the direction I hinted at there of doing content filtering on the writer GUID and then pushing this content filter as far to the writer as possible very much has my preference.

    In the case of a content filter on the writer GUID, I would then drop the link from the writer to the reader doesn't want the data from that writer, and try to propagate that knowledge all the way to writer so that it can stop sending data if no other readers are left. That way, there is no crazy asymmetry in the discovery and no need for A to assume B has started ignoring A based on timeouts. I think that is more complicated, but also more elegant, approach.

    So the return question is: are you asking because you want to ignore your own data? If that is all you want to achieve, then the content filter bit would actually not be so hard to do, as it could be done set at creation time of the readers and just be taken into account in the initial matching. Pretty much like any other RxO QoS.

  2. qqqshengwei commented on Dec 28, 2018

    @qqqshengwei
    Author

    Thank you for your generous answer. I found the useful method participant_instance_id to get a participant handle by its GUID. However, I am confused about how to get GUID of a participant by its instance handle. It is really a good idea to create a const "global" ignore list (I think it should stay in entity.c) and a guard method like is_ignored_participant_guid in handle_SPDP_alive. I apologize for my late reply.

  3. eboasson commented on Dec 29, 2018

    @eboasson
    Contributor

    There is the function dds_get_instance_handle to get the instance handle for an entity, it forwards to participant_instance_id. You could use that instance handle, pass it to the dds_read_instance function in a data reader for the DCPSParticipant topic, and you'd end up with a sample that describes the participant, and the key value in this sample is the GUID.

    There is no really easy way to do the above because there is no hash table that maps entity ids to entities (only hash tables that map GUIDs to entity ids ...). You could create a function to do the lookup and leverage the new-ish built-in topic support. You'd basically do something very similar to the current dds_instance_get_key. (That is not to say that the code for dds_instance_get_key is anywhere near correct, because it should inspect the history cache of the reader/writer that's passed in and look in there instead of the current code that can cause all kinds of trouble ...)

    Basically you'd end up with something like:

    dds_get_entity_guid (dds_builtintopic_guid_t *out_guid, dds_instance_handle_t ih)
    {
      ret
      if ((tk = ddsi_tkmap_find_by_id(gv.m_tkmap, ih)) == NULL) {
        ret = NOT_FOUND;
      } else {
        if (tk->m_sample->ops != &ddsi_serdata_ops_builtin) {
         ret = NOT_FOUND; /* not associated with a built-in topic */
       } else {
          struct ddsi_serdata_builtin *d = tk->m_sample;
          nn_guid_t tmp = nn_hton_guid (d->guid);
          memcpy (out_guid, &tmp, sizeof (tmp));
          ret = OK;
        }
        ddsi_tkmap_instance_unref (tk);
      }
      return ret;
    }

    Regarding the ignore list and adding a is_ignored_participant_guid would be a sensible step to begin with. Least effort to get the result you need and that without causing trouble in the future 😁.

  4. qqqshengwei commented on Jan 1, 2019

    @qqqshengwei
    Author

    Happy New Year.
    I realized dds_get_entity_guid in dds_entity.c with something like:

    dds_return_t
    dds_get_entity_guid (
            _Out_   dds_builtintopic_guid_t *guid,
           _In_    dds_instance_handle_t *ihdl)
    {
        dds_return_t ret;
        struct thread_state1 * const thr = lookup_thread_state();
        const bool asleep = !vtime_awake_p(thr->vtime);
        struct ddsi_tkmap_instance *tk;
        if (asleep) {
            thread_state_awake(thr);
        }
        if ((tk = ddsi_tkmap_find_by_id(gv.m_tkmap, ihdl)) == NULL) {
            DDS_ERROR("No instance related with the provided handle is found\n");
            ret = DDS_ERRNO(DDS_RETCODE_PRECONDITION_NOT_MET);
        } else {
            if(tk->m_sample->ops !=&ddsi_serdata_ops_builtin){
                DDS_ERROR("Not associated with a built-in topic.\n");
                ret = DDS_ERRNO(DDS_RETCODE_PRECONDITION_NOT_MET);
            } else {
                struct ddsi_serdata_builtin *d = tk->m_sample;
                nn_guid_t tmp=d->key;
                memcpy(guid,&tmp, sizeof(tmp));
                ret=DDS_RETCODE_OK;
            }
            ddsi_tkmap_instance_unref (tk);
        }
        if (asleep) {
            thread_state_asleep(thr);
        }
        return ret;
    }
    

    I defined a declaration in dds.h with something like:
    _Pre_satisfies_(entity & DDS_ENTITY_KIND_MASK) DDS_EXPORT dds_return_t dds_get_entity_guid ( _Out_ dds_builtintopic_guid_t *guid, _In_ dds_instance_handle_t *ihdl);
    and tried to test this method in ping/pong. However I got nothing. I think the struct ddsi_serdata_builtin and 'tk->m_sample' are not the same. Do you mean the nn_guid_t key of ddsi_serdata_builtin is the expected GUID?Where can I get the right ddsi_serdata_builtin?

  5. eboasson commented on Jan 1, 2019

    @eboasson
    Contributor

    Happy New Year to you too :)

    There are a couple of details, including a mistake I made in generating instance handles for participants — I guess I gave to much attention to the instance handles of endpoints ... oops!

    • That mistake is the main problem. The entity creation code avoids generating entries in the key-value to instance-id map (tkmap) for entity kinds defined as "built-in" kinds in the DDSI spec. I did that because I did not want to pollute the built-in topics or tkmap with the various built-in endpoints for discovery, but I overlooked that the participant id is also classified as a "built-in". Oops. The result is that a different iid got allocated for the DCPSParticipant instance and so when you do a look up, there is nothing to be found. (Fixed it just now, so be sure to pull the latest change in.) [1]
    • The second issue is that the instance handle (a.k.a. instance id, a.k.a. iid) is just a 64-bit integer, so you have a type error passing ihdl (a pointer to an instance handle) to ddsi_tkmap_find_by_id. Chances that it will be found are vanishingly small (but not exactly 0).
    • Then there is a byte-order problem on little-endian machines. The nn_hton_guid function converts from GUIDs-as-uint32_t-arrays to GUIDs-as-byte-arrays, but that involves a byteswap on little-endian machines. The byteswap is really only needed for the entity id interpretation, as everything else is completely meaningless, but to be consistent it swaps all 4 32-bit words. Without that conversion, you'll get a messed-up GUID.

    With those changes, it works for me. Next time, I should perhaps do that simple check before I put some code in a comment 😁. That said, I am wondering if dds_get_entity_guid is the best name after all: the other "get" operations on entities operate strictly on local entities (like dds_get_instance_handle), and this is based on full discovery information, including the local entities. Perhaps reflecting that distinction would be a good idea ... "lookup" instead of "get" might do that trick?

    However I got nothing. I think the struct ddsi_serdata_builtin and 'tk->m_sample' are not the same

    Actually, they do match nicely. Eachtk->m_sample is just a ddsi_serdata which can be of any "topic class", for want of a better word, but if you know which exact one you are looking for, you can check the tk->m_sample->ops pointer (this being the equivalent of a vtable). "Normal" topics use a different set of operations, typically ddsi_serdata_ops_cdr. So that test is sufficient.

    Finally, in my mailbox I had an earlier version of your comment, with the entirely reasonable question:

    Is it because I forget to judge vtime_awake_p(thr->vtime)? Should I do this and why ?

    I see you've fixed it, and exactly as I would have suggested. But that doesn't necessarily mean the "why" part is properly answered. It is quite possible that you have figured it out, but I'll answer the question anyway.

    The background is that the DDSI core tries to satisfy three of my desiderata: (1) minimal reference counting (I hate having reference counts all over the place, in particular because of the overhead and risk of manipulating them all the time, but of course there are times when it is the best solution); (2) no "big kernel lock" serialising everything; and (3) being able to delete entities safely.

    So instead of reference counting (or at least, manipulating reference counts on entities for each read/write/take operation), there is a "rule" that says you may only obtain a pointer to an entity by doing a lookup of its GUID while awake, and that pointer is then only valid for as long as you stay awake. After that, it may be garbage collected. [2] (The garbage collector in q_gc.c is invoked explicitly, all it does is defer things until all threads are, or have been, asleep since queueing an entry for collection. A bit like the RCU mechanism in the Linux kernel, actually.) The result is that the vast majority of code can use pointers to entities without any complications.

    The tkmap reuses the mechanism. It too uses a concurrent hash table with lock-free lookups, and so has a big problem when it needs to resize or delete entries (this the sort of thing that's easy when you have a "real" garbage collector). Resizing means allocating a new, larger array of buckets, but freeing the old one must be deferred until no thread can still access the old one; deleting an entry means removing it from the hash table (covered by the concurrent hash table), and then ... what? Memory must be freed, but that again must be deferred. So the same rule applies to tkmap_instance.

    The vtime_awake_p(thr->vtime) assertion is therefore simply a partial check that this rule is adhered to. The reason for the if (asleep) by the way is that the listeners currently get invoked while awake, so it is not safe to unconditionally toggle thread's state. Certainly for some of the listeners this can be solved by change the precise time at which they're invoked, and so I do intend to have a good look at that and see if I can defer all listener invocations until the thread's no longer "awake" in this sense. Then, all these tests can go. If, on the other hand, it proves infeasible to make that change, I'll probable change the functions to change the state so that it has a counter. Then those tests can disappear everywhere else.

    As a side benefit, this toggling of awake/asleep states means one can have another thread that is pretty much completely independent of all the others and monitors whether the other threads do not stay awake for too long — so an internal detection mechanism for deadlocks. (See q_servicelease.c for that.)

    [1] Note that iids are unique in the system, you basically never run out of 64-bit integers. So it is 100% certain you will not be able to find the participant's instance id without my fix ...
    [2] There are ways around it, look at deleting a proxy writer if you want to get the gory details.

  6. qqqshengwei commented on Jan 6, 2019

    @qqqshengwei
    Author

    Yes, dds_lookup_entity_guid is much better and I have already changed it. In pong.c, I used info[j].instance_handle as the _In_ dds_instance_handle_t ihdl of this method. However, I always get Not associated with a built-in topic. I have no ideas about how to get remote dds_instance_handle_t. I have been thinking about the right way.

  7. eboasson commented on Jan 7, 2019

    @eboasson
    Contributor

    Hi,

    The info[j].instance_handle of the RoundTrip topic is the unqiue identifier for the key value of the data; info[j].publication_handle should be correspond to the instance handle of the DCPSPublication topic (as that identifies the writer). So I think you're just looking at the wrong entry in the sample info. Does that make sense?

    Just to be sure I hacked my tree a little bit to check this. I modified pubsub to lookup and print the GUID from the publication_handle in print_sampleinfo, and the GUID it print matches what I see when I then also run ddls -a, and so it appears that things line up nicely.

      {
        dds_builtintopic_guid_t guid;
        dds_lookup_entity_guid (&guid, si->publication_handle);
        printf (" [%x:%x:%x:%x:%x:%x:%x:%x:%x:%x:%x:%x:%x:%x:%x:%x] ", guid.v[0], guid.v[1], guid.v[2], guid.v[3], guid.v[4], guid.v[5], guid.v[6], guid.v[7], guid.v[8], guid.v[9], guid.v[10], guid.v[11], guid.v[12], guid.v[13], guid.v[14], guid.v[15]);
      }
  8. qqqshengwei commented on Feb 28, 2019

    @qqqshengwei
    Author

    Thanks for your help! We have finish a primary ignore strategy of ignoring a participant. We just preserve a GUID list which consists of GUIDs derived from the ddsi_tkmap_find_by_id by publication_handle. Then we check the GUID list at the top of the method handle_submsg_sequence and return 0 if we found it the right one to be ignored. We found it work right for different participants from difference processes. However, if we create one writer and one reader from the same participant and do ignore after the reader reads (dds_read) info and get the publication_handle, the ignore method is out of operation and the reader can still read info from the writer. How can I handle this situation. I need you help. Thanks!

  9. eboasson commented on Mar 4, 2019

    @eboasson
    Contributor

    Hi @qqqshengwei, that's very good news!

    The reason it doesn't work for writers and readers in the same process is that those take a shortcut via deliver_locally (currently in dds_write.c). Having such a shortcut has the obvious advantage of having less overhead, but there are downsides too. You ran into one of them, the other one is in handling all kinds of edge-cases. So I'm not too certain this will remain exactly like this "forever".

    If there is only one participant in the process, you could simply skip that function. If there is only one participant in the process — which seems to be the case for most — it will be almost the same.

    Now, Cyclone does allow you to create multiple participants in a process, and so the best implementation of ignore_participant is to unmatch any readers and writers from the ignored one and prevent matching new ones. That is also relevant in your current implementation, because it also means there will be less useless state to keep around and that, e.g., lists of matching publications and subscriptions will be correct.

  10. eboasson commented on Jun 10, 2019

    @eboasson
    Contributor

    @qqqshengwei : I seem to assume that merging a commit referencing an issue would notify those who are watching the issue ... but I am not sure it actually works like that. So: Cyclone eventually gained a QoS setting on the reader/writer to ignore readers/writers in the same participant. All you now need to do is use dds_qset_ignorelocal:

    • DDS_IGNORELOCAL_NONE: normal behaviour;
    • DDS_IGNORELOCAL_PARTICIPANT: ignore readers/writers in the same participant;
    • DDS_IGNORELOCAL_PROCESS: ignore readers/writers in the same process (which obviously only makes a difference if you have multiple participants in the same process).

    If you're aware of it, great; if you're not yet aware of it, then it was about time I notified you of it.

    Given this and the fact that there's been no further comment on this issue for three months, I'll close it.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions