diff mbox series

[ovs-dev,v2,2/2] ovsdb-idl: Preserve references for deleted rows.

Message ID 20210303143957.18962.18315.stgit@dceara.remote.csb
State Changes Requested
Headers show
Series ovsdb-idl: Preserve references for tracked deleted rows. | expand

Commit Message

Dumitru Ceara March 3, 2021, 2:40 p.m. UTC
Considering two DB rows, 'a' from table A and 'b' from table B (with
column 'ref_a' a reference to table A):
a = {A._uuid=<U1>}
b = {B._uuid=<U2>, B.ref_a=<U1>}

Assuming both records are present in the IDL client's in-memory view of
the database, depending whether row 'b' is also deleted in the same
transaction or not, deletion of row 'a' should generate the following
tracked changes:

1. only row 'a' is deleted:
- for table A:
  - deleted records: a = {A._uuid=<U1>}
- for table B:
  - updated records: b = {B._uuid=<U2>, B.ref_a=[]}

2. row 'a' and row 'b' are deleted in the same update:
- for table A:
  - deleted records: a = {A._uuid=<U1>}
- for table B:
  - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}

To ensure this, we now delay reparsing row backrefs until the row has
been removed from the table's hmap and until the IDL client has
processed all tracked changes (ovsdb_idl_track_clear() was called).

Without this change, in scenario 2 above, the tracked changes for table
B would be:
- deleted records: b = {B._uuid=<U2>, B.ref_a=[]}

In particular, for strong references, row 'a' can never be deleted in
a transaction that happens strictly before row 'b' is deleted.  In some
cases [0] both rows are deleted in the same transaction and having
B.ref_a=[] would violate the integrity of the database from client
perspective.  This would force the client to always validate that
strong reference fields are non-NULL.  This is not really an option
because the information in the original reference is required for
incrementally processing the record deletion.

[0] with ovn-monitor-all=true, the following command triggers a crash
    in ovn-controller because a strong reference field becomes NULL:
    $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp 00:00:00:00:00:01 1.0.0.1/24
    $ ovn-nbctl lr-del r

Reported-at: https://bugzilla.redhat.com/1932642
Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for deleted rows.")
Signed-off-by: Dumitru Ceara <dceara@redhat.com>
---
v2:
  - Added ovsdb-idl.at test for strong references.
---
 lib/ovsdb-idl.c    |    6 ++-
 tests/ovsdb-idl.at |  118 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/test-ovsdb.c |   45 ++++++++++++++++++++
 3 files changed, 167 insertions(+), 2 deletions(-)

Comments

Ilya Maximets March 9, 2021, 3:07 p.m. UTC | #1
On 3/3/21 3:40 PM, Dumitru Ceara wrote:
> Considering two DB rows, 'a' from table A and 'b' from table B (with
> column 'ref_a' a reference to table A):
> a = {A._uuid=<U1>}
> b = {B._uuid=<U2>, B.ref_a=<U1>}
> 
> Assuming both records are present in the IDL client's in-memory view of
> the database, depending whether row 'b' is also deleted in the same
> transaction or not, deletion of row 'a' should generate the following
> tracked changes:
> 
> 1. only row 'a' is deleted:
> - for table A:
>   - deleted records: a = {A._uuid=<U1>}
> - for table B:
>   - updated records: b = {B._uuid=<U2>, B.ref_a=[]}
> 
> 2. row 'a' and row 'b' are deleted in the same update:
> - for table A:
>   - deleted records: a = {A._uuid=<U1>}
> - for table B:
>   - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}
> 
> To ensure this, we now delay reparsing row backrefs until the row has
> been removed from the table's hmap and until the IDL client has
> processed all tracked changes (ovsdb_idl_track_clear() was called).
> 
> Without this change, in scenario 2 above, the tracked changes for table
> B would be:
> - deleted records: b = {B._uuid=<U2>, B.ref_a=[]}
> 
> In particular, for strong references, row 'a' can never be deleted in
> a transaction that happens strictly before row 'b' is deleted.  In some
> cases [0] both rows are deleted in the same transaction and having
> B.ref_a=[] would violate the integrity of the database from client
> perspective.  This would force the client to always validate that
> strong reference fields are non-NULL.  This is not really an option
> because the information in the original reference is required for
> incrementally processing the record deletion.
> 
> [0] with ovn-monitor-all=true, the following command triggers a crash
>     in ovn-controller because a strong reference field becomes NULL:
>     $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp 00:00:00:00:00:01 1.0.0.1/24
>     $ ovn-nbctl lr-del r
> 
> Reported-at: https://bugzilla.redhat.com/1932642
> Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for deleted rows.")
> Signed-off-by: Dumitru Ceara <dceara@redhat.com>
> ---
> v2:
>   - Added ovsdb-idl.at test for strong references.
> ---
>  lib/ovsdb-idl.c    |    6 ++-
>  tests/ovsdb-idl.at |  118 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>  tests/test-ovsdb.c |   45 ++++++++++++++++++++
>  3 files changed, 167 insertions(+), 2 deletions(-)
> 
> diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
> index 9e1e787..ecd4924 100644
> --- a/lib/ovsdb-idl.c
> +++ b/lib/ovsdb-idl.c
> @@ -163,6 +163,7 @@ static void ovsdb_idl_row_unparse(struct ovsdb_idl_row *);
>  static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
>  static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
>  static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *, bool destroy_dsts);
> +static void ovsdb_idl_row_reparse_backrefs(struct ovsdb_idl_row *row);
>  
>  static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
>  static bool ovsdb_idl_txn_extract_mutations(struct ovsdb_idl_row *,
> @@ -367,6 +368,8 @@ ovsdb_idl_clear(struct ovsdb_idl *db)
>                  ovsdb_idl_row_unparse(row);
>              }
>              LIST_FOR_EACH_SAFE (arc, next_arc, src_node, &row->src_arcs) {
> +                ovs_list_remove(&arc->src_node);
> +                ovs_list_remove(&arc->dst_node);
>                  free(arc);
>              }
>              /* No need to do anything with dst_arcs: some node has those arcs
> @@ -1234,6 +1237,7 @@ ovsdb_idl_track_clear__(struct ovsdb_idl *idl, bool flush_all)
>                  ovs_list_remove(&row->track_node);
>                  ovs_list_init(&row->track_node);
>                  if (ovsdb_idl_row_is_orphan(row)) {
> +                    ovsdb_idl_row_reparse_backrefs(row);
>                      ovsdb_idl_row_unparse(row);
>                      if (row->tracked_old_datum) {
>                          const struct ovsdb_idl_table_class *class =
> @@ -2156,8 +2160,6 @@ ovsdb_idl_delete_row(struct ovsdb_idl_row *row)
>      ovsdb_idl_row_clear_old(row);
>      if (ovs_list_is_empty(&row->dst_arcs)) {
>          ovsdb_idl_row_destroy(row);
> -    } else {
> -        ovsdb_idl_row_reparse_backrefs(row);
>      }

By removing the ovsdb_idl_row_reparse_backrefs() you're making an assumption
that we always have a modification event for all rows that are referencing
this one.  But that is not always the case.

There is scenario where this re-parsing is needed.  In short, if you're
reducing the scope of conditional monitoring and some rows becomes
orphan rows.  In this case ovsdb-server will not generate 'modify' event
for the row that references our newly orphan row, so it will not be
re-parsed and these orphan rows will be accessible the row that references
them.

Here is a quick'n'dirty testcase, how to reproduce:
diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
index 702502280..c6bb3f348 100644
--- a/tests/ovsdb-idl.at
+++ b/tests/ovsdb-idl.at
@@ -1174,7 +1174,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated],
 dnl This test creates database with weak references and checks that orphan
 dnl rows created for weak references are not available for iteration via
 dnl list of tracked changes.
-OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references],
+OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references quickndirty],
   [['["idltest",
       {"op": "insert",
        "table": "simple",
@@ -1196,7 +1196,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak refer
                               ["named-uuid", "weak_row1"],
                               ["named-uuid", "weak_row2"]]
                            ]}}]']],
-  [['condition simple []' \
+  [['condition simple [true]' \
     'condition simple [["s","==","row1_s"]]' \
     '["idltest",
       {"op": "update",
diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
index 36fb9cc08..ace87b13d 100644
--- a/tests/test-ovsdb.c
+++ b/tests/test-ovsdb.c
@@ -2157,6 +2157,7 @@ print_idl_row_singleton(const struct idltest_singleton *sng, int step)
 static void
 print_idl(struct ovsdb_idl *idl, int step)
 {
+    const struct idltest_simple6 *s6;
     const struct idltest_simple *s;
     const struct idltest_link1 *l1;
     const struct idltest_link2 *l2;
@@ -2175,6 +2176,10 @@ print_idl(struct ovsdb_idl *idl, int step)
         print_idl_row_link2(l2, step);
         n++;
     }
+    IDLTEST_SIMPLE6_FOR_EACH (s6, idl) {
+        print_idl_row_simple6(s6, step);
+        n++;
+    }
     IDLTEST_SINGLETON_FOR_EACH (sng, idl) {
         print_idl_row_singleton(sng, step);
         n++;
@@ -2638,8 +2643,12 @@ do_idl(struct ovs_cmdl_context *ctx)
 
             /* Print update. */
             if (track) {
-                print_idl_track(idl, step++);
+                print_idl_track(idl, step);
+                print_and_log("%03d: print_idl[1]", step);
+                print_idl(idl, step);
                 ovsdb_idl_track_clear(idl);
+                print_and_log("%03d: print_idl[2]", step);
+                print_idl(idl, step++);
             } else {
                 print_idl(idl, step++);
             }
---

By running make -j8 check TESTSUITEFLAGS='-k quickndirty -v', you will have
some output for the commands in the log.


The first condition for a 'simple' table is 'true', so we will receive all
3 rows from this table and also row from the 'simple6'.
All 3 rows are in a weak references:

test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
test-ovsdb|test_ovsdb|001: table simple: updated columns: s
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
test-ovsdb|test_ovsdb|001: table simple: updated columns: s
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
test-ovsdb|test_ovsdb|001: table simple: updated columns: s
test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref


All of them are accessible for a user before the track_clear():

test-ovsdb|test_ovsdb|001: print_idl[1]
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
test-ovsdb|test_ovsdb|001: table simple: updated columns: s
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
test-ovsdb|test_ovsdb|001: table simple: updated columns: s
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
test-ovsdb|test_ovsdb|001: table simple: updated columns: s
test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref

And after:

test-ovsdb|test_ovsdb|001: print_idl[2]
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
test-ovsdb|test_ovsdb|001: table simple6: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]


Now condition is changed.  This makes 2 of 3 rows to become orphan rows.
Note that the update notification only removes orphan rows, but doesn't
modify the row in simple6, because rows wasn't actually removed, just
not needed anymore:

test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"3aa79e4a-d752-4d53-8e16-b62887d73ad8":{"delete":null},"d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a":{"delete":null}}}]
test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5

Track list is empty, but 2 orphan rows should be on a track list as deleted:

test-ovsdb|test_ovsdb|003: empty

But all 3 rows are still accessible for a user from the row in 'simple6' even
though table 'simple' has only 1 row accessible:

test-ovsdb|test_ovsdb|003: print_idl[1]
test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]

And they are still here after the track_clear():

test-ovsdb|test_ovsdb|003: print_idl[2]
test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]


When we have unrelated update on a row in 'simple6' it finally gets
re-parsed and we have tracked orphan rows as deleted and they are
no longer accessible for a user from a row in 'simple6':

test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"6fcb8599-4161-4314-901c-2ca4a18686c3":{"modify":{"name":"new_name"}}}}]

Tracked:

test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s <> uuid=d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a
test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s <> uuid=3aa79e4a-d752-4d53-8e16-b62887d73ad8
test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
test-ovsdb|test_ovsdb|005: table simple6: updated columns: name

Before track_clear():

test-ovsdb|test_ovsdb|005: print_idl[1]
test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
test-ovsdb|test_ovsdb|005: table simple6: updated columns: name

After:

test-ovsdb|test_ovsdb|005: print_idl[2]
test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]


Best regards, Ilya Maximets.
Dumitru Ceara March 9, 2021, 4:26 p.m. UTC | #2
On 3/9/21 4:07 PM, Ilya Maximets wrote:
> On 3/3/21 3:40 PM, Dumitru Ceara wrote:
>> Considering two DB rows, 'a' from table A and 'b' from table B (with
>> column 'ref_a' a reference to table A):
>> a = {A._uuid=<U1>}
>> b = {B._uuid=<U2>, B.ref_a=<U1>}
>>
>> Assuming both records are present in the IDL client's in-memory view of
>> the database, depending whether row 'b' is also deleted in the same
>> transaction or not, deletion of row 'a' should generate the following
>> tracked changes:
>>
>> 1. only row 'a' is deleted:
>> - for table A:
>>    - deleted records: a = {A._uuid=<U1>}
>> - for table B:
>>    - updated records: b = {B._uuid=<U2>, B.ref_a=[]}
>>
>> 2. row 'a' and row 'b' are deleted in the same update:
>> - for table A:
>>    - deleted records: a = {A._uuid=<U1>}
>> - for table B:
>>    - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}
>>
>> To ensure this, we now delay reparsing row backrefs until the row has
>> been removed from the table's hmap and until the IDL client has
>> processed all tracked changes (ovsdb_idl_track_clear() was called).
>>
>> Without this change, in scenario 2 above, the tracked changes for table
>> B would be:
>> - deleted records: b = {B._uuid=<U2>, B.ref_a=[]}
>>
>> In particular, for strong references, row 'a' can never be deleted in
>> a transaction that happens strictly before row 'b' is deleted.  In some
>> cases [0] both rows are deleted in the same transaction and having
>> B.ref_a=[] would violate the integrity of the database from client
>> perspective.  This would force the client to always validate that
>> strong reference fields are non-NULL.  This is not really an option
>> because the information in the original reference is required for
>> incrementally processing the record deletion.
>>
>> [0] with ovn-monitor-all=true, the following command triggers a crash
>>      in ovn-controller because a strong reference field becomes NULL:
>>      $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp 00:00:00:00:00:01 1.0.0.1/24
>>      $ ovn-nbctl lr-del r
>>
>> Reported-at: https://bugzilla.redhat.com/1932642
>> Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for deleted rows.")
>> Signed-off-by: Dumitru Ceara <dceara@redhat.com>
>> ---
>> v2:
>>    - Added ovsdb-idl.at test for strong references.
>> ---
>>   lib/ovsdb-idl.c    |    6 ++-
>>   tests/ovsdb-idl.at |  118 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>>   tests/test-ovsdb.c |   45 ++++++++++++++++++++
>>   3 files changed, 167 insertions(+), 2 deletions(-)
>>
>> diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
>> index 9e1e787..ecd4924 100644
>> --- a/lib/ovsdb-idl.c
>> +++ b/lib/ovsdb-idl.c
>> @@ -163,6 +163,7 @@ static void ovsdb_idl_row_unparse(struct ovsdb_idl_row *);
>>   static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
>>   static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
>>   static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *, bool destroy_dsts);
>> +static void ovsdb_idl_row_reparse_backrefs(struct ovsdb_idl_row *row);
>>   
>>   static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
>>   static bool ovsdb_idl_txn_extract_mutations(struct ovsdb_idl_row *,
>> @@ -367,6 +368,8 @@ ovsdb_idl_clear(struct ovsdb_idl *db)
>>                   ovsdb_idl_row_unparse(row);
>>               }
>>               LIST_FOR_EACH_SAFE (arc, next_arc, src_node, &row->src_arcs) {
>> +                ovs_list_remove(&arc->src_node);
>> +                ovs_list_remove(&arc->dst_node);
>>                   free(arc);
>>               }
>>               /* No need to do anything with dst_arcs: some node has those arcs
>> @@ -1234,6 +1237,7 @@ ovsdb_idl_track_clear__(struct ovsdb_idl *idl, bool flush_all)
>>                   ovs_list_remove(&row->track_node);
>>                   ovs_list_init(&row->track_node);
>>                   if (ovsdb_idl_row_is_orphan(row)) {
>> +                    ovsdb_idl_row_reparse_backrefs(row);
>>                       ovsdb_idl_row_unparse(row);
>>                       if (row->tracked_old_datum) {
>>                           const struct ovsdb_idl_table_class *class =
>> @@ -2156,8 +2160,6 @@ ovsdb_idl_delete_row(struct ovsdb_idl_row *row)
>>       ovsdb_idl_row_clear_old(row);
>>       if (ovs_list_is_empty(&row->dst_arcs)) {
>>           ovsdb_idl_row_destroy(row);
>> -    } else {
>> -        ovsdb_idl_row_reparse_backrefs(row);
>>       }
> 
> By removing the ovsdb_idl_row_reparse_backrefs() you're making an assumption
> that we always have a modification event for all rows that are referencing
> this one.  But that is not always the case.
> 
> There is scenario where this re-parsing is needed.  In short, if you're
> reducing the scope of conditional monitoring and some rows becomes
> orphan rows.  In this case ovsdb-server will not generate 'modify' event
> for the row that references our newly orphan row, so it will not be
> re-parsed and these orphan rows will be accessible the row that references
> them.

You're right, thanks for spotting this and for the test case.

> 
> Here is a quick'n'dirty testcase, how to reproduce:
> diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
> index 702502280..c6bb3f348 100644
> --- a/tests/ovsdb-idl.at
> +++ b/tests/ovsdb-idl.at
> @@ -1174,7 +1174,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated],
>   dnl This test creates database with weak references and checks that orphan
>   dnl rows created for weak references are not available for iteration via
>   dnl list of tracked changes.
> -OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references],
> +OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references quickndirty],
>     [['["idltest",
>         {"op": "insert",
>          "table": "simple",
> @@ -1196,7 +1196,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak refer
>                                 ["named-uuid", "weak_row1"],
>                                 ["named-uuid", "weak_row2"]]
>                              ]}}]']],
> -  [['condition simple []' \
> +  [['condition simple [true]' \
>       'condition simple [["s","==","row1_s"]]' \
>       '["idltest",
>         {"op": "update",
> diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
> index 36fb9cc08..ace87b13d 100644
> --- a/tests/test-ovsdb.c
> +++ b/tests/test-ovsdb.c
> @@ -2157,6 +2157,7 @@ print_idl_row_singleton(const struct idltest_singleton *sng, int step)
>   static void
>   print_idl(struct ovsdb_idl *idl, int step)
>   {
> +    const struct idltest_simple6 *s6;
>       const struct idltest_simple *s;
>       const struct idltest_link1 *l1;
>       const struct idltest_link2 *l2;
> @@ -2175,6 +2176,10 @@ print_idl(struct ovsdb_idl *idl, int step)
>           print_idl_row_link2(l2, step);
>           n++;
>       }
> +    IDLTEST_SIMPLE6_FOR_EACH (s6, idl) {
> +        print_idl_row_simple6(s6, step);
> +        n++;
> +    }
>       IDLTEST_SINGLETON_FOR_EACH (sng, idl) {
>           print_idl_row_singleton(sng, step);
>           n++;
> @@ -2638,8 +2643,12 @@ do_idl(struct ovs_cmdl_context *ctx)
>   
>               /* Print update. */
>               if (track) {
> -                print_idl_track(idl, step++);
> +                print_idl_track(idl, step);
> +                print_and_log("%03d: print_idl[1]", step);
> +                print_idl(idl, step);
>                   ovsdb_idl_track_clear(idl);
> +                print_and_log("%03d: print_idl[2]", step);
> +                print_idl(idl, step++);
>               } else {
>                   print_idl(idl, step++);
>               }
> ---
> 
> By running make -j8 check TESTSUITEFLAGS='-k quickndirty -v', you will have
> some output for the commands in the log.
> 
> 
> The first condition for a 'simple' table is 'true', so we will receive all
> 3 rows from this table and also row from the 'simple6'.
> All 3 rows are in a weak references:
> 
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
> 
> 
> All of them are accessible for a user before the track_clear():
> 
> test-ovsdb|test_ovsdb|001: print_idl[1]
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
> 
> And after:
> 
> test-ovsdb|test_ovsdb|001: print_idl[2]
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
> test-ovsdb|test_ovsdb|001: table simple6: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
> 
> 
> Now condition is changed.  This makes 2 of 3 rows to become orphan rows.
> Note that the update notification only removes orphan rows, but doesn't
> modify the row in simple6, because rows wasn't actually removed, just
> not needed anymore:
> 
> test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"3aa79e4a-d752-4d53-8e16-b62887d73ad8":{"delete":null},"d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a":{"delete":null}}}]
> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
> 
> Track list is empty, but 2 orphan rows should be on a track list as deleted:

This actually uncovers a new potential issue: the 2 orphan rows are not 
on the tracked list even without my change.  That's because 
ovsdb_idl_row_destroy() doesn't get called if the row becomes orphan so 
it won't be added to 'row->table->track_list'.

> 
> test-ovsdb|test_ovsdb|003: empty
> 
> But all 3 rows are still accessible for a user from the row in 'simple6' even
> though table 'simple' has only 1 row accessible:
> 
> test-ovsdb|test_ovsdb|003: print_idl[1]
> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
> 
> And they are still here after the track_clear():
> 
> test-ovsdb|test_ovsdb|003: print_idl[2]
> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
> 
> 
> When we have unrelated update on a row in 'simple6' it finally gets
> re-parsed and we have tracked orphan rows as deleted and they are
> no longer accessible for a user from a row in 'simple6':
> 
> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"6fcb8599-4161-4314-901c-2ca4a18686c3":{"modify":{"name":"new_name"}}}}]
> 
> Tracked:
> 
> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s <> uuid=d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a
> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s <> uuid=3aa79e4a-d752-4d53-8e16-b62887d73ad8
> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
> 
> Before track_clear():
> 
> test-ovsdb|test_ovsdb|005: print_idl[1]
> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
> 
> After:
> 
> test-ovsdb|test_ovsdb|005: print_idl[2]
> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
> 

To cover all cases above I'm thinking of the following approach:

1. Delay ovsdb_idl_row_reparse_backrefs() for deleted rows until 
ovsdb_idl_parse_update() has finished.

2. After ovsdb_idl_parse_update(), walk all deleted rows and reparse 
backrefs but only for src rows that have not been deleted.

3. IDL user (e.g., ovn-controller) processes tracked changes.  At this 
point:
- non-deleted src rows with a weak reference to orphaned dst rows have 
ref == NULL.
- deleted src rows with a weak/strong reference to orphaned dst rows 
have ref != NULL but that's fine because they'll both be cleaned up at 
the next step below.

4. ovsdb_idl_track_clear()

What do you think?

> 
> Best regards, Ilya Maximets.
> 

Thanks,
Dumitru
Ilya Maximets March 9, 2021, 4:54 p.m. UTC | #3
On 3/9/21 5:26 PM, Dumitru Ceara wrote:
> On 3/9/21 4:07 PM, Ilya Maximets wrote:
>> On 3/3/21 3:40 PM, Dumitru Ceara wrote:
>>> Considering two DB rows, 'a' from table A and 'b' from table B (with
>>> column 'ref_a' a reference to table A):
>>> a = {A._uuid=<U1>}
>>> b = {B._uuid=<U2>, B.ref_a=<U1>}
>>>
>>> Assuming both records are present in the IDL client's in-memory view of
>>> the database, depending whether row 'b' is also deleted in the same
>>> transaction or not, deletion of row 'a' should generate the following
>>> tracked changes:
>>>
>>> 1. only row 'a' is deleted:
>>> - for table A:
>>>    - deleted records: a = {A._uuid=<U1>}
>>> - for table B:
>>>    - updated records: b = {B._uuid=<U2>, B.ref_a=[]}
>>>
>>> 2. row 'a' and row 'b' are deleted in the same update:
>>> - for table A:
>>>    - deleted records: a = {A._uuid=<U1>}
>>> - for table B:
>>>    - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}
>>>
>>> To ensure this, we now delay reparsing row backrefs until the row has
>>> been removed from the table's hmap and until the IDL client has
>>> processed all tracked changes (ovsdb_idl_track_clear() was called).
>>>
>>> Without this change, in scenario 2 above, the tracked changes for table
>>> B would be:
>>> - deleted records: b = {B._uuid=<U2>, B.ref_a=[]}
>>>
>>> In particular, for strong references, row 'a' can never be deleted in
>>> a transaction that happens strictly before row 'b' is deleted.  In some
>>> cases [0] both rows are deleted in the same transaction and having
>>> B.ref_a=[] would violate the integrity of the database from client
>>> perspective.  This would force the client to always validate that
>>> strong reference fields are non-NULL.  This is not really an option
>>> because the information in the original reference is required for
>>> incrementally processing the record deletion.
>>>
>>> [0] with ovn-monitor-all=true, the following command triggers a crash
>>>      in ovn-controller because a strong reference field becomes NULL:
>>>      $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp 00:00:00:00:00:01 1.0.0.1/24
>>>      $ ovn-nbctl lr-del r
>>>
>>> Reported-at: https://bugzilla.redhat.com/1932642
>>> Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for deleted rows.")
>>> Signed-off-by: Dumitru Ceara <dceara@redhat.com>
>>> ---
>>> v2:
>>>    - Added ovsdb-idl.at test for strong references.
>>> ---
>>>   lib/ovsdb-idl.c    |    6 ++-
>>>   tests/ovsdb-idl.at |  118 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>>>   tests/test-ovsdb.c |   45 ++++++++++++++++++++
>>>   3 files changed, 167 insertions(+), 2 deletions(-)
>>>
>>> diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
>>> index 9e1e787..ecd4924 100644
>>> --- a/lib/ovsdb-idl.c
>>> +++ b/lib/ovsdb-idl.c
>>> @@ -163,6 +163,7 @@ static void ovsdb_idl_row_unparse(struct ovsdb_idl_row *);
>>>   static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
>>>   static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
>>>   static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *, bool destroy_dsts);
>>> +static void ovsdb_idl_row_reparse_backrefs(struct ovsdb_idl_row *row);
>>>     static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
>>>   static bool ovsdb_idl_txn_extract_mutations(struct ovsdb_idl_row *,
>>> @@ -367,6 +368,8 @@ ovsdb_idl_clear(struct ovsdb_idl *db)
>>>                   ovsdb_idl_row_unparse(row);
>>>               }
>>>               LIST_FOR_EACH_SAFE (arc, next_arc, src_node, &row->src_arcs) {
>>> +                ovs_list_remove(&arc->src_node);
>>> +                ovs_list_remove(&arc->dst_node);
>>>                   free(arc);
>>>               }
>>>               /* No need to do anything with dst_arcs: some node has those arcs
>>> @@ -1234,6 +1237,7 @@ ovsdb_idl_track_clear__(struct ovsdb_idl *idl, bool flush_all)
>>>                   ovs_list_remove(&row->track_node);
>>>                   ovs_list_init(&row->track_node);
>>>                   if (ovsdb_idl_row_is_orphan(row)) {
>>> +                    ovsdb_idl_row_reparse_backrefs(row);
>>>                       ovsdb_idl_row_unparse(row);
>>>                       if (row->tracked_old_datum) {
>>>                           const struct ovsdb_idl_table_class *class =
>>> @@ -2156,8 +2160,6 @@ ovsdb_idl_delete_row(struct ovsdb_idl_row *row)
>>>       ovsdb_idl_row_clear_old(row);
>>>       if (ovs_list_is_empty(&row->dst_arcs)) {
>>>           ovsdb_idl_row_destroy(row);
>>> -    } else {
>>> -        ovsdb_idl_row_reparse_backrefs(row);
>>>       }
>>
>> By removing the ovsdb_idl_row_reparse_backrefs() you're making an assumption
>> that we always have a modification event for all rows that are referencing
>> this one.  But that is not always the case.
>>
>> There is scenario where this re-parsing is needed.  In short, if you're
>> reducing the scope of conditional monitoring and some rows becomes
>> orphan rows.  In this case ovsdb-server will not generate 'modify' event
>> for the row that references our newly orphan row, so it will not be
>> re-parsed and these orphan rows will be accessible the row that references
>> them.
> 
> You're right, thanks for spotting this and for the test case.
> 
>>
>> Here is a quick'n'dirty testcase, how to reproduce:
>> diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
>> index 702502280..c6bb3f348 100644
>> --- a/tests/ovsdb-idl.at
>> +++ b/tests/ovsdb-idl.at
>> @@ -1174,7 +1174,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated],
>>   dnl This test creates database with weak references and checks that orphan
>>   dnl rows created for weak references are not available for iteration via
>>   dnl list of tracked changes.
>> -OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references],
>> +OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references quickndirty],
>>     [['["idltest",
>>         {"op": "insert",
>>          "table": "simple",
>> @@ -1196,7 +1196,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak refer
>>                                 ["named-uuid", "weak_row1"],
>>                                 ["named-uuid", "weak_row2"]]
>>                              ]}}]']],
>> -  [['condition simple []' \
>> +  [['condition simple [true]' \
>>       'condition simple [["s","==","row1_s"]]' \
>>       '["idltest",
>>         {"op": "update",
>> diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
>> index 36fb9cc08..ace87b13d 100644
>> --- a/tests/test-ovsdb.c
>> +++ b/tests/test-ovsdb.c
>> @@ -2157,6 +2157,7 @@ print_idl_row_singleton(const struct idltest_singleton *sng, int step)
>>   static void
>>   print_idl(struct ovsdb_idl *idl, int step)
>>   {
>> +    const struct idltest_simple6 *s6;
>>       const struct idltest_simple *s;
>>       const struct idltest_link1 *l1;
>>       const struct idltest_link2 *l2;
>> @@ -2175,6 +2176,10 @@ print_idl(struct ovsdb_idl *idl, int step)
>>           print_idl_row_link2(l2, step);
>>           n++;
>>       }
>> +    IDLTEST_SIMPLE6_FOR_EACH (s6, idl) {
>> +        print_idl_row_simple6(s6, step);
>> +        n++;
>> +    }
>>       IDLTEST_SINGLETON_FOR_EACH (sng, idl) {
>>           print_idl_row_singleton(sng, step);
>>           n++;
>> @@ -2638,8 +2643,12 @@ do_idl(struct ovs_cmdl_context *ctx)
>>                 /* Print update. */
>>               if (track) {
>> -                print_idl_track(idl, step++);
>> +                print_idl_track(idl, step);
>> +                print_and_log("%03d: print_idl[1]", step);
>> +                print_idl(idl, step);
>>                   ovsdb_idl_track_clear(idl);
>> +                print_and_log("%03d: print_idl[2]", step);
>> +                print_idl(idl, step++);
>>               } else {
>>                   print_idl(idl, step++);
>>               }
>> ---
>>
>> By running make -j8 check TESTSUITEFLAGS='-k quickndirty -v', you will have
>> some output for the commands in the log.
>>
>>
>> The first condition for a 'simple' table is 'true', so we will receive all
>> 3 rows from this table and also row from the 'simple6'.
>> All 3 rows are in a weak references:
>>
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
>>
>>
>> All of them are accessible for a user before the track_clear():
>>
>> test-ovsdb|test_ovsdb|001: print_idl[1]
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
>>
>> And after:
>>
>> test-ovsdb|test_ovsdb|001: print_idl[2]
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>> test-ovsdb|test_ovsdb|001: table simple6: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>>
>>
>> Now condition is changed.  This makes 2 of 3 rows to become orphan rows.
>> Note that the update notification only removes orphan rows, but doesn't
>> modify the row in simple6, because rows wasn't actually removed, just
>> not needed anymore:
>>
>> test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"3aa79e4a-d752-4d53-8e16-b62887d73ad8":{"delete":null},"d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a":{"delete":null}}}]
>> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
>>
>> Track list is empty, but 2 orphan rows should be on a track list as deleted:
> 
> This actually uncovers a new potential issue: the 2 orphan rows are not on the tracked list even without my change.  That's because ovsdb_idl_row_destroy() doesn't get called if the row becomes orphan so it won't be added to 'row->table->track_list'.

Hmm.  Thanks for sponning that.

On current master these 2 rows appears on a tracked list only after
occasional unrelated modification of a row in 'simple6' and that is
not good:

test-ovsdb|test_ovsdb|002: change conditions
test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78":{"delete":null},"5a1d0257-fc27-4046-9e18-25539f122052":{"delete":null}}}]
test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
test-ovsdb|test_ovsdb|003: empty
test-ovsdb|test_ovsdb|003: print_idl[1]
test-ovsdb|test_ovsdb|003: table simple: s=row1_s uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
test-ovsdb|test_ovsdb|003: print_idl[2]
test-ovsdb|test_ovsdb|003: table simple: s=row1_s uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]

test-ovsdb|jsonrpc|unix:socket: send request, method="transact", params=["idltest",{"where":[],"row":{"name":"new_name"},"op":"update","table":"simple6"}], id=6
test-ovsdb|jsonrpc|unix:socket: received reply, result=[{"count":1}], id=6
test-ovsdb|test_ovsdb|004: {"error":null,"result":[{"count":1}]}
test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"618cef34-e635-4841-9dc7-a14eb2b9627e":{"modify":{"name":"new_name"}}}}]
test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s uuid=5a1d0257-fc27-4046-9e18-25539f122052
test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s uuid=0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78
test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
test-ovsdb|test_ovsdb|005: table simple6: updated columns: name


It's probably because ovsdb_idl_row_reparse_backrefs() calls
ovsdb_idl_row_clear_arcs() with destroy_dsts=false.  So, re-parsing
invoked for two deleted rows doesn't actually destroy them.

> 
>>
>> test-ovsdb|test_ovsdb|003: empty
>>
>> But all 3 rows are still accessible for a user from the row in 'simple6' even
>> though table 'simple' has only 1 row accessible:
>>
>> test-ovsdb|test_ovsdb|003: print_idl[1]
>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>>
>> And they are still here after the track_clear():
>>
>> test-ovsdb|test_ovsdb|003: print_idl[2]
>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>>
>>
>> When we have unrelated update on a row in 'simple6' it finally gets
>> re-parsed and we have tracked orphan rows as deleted and they are
>> no longer accessible for a user from a row in 'simple6':
>>
>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"6fcb8599-4161-4314-901c-2ca4a18686c3":{"modify":{"name":"new_name"}}}}]
>>
>> Tracked:
>>
>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s <> uuid=d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a
>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s <> uuid=3aa79e4a-d752-4d53-8e16-b62887d73ad8
>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>>
>> Before track_clear():
>>
>> test-ovsdb|test_ovsdb|005: print_idl[1]
>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>>
>> After:
>>
>> test-ovsdb|test_ovsdb|005: print_idl[2]
>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>>
> 
> To cover all cases above I'm thinking of the following approach:
> 
> 1. Delay ovsdb_idl_row_reparse_backrefs() for deleted rows until ovsdb_idl_parse_update() has finished.
> 
> 2. After ovsdb_idl_parse_update(), walk all deleted rows and reparse backrefs but only for src rows that have not been deleted.
> 
> 3. IDL user (e.g., ovn-controller) processes tracked changes.  At this point:
> - non-deleted src rows with a weak reference to orphaned dst rows have ref == NULL.
> - deleted src rows with a weak/strong reference to orphaned dst rows have ref != NULL but that's fine because they'll both be cleaned up at the next step below.
> 
> 4. ovsdb_idl_track_clear()
> 
> What do you think?

Sounds OK.  I'm not sure how the implementation should look like, though.
Will this also fix the issue with not reacking deleted orphaned rows right away?

> 
>>
>> Best regards, Ilya Maximets.
>>
> 
> Thanks,
> Dumitru
>
Ilya Maximets March 9, 2021, 4:56 p.m. UTC | #4
On 3/9/21 5:54 PM, Ilya Maximets wrote:
> On 3/9/21 5:26 PM, Dumitru Ceara wrote:
>> On 3/9/21 4:07 PM, Ilya Maximets wrote:
>>> On 3/3/21 3:40 PM, Dumitru Ceara wrote:
>>>> Considering two DB rows, 'a' from table A and 'b' from table B (with
>>>> column 'ref_a' a reference to table A):
>>>> a = {A._uuid=<U1>}
>>>> b = {B._uuid=<U2>, B.ref_a=<U1>}
>>>>
>>>> Assuming both records are present in the IDL client's in-memory view of
>>>> the database, depending whether row 'b' is also deleted in the same
>>>> transaction or not, deletion of row 'a' should generate the following
>>>> tracked changes:
>>>>
>>>> 1. only row 'a' is deleted:
>>>> - for table A:
>>>>    - deleted records: a = {A._uuid=<U1>}
>>>> - for table B:
>>>>    - updated records: b = {B._uuid=<U2>, B.ref_a=[]}
>>>>
>>>> 2. row 'a' and row 'b' are deleted in the same update:
>>>> - for table A:
>>>>    - deleted records: a = {A._uuid=<U1>}
>>>> - for table B:
>>>>    - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}
>>>>
>>>> To ensure this, we now delay reparsing row backrefs until the row has
>>>> been removed from the table's hmap and until the IDL client has
>>>> processed all tracked changes (ovsdb_idl_track_clear() was called).
>>>>
>>>> Without this change, in scenario 2 above, the tracked changes for table
>>>> B would be:
>>>> - deleted records: b = {B._uuid=<U2>, B.ref_a=[]}
>>>>
>>>> In particular, for strong references, row 'a' can never be deleted in
>>>> a transaction that happens strictly before row 'b' is deleted.  In some
>>>> cases [0] both rows are deleted in the same transaction and having
>>>> B.ref_a=[] would violate the integrity of the database from client
>>>> perspective.  This would force the client to always validate that
>>>> strong reference fields are non-NULL.  This is not really an option
>>>> because the information in the original reference is required for
>>>> incrementally processing the record deletion.
>>>>
>>>> [0] with ovn-monitor-all=true, the following command triggers a crash
>>>>      in ovn-controller because a strong reference field becomes NULL:
>>>>      $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp 00:00:00:00:00:01 1.0.0.1/24
>>>>      $ ovn-nbctl lr-del r
>>>>
>>>> Reported-at: https://bugzilla.redhat.com/1932642
>>>> Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for deleted rows.")
>>>> Signed-off-by: Dumitru Ceara <dceara@redhat.com>
>>>> ---
>>>> v2:
>>>>    - Added ovsdb-idl.at test for strong references.
>>>> ---
>>>>   lib/ovsdb-idl.c    |    6 ++-
>>>>   tests/ovsdb-idl.at |  118 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>>>>   tests/test-ovsdb.c |   45 ++++++++++++++++++++
>>>>   3 files changed, 167 insertions(+), 2 deletions(-)
>>>>
>>>> diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
>>>> index 9e1e787..ecd4924 100644
>>>> --- a/lib/ovsdb-idl.c
>>>> +++ b/lib/ovsdb-idl.c
>>>> @@ -163,6 +163,7 @@ static void ovsdb_idl_row_unparse(struct ovsdb_idl_row *);
>>>>   static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
>>>>   static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
>>>>   static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *, bool destroy_dsts);
>>>> +static void ovsdb_idl_row_reparse_backrefs(struct ovsdb_idl_row *row);
>>>>     static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
>>>>   static bool ovsdb_idl_txn_extract_mutations(struct ovsdb_idl_row *,
>>>> @@ -367,6 +368,8 @@ ovsdb_idl_clear(struct ovsdb_idl *db)
>>>>                   ovsdb_idl_row_unparse(row);
>>>>               }
>>>>               LIST_FOR_EACH_SAFE (arc, next_arc, src_node, &row->src_arcs) {
>>>> +                ovs_list_remove(&arc->src_node);
>>>> +                ovs_list_remove(&arc->dst_node);
>>>>                   free(arc);
>>>>               }
>>>>               /* No need to do anything with dst_arcs: some node has those arcs
>>>> @@ -1234,6 +1237,7 @@ ovsdb_idl_track_clear__(struct ovsdb_idl *idl, bool flush_all)
>>>>                   ovs_list_remove(&row->track_node);
>>>>                   ovs_list_init(&row->track_node);
>>>>                   if (ovsdb_idl_row_is_orphan(row)) {
>>>> +                    ovsdb_idl_row_reparse_backrefs(row);
>>>>                       ovsdb_idl_row_unparse(row);
>>>>                       if (row->tracked_old_datum) {
>>>>                           const struct ovsdb_idl_table_class *class =
>>>> @@ -2156,8 +2160,6 @@ ovsdb_idl_delete_row(struct ovsdb_idl_row *row)
>>>>       ovsdb_idl_row_clear_old(row);
>>>>       if (ovs_list_is_empty(&row->dst_arcs)) {
>>>>           ovsdb_idl_row_destroy(row);
>>>> -    } else {
>>>> -        ovsdb_idl_row_reparse_backrefs(row);
>>>>       }
>>>
>>> By removing the ovsdb_idl_row_reparse_backrefs() you're making an assumption
>>> that we always have a modification event for all rows that are referencing
>>> this one.  But that is not always the case.
>>>
>>> There is scenario where this re-parsing is needed.  In short, if you're
>>> reducing the scope of conditional monitoring and some rows becomes
>>> orphan rows.  In this case ovsdb-server will not generate 'modify' event
>>> for the row that references our newly orphan row, so it will not be
>>> re-parsed and these orphan rows will be accessible the row that references
>>> them.
>>
>> You're right, thanks for spotting this and for the test case.
>>
>>>
>>> Here is a quick'n'dirty testcase, how to reproduce:
>>> diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
>>> index 702502280..c6bb3f348 100644
>>> --- a/tests/ovsdb-idl.at
>>> +++ b/tests/ovsdb-idl.at
>>> @@ -1174,7 +1174,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated],
>>>   dnl This test creates database with weak references and checks that orphan
>>>   dnl rows created for weak references are not available for iteration via
>>>   dnl list of tracked changes.
>>> -OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references],
>>> +OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references quickndirty],
>>>     [['["idltest",
>>>         {"op": "insert",
>>>          "table": "simple",
>>> @@ -1196,7 +1196,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak refer
>>>                                 ["named-uuid", "weak_row1"],
>>>                                 ["named-uuid", "weak_row2"]]
>>>                              ]}}]']],
>>> -  [['condition simple []' \
>>> +  [['condition simple [true]' \
>>>       'condition simple [["s","==","row1_s"]]' \
>>>       '["idltest",
>>>         {"op": "update",
>>> diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
>>> index 36fb9cc08..ace87b13d 100644
>>> --- a/tests/test-ovsdb.c
>>> +++ b/tests/test-ovsdb.c
>>> @@ -2157,6 +2157,7 @@ print_idl_row_singleton(const struct idltest_singleton *sng, int step)
>>>   static void
>>>   print_idl(struct ovsdb_idl *idl, int step)
>>>   {
>>> +    const struct idltest_simple6 *s6;
>>>       const struct idltest_simple *s;
>>>       const struct idltest_link1 *l1;
>>>       const struct idltest_link2 *l2;
>>> @@ -2175,6 +2176,10 @@ print_idl(struct ovsdb_idl *idl, int step)
>>>           print_idl_row_link2(l2, step);
>>>           n++;
>>>       }
>>> +    IDLTEST_SIMPLE6_FOR_EACH (s6, idl) {
>>> +        print_idl_row_simple6(s6, step);
>>> +        n++;
>>> +    }
>>>       IDLTEST_SINGLETON_FOR_EACH (sng, idl) {
>>>           print_idl_row_singleton(sng, step);
>>>           n++;
>>> @@ -2638,8 +2643,12 @@ do_idl(struct ovs_cmdl_context *ctx)
>>>                 /* Print update. */
>>>               if (track) {
>>> -                print_idl_track(idl, step++);
>>> +                print_idl_track(idl, step);
>>> +                print_and_log("%03d: print_idl[1]", step);
>>> +                print_idl(idl, step);
>>>                   ovsdb_idl_track_clear(idl);
>>> +                print_and_log("%03d: print_idl[2]", step);
>>> +                print_idl(idl, step++);
>>>               } else {
>>>                   print_idl(idl, step++);
>>>               }
>>> ---
>>>
>>> By running make -j8 check TESTSUITEFLAGS='-k quickndirty -v', you will have
>>> some output for the commands in the log.
>>>
>>>
>>> The first condition for a 'simple' table is 'true', so we will receive all
>>> 3 rows from this table and also row from the 'simple6'.
>>> All 3 rows are in a weak references:
>>>
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
>>>
>>>
>>> All of them are accessible for a user before the track_clear():
>>>
>>> test-ovsdb|test_ovsdb|001: print_idl[1]
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
>>>
>>> And after:
>>>
>>> test-ovsdb|test_ovsdb|001: print_idl[2]
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>>> test-ovsdb|test_ovsdb|001: table simple6: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>>>
>>>
>>> Now condition is changed.  This makes 2 of 3 rows to become orphan rows.
>>> Note that the update notification only removes orphan rows, but doesn't
>>> modify the row in simple6, because rows wasn't actually removed, just
>>> not needed anymore:
>>>
>>> test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
>>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"3aa79e4a-d752-4d53-8e16-b62887d73ad8":{"delete":null},"d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a":{"delete":null}}}]
>>> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
>>>
>>> Track list is empty, but 2 orphan rows should be on a track list as deleted:
>>
>> This actually uncovers a new potential issue: the 2 orphan rows are not on the tracked list even without my change.  That's because ovsdb_idl_row_destroy() doesn't get called if the row becomes orphan so it won't be added to 'row->table->track_list'.
> 
> Hmm.  Thanks for sponning that.

*spotting

> 
> On current master these 2 rows appears on a tracked list only after
> occasional unrelated modification of a row in 'simple6' and that is
> not good:
> 
> test-ovsdb|test_ovsdb|002: change conditions
> test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78":{"delete":null},"5a1d0257-fc27-4046-9e18-25539f122052":{"delete":null}}}]
> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
> test-ovsdb|test_ovsdb|003: empty
> test-ovsdb|test_ovsdb|003: print_idl[1]
> test-ovsdb|test_ovsdb|003: table simple: s=row1_s uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
> test-ovsdb|test_ovsdb|003: print_idl[2]
> test-ovsdb|test_ovsdb|003: table simple: s=row1_s uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
> 
> test-ovsdb|jsonrpc|unix:socket: send request, method="transact", params=["idltest",{"where":[],"row":{"name":"new_name"},"op":"update","table":"simple6"}], id=6
> test-ovsdb|jsonrpc|unix:socket: received reply, result=[{"count":1}], id=6
> test-ovsdb|test_ovsdb|004: {"error":null,"result":[{"count":1}]}
> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"618cef34-e635-4841-9dc7-a14eb2b9627e":{"modify":{"name":"new_name"}}}}]
> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s uuid=5a1d0257-fc27-4046-9e18-25539f122052
> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s uuid=0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78
> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
> 
> 
> It's probably because ovsdb_idl_row_reparse_backrefs() calls
> ovsdb_idl_row_clear_arcs() with destroy_dsts=false.  So, re-parsing
> invoked for two deleted rows doesn't actually destroy them.
> 
>>
>>>
>>> test-ovsdb|test_ovsdb|003: empty
>>>
>>> But all 3 rows are still accessible for a user from the row in 'simple6' even
>>> though table 'simple' has only 1 row accessible:
>>>
>>> test-ovsdb|test_ovsdb|003: print_idl[1]
>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>>>
>>> And they are still here after the track_clear():
>>>
>>> test-ovsdb|test_ovsdb|003: print_idl[2]
>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>>>
>>>
>>> When we have unrelated update on a row in 'simple6' it finally gets
>>> re-parsed and we have tracked orphan rows as deleted and they are
>>> no longer accessible for a user from a row in 'simple6':
>>>
>>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"6fcb8599-4161-4314-901c-2ca4a18686c3":{"modify":{"name":"new_name"}}}}]
>>>
>>> Tracked:
>>>
>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s <> uuid=d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a
>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s <> uuid=3aa79e4a-d752-4d53-8e16-b62887d73ad8
>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>>>
>>> Before track_clear():
>>>
>>> test-ovsdb|test_ovsdb|005: print_idl[1]
>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>>>
>>> After:
>>>
>>> test-ovsdb|test_ovsdb|005: print_idl[2]
>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>>>
>>
>> To cover all cases above I'm thinking of the following approach:
>>
>> 1. Delay ovsdb_idl_row_reparse_backrefs() for deleted rows until ovsdb_idl_parse_update() has finished.
>>
>> 2. After ovsdb_idl_parse_update(), walk all deleted rows and reparse backrefs but only for src rows that have not been deleted.
>>
>> 3. IDL user (e.g., ovn-controller) processes tracked changes.  At this point:
>> - non-deleted src rows with a weak reference to orphaned dst rows have ref == NULL.
>> - deleted src rows with a weak/strong reference to orphaned dst rows have ref != NULL but that's fine because they'll both be cleaned up at the next step below.
>>
>> 4. ovsdb_idl_track_clear()
>>
>> What do you think?
> 
> Sounds OK.  I'm not sure how the implementation should look like, though.
> Will this also fix the issue with not reacking deleted orphaned rows right away?
> 
>>
>>>
>>> Best regards, Ilya Maximets.
>>>
>>
>> Thanks,
>> Dumitru
>>
>
Dumitru Ceara March 9, 2021, 5:03 p.m. UTC | #5
On 3/9/21 5:56 PM, Ilya Maximets wrote:
> On 3/9/21 5:54 PM, Ilya Maximets wrote:
>> On 3/9/21 5:26 PM, Dumitru Ceara wrote:
>>> On 3/9/21 4:07 PM, Ilya Maximets wrote:
>>>> On 3/3/21 3:40 PM, Dumitru Ceara wrote:
>>>>> Considering two DB rows, 'a' from table A and 'b' from table B (with
>>>>> column 'ref_a' a reference to table A):
>>>>> a = {A._uuid=<U1>}
>>>>> b = {B._uuid=<U2>, B.ref_a=<U1>}
>>>>>
>>>>> Assuming both records are present in the IDL client's in-memory view of
>>>>> the database, depending whether row 'b' is also deleted in the same
>>>>> transaction or not, deletion of row 'a' should generate the following
>>>>> tracked changes:
>>>>>
>>>>> 1. only row 'a' is deleted:
>>>>> - for table A:
>>>>>     - deleted records: a = {A._uuid=<U1>}
>>>>> - for table B:
>>>>>     - updated records: b = {B._uuid=<U2>, B.ref_a=[]}
>>>>>
>>>>> 2. row 'a' and row 'b' are deleted in the same update:
>>>>> - for table A:
>>>>>     - deleted records: a = {A._uuid=<U1>}
>>>>> - for table B:
>>>>>     - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}
>>>>>
>>>>> To ensure this, we now delay reparsing row backrefs until the row has
>>>>> been removed from the table's hmap and until the IDL client has
>>>>> processed all tracked changes (ovsdb_idl_track_clear() was called).
>>>>>
>>>>> Without this change, in scenario 2 above, the tracked changes for table
>>>>> B would be:
>>>>> - deleted records: b = {B._uuid=<U2>, B.ref_a=[]}
>>>>>
>>>>> In particular, for strong references, row 'a' can never be deleted in
>>>>> a transaction that happens strictly before row 'b' is deleted.  In some
>>>>> cases [0] both rows are deleted in the same transaction and having
>>>>> B.ref_a=[] would violate the integrity of the database from client
>>>>> perspective.  This would force the client to always validate that
>>>>> strong reference fields are non-NULL.  This is not really an option
>>>>> because the information in the original reference is required for
>>>>> incrementally processing the record deletion.
>>>>>
>>>>> [0] with ovn-monitor-all=true, the following command triggers a crash
>>>>>       in ovn-controller because a strong reference field becomes NULL:
>>>>>       $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp 00:00:00:00:00:01 1.0.0.1/24
>>>>>       $ ovn-nbctl lr-del r
>>>>>
>>>>> Reported-at: https://bugzilla.redhat.com/1932642
>>>>> Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for deleted rows.")
>>>>> Signed-off-by: Dumitru Ceara <dceara@redhat.com>
>>>>> ---
>>>>> v2:
>>>>>     - Added ovsdb-idl.at test for strong references.
>>>>> ---
>>>>>    lib/ovsdb-idl.c    |    6 ++-
>>>>>    tests/ovsdb-idl.at |  118 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>>>>>    tests/test-ovsdb.c |   45 ++++++++++++++++++++
>>>>>    3 files changed, 167 insertions(+), 2 deletions(-)
>>>>>
>>>>> diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
>>>>> index 9e1e787..ecd4924 100644
>>>>> --- a/lib/ovsdb-idl.c
>>>>> +++ b/lib/ovsdb-idl.c
>>>>> @@ -163,6 +163,7 @@ static void ovsdb_idl_row_unparse(struct ovsdb_idl_row *);
>>>>>    static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
>>>>>    static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
>>>>>    static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *, bool destroy_dsts);
>>>>> +static void ovsdb_idl_row_reparse_backrefs(struct ovsdb_idl_row *row);
>>>>>      static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
>>>>>    static bool ovsdb_idl_txn_extract_mutations(struct ovsdb_idl_row *,
>>>>> @@ -367,6 +368,8 @@ ovsdb_idl_clear(struct ovsdb_idl *db)
>>>>>                    ovsdb_idl_row_unparse(row);
>>>>>                }
>>>>>                LIST_FOR_EACH_SAFE (arc, next_arc, src_node, &row->src_arcs) {
>>>>> +                ovs_list_remove(&arc->src_node);
>>>>> +                ovs_list_remove(&arc->dst_node);
>>>>>                    free(arc);
>>>>>                }
>>>>>                /* No need to do anything with dst_arcs: some node has those arcs
>>>>> @@ -1234,6 +1237,7 @@ ovsdb_idl_track_clear__(struct ovsdb_idl *idl, bool flush_all)
>>>>>                    ovs_list_remove(&row->track_node);
>>>>>                    ovs_list_init(&row->track_node);
>>>>>                    if (ovsdb_idl_row_is_orphan(row)) {
>>>>> +                    ovsdb_idl_row_reparse_backrefs(row);
>>>>>                        ovsdb_idl_row_unparse(row);
>>>>>                        if (row->tracked_old_datum) {
>>>>>                            const struct ovsdb_idl_table_class *class =
>>>>> @@ -2156,8 +2160,6 @@ ovsdb_idl_delete_row(struct ovsdb_idl_row *row)
>>>>>        ovsdb_idl_row_clear_old(row);
>>>>>        if (ovs_list_is_empty(&row->dst_arcs)) {
>>>>>            ovsdb_idl_row_destroy(row);
>>>>> -    } else {
>>>>> -        ovsdb_idl_row_reparse_backrefs(row);
>>>>>        }
>>>>
>>>> By removing the ovsdb_idl_row_reparse_backrefs() you're making an assumption
>>>> that we always have a modification event for all rows that are referencing
>>>> this one.  But that is not always the case.
>>>>
>>>> There is scenario where this re-parsing is needed.  In short, if you're
>>>> reducing the scope of conditional monitoring and some rows becomes
>>>> orphan rows.  In this case ovsdb-server will not generate 'modify' event
>>>> for the row that references our newly orphan row, so it will not be
>>>> re-parsed and these orphan rows will be accessible the row that references
>>>> them.
>>>
>>> You're right, thanks for spotting this and for the test case.
>>>
>>>>
>>>> Here is a quick'n'dirty testcase, how to reproduce:
>>>> diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
>>>> index 702502280..c6bb3f348 100644
>>>> --- a/tests/ovsdb-idl.at
>>>> +++ b/tests/ovsdb-idl.at
>>>> @@ -1174,7 +1174,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated],
>>>>    dnl This test creates database with weak references and checks that orphan
>>>>    dnl rows created for weak references are not available for iteration via
>>>>    dnl list of tracked changes.
>>>> -OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references],
>>>> +OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak references quickndirty],
>>>>      [['["idltest",
>>>>          {"op": "insert",
>>>>           "table": "simple",
>>>> @@ -1196,7 +1196,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan weak refer
>>>>                                  ["named-uuid", "weak_row1"],
>>>>                                  ["named-uuid", "weak_row2"]]
>>>>                               ]}}]']],
>>>> -  [['condition simple []' \
>>>> +  [['condition simple [true]' \
>>>>        'condition simple [["s","==","row1_s"]]' \
>>>>        '["idltest",
>>>>          {"op": "update",
>>>> diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
>>>> index 36fb9cc08..ace87b13d 100644
>>>> --- a/tests/test-ovsdb.c
>>>> +++ b/tests/test-ovsdb.c
>>>> @@ -2157,6 +2157,7 @@ print_idl_row_singleton(const struct idltest_singleton *sng, int step)
>>>>    static void
>>>>    print_idl(struct ovsdb_idl *idl, int step)
>>>>    {
>>>> +    const struct idltest_simple6 *s6;
>>>>        const struct idltest_simple *s;
>>>>        const struct idltest_link1 *l1;
>>>>        const struct idltest_link2 *l2;
>>>> @@ -2175,6 +2176,10 @@ print_idl(struct ovsdb_idl *idl, int step)
>>>>            print_idl_row_link2(l2, step);
>>>>            n++;
>>>>        }
>>>> +    IDLTEST_SIMPLE6_FOR_EACH (s6, idl) {
>>>> +        print_idl_row_simple6(s6, step);
>>>> +        n++;
>>>> +    }
>>>>        IDLTEST_SINGLETON_FOR_EACH (sng, idl) {
>>>>            print_idl_row_singleton(sng, step);
>>>>            n++;
>>>> @@ -2638,8 +2643,12 @@ do_idl(struct ovs_cmdl_context *ctx)
>>>>                  /* Print update. */
>>>>                if (track) {
>>>> -                print_idl_track(idl, step++);
>>>> +                print_idl_track(idl, step);
>>>> +                print_and_log("%03d: print_idl[1]", step);
>>>> +                print_idl(idl, step);
>>>>                    ovsdb_idl_track_clear(idl);
>>>> +                print_and_log("%03d: print_idl[2]", step);
>>>> +                print_idl(idl, step++);
>>>>                } else {
>>>>                    print_idl(idl, step++);
>>>>                }
>>>> ---
>>>>
>>>> By running make -j8 check TESTSUITEFLAGS='-k quickndirty -v', you will have
>>>> some output for the commands in the log.
>>>>
>>>>
>>>> The first condition for a 'simple' table is 'true', so we will receive all
>>>> 3 rows from this table and also row from the 'simple6'.
>>>> All 3 rows are in a weak references:
>>>>
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
>>>>
>>>>
>>>> All of them are accessible for a user before the track_clear():
>>>>
>>>> test-ovsdb|test_ovsdb|001: print_idl[1]
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name weak_ref
>>>>
>>>> And after:
>>>>
>>>> test-ovsdb|test_ovsdb|001: print_idl[2]
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>>>> test-ovsdb|test_ovsdb|001: table simple6: name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>>>>
>>>>
>>>> Now condition is changed.  This makes 2 of 3 rows to become orphan rows.
>>>> Note that the update notification only removes orphan rows, but doesn't
>>>> modify the row in simple6, because rows wasn't actually removed, just
>>>> not needed anymore:
>>>>
>>>> test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
>>>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"3aa79e4a-d752-4d53-8e16-b62887d73ad8":{"delete":null},"d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a":{"delete":null}}}]
>>>> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
>>>>
>>>> Track list is empty, but 2 orphan rows should be on a track list as deleted:
>>>
>>> This actually uncovers a new potential issue: the 2 orphan rows are not on the tracked list even without my change.  That's because ovsdb_idl_row_destroy() doesn't get called if the row becomes orphan so it won't be added to 'row->table->track_list'.
>>
>> Hmm.  Thanks for sponning that.
> 
> *spotting
> 
>>
>> On current master these 2 rows appears on a tracked list only after
>> occasional unrelated modification of a row in 'simple6' and that is
>> not good:
>>
>> test-ovsdb|test_ovsdb|002: change conditions
>> test-ovsdb|jsonrpc|unix:socket: send request, method="monitor_cond_change", params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], id=5
>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78":{"delete":null},"5a1d0257-fc27-4046-9e18-25539f122052":{"delete":null}}}]
>> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
>> test-ovsdb|test_ovsdb|003: empty
>> test-ovsdb|test_ovsdb|003: print_idl[1]
>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
>> test-ovsdb|test_ovsdb|003: print_idl[2]
>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
>>
>> test-ovsdb|jsonrpc|unix:socket: send request, method="transact", params=["idltest",{"where":[],"row":{"name":"new_name"},"op":"update","table":"simple6"}], id=6
>> test-ovsdb|jsonrpc|unix:socket: received reply, result=[{"count":1}], id=6
>> test-ovsdb|test_ovsdb|004: {"error":null,"result":[{"count":1}]}
>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"618cef34-e635-4841-9dc7-a14eb2b9627e":{"modify":{"name":"new_name"}}}}]
>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s uuid=5a1d0257-fc27-4046-9e18-25539f122052
>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s uuid=0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78
>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>>
>>
>> It's probably because ovsdb_idl_row_reparse_backrefs() calls
>> ovsdb_idl_row_clear_arcs() with destroy_dsts=false.  So, re-parsing
>> invoked for two deleted rows doesn't actually destroy them.
>>
>>>
>>>>
>>>> test-ovsdb|test_ovsdb|003: empty
>>>>
>>>> But all 3 rows are still accessible for a user from the row in 'simple6' even
>>>> though table 'simple' has only 1 row accessible:
>>>>
>>>> test-ovsdb|test_ovsdb|003: print_idl[1]
>>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>>>>
>>>> And they are still here after the track_clear():
>>>>
>>>> test-ovsdb|test_ovsdb|003: print_idl[2]
>>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>>>>
>>>>
>>>> When we have unrelated update on a row in 'simple6' it finally gets
>>>> re-parsed and we have tracked orphan rows as deleted and they are
>>>> no longer accessible for a user from a row in 'simple6':
>>>>
>>>> test-ovsdb|jsonrpc|unix:socket: received notification, method="update3", params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"6fcb8599-4161-4314-901c-2ca4a18686c3":{"modify":{"name":"new_name"}}}}]
>>>>
>>>> Tracked:
>>>>
>>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s <> uuid=d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a
>>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s <> uuid=3aa79e4a-d752-4d53-8e16-b62887d73ad8
>>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>>>>
>>>> Before track_clear():
>>>>
>>>> test-ovsdb|test_ovsdb|005: print_idl[1]
>>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>>>>
>>>> After:
>>>>
>>>> test-ovsdb|test_ovsdb|005: print_idl[2]
>>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>>>>
>>>
>>> To cover all cases above I'm thinking of the following approach:
>>>
>>> 1. Delay ovsdb_idl_row_reparse_backrefs() for deleted rows until ovsdb_idl_parse_update() has finished.
>>>
>>> 2. After ovsdb_idl_parse_update(), walk all deleted rows and reparse backrefs but only for src rows that have not been deleted.
>>>
>>> 3. IDL user (e.g., ovn-controller) processes tracked changes.  At this point:
>>> - non-deleted src rows with a weak reference to orphaned dst rows have ref == NULL.
>>> - deleted src rows with a weak/strong reference to orphaned dst rows have ref != NULL but that's fine because they'll both be cleaned up at the next step below.
>>>
>>> 4. ovsdb_idl_track_clear()
>>>
>>> What do you think?
>>
>> Sounds OK.  I'm not sure how the implementation should look like, though.
>> Will this also fix the issue with not reacking deleted orphaned rows right away?

I think so.

If not, I'll add it as a third patch in the series.

Thanks,
Dumitru
Han Zhou March 10, 2021, 8:33 a.m. UTC | #6
On Tue, Mar 9, 2021 at 9:03 AM Dumitru Ceara <dceara@redhat.com> wrote:
>
> On 3/9/21 5:56 PM, Ilya Maximets wrote:
> > On 3/9/21 5:54 PM, Ilya Maximets wrote:
> >> On 3/9/21 5:26 PM, Dumitru Ceara wrote:
> >>> On 3/9/21 4:07 PM, Ilya Maximets wrote:
> >>>> On 3/3/21 3:40 PM, Dumitru Ceara wrote:
> >>>>> Considering two DB rows, 'a' from table A and 'b' from table B (with
> >>>>> column 'ref_a' a reference to table A):
> >>>>> a = {A._uuid=<U1>}
> >>>>> b = {B._uuid=<U2>, B.ref_a=<U1>}
> >>>>>
> >>>>> Assuming both records are present in the IDL client's in-memory
view of
> >>>>> the database, depending whether row 'b' is also deleted in the same
> >>>>> transaction or not, deletion of row 'a' should generate the
following
> >>>>> tracked changes:
> >>>>>
> >>>>> 1. only row 'a' is deleted:
> >>>>> - for table A:
> >>>>>     - deleted records: a = {A._uuid=<U1>}
> >>>>> - for table B:
> >>>>>     - updated records: b = {B._uuid=<U2>, B.ref_a=[]}
> >>>>>
> >>>>> 2. row 'a' and row 'b' are deleted in the same update:
> >>>>> - for table A:
> >>>>>     - deleted records: a = {A._uuid=<U1>}
> >>>>> - for table B:
> >>>>>     - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}
> >>>>>
> >>>>> To ensure this, we now delay reparsing row backrefs until the row
has
> >>>>> been removed from the table's hmap and until the IDL client has
> >>>>> processed all tracked changes (ovsdb_idl_track_clear() was called).
> >>>>>
> >>>>> Without this change, in scenario 2 above, the tracked changes for
table
> >>>>> B would be:
> >>>>> - deleted records: b = {B._uuid=<U2>, B.ref_a=[]}
> >>>>>
> >>>>> In particular, for strong references, row 'a' can never be deleted
in
> >>>>> a transaction that happens strictly before row 'b' is deleted.  In
some
> >>>>> cases [0] both rows are deleted in the same transaction and having
> >>>>> B.ref_a=[] would violate the integrity of the database from client
> >>>>> perspective.  This would force the client to always validate that
> >>>>> strong reference fields are non-NULL.  This is not really an option
> >>>>> because the information in the original reference is required for
> >>>>> incrementally processing the record deletion.
> >>>>>
> >>>>> [0] with ovn-monitor-all=true, the following command triggers a
crash
> >>>>>       in ovn-controller because a strong reference field becomes
NULL:
> >>>>>       $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp
00:00:00:00:00:01 1.0.0.1/24
> >>>>>       $ ovn-nbctl lr-del r
> >>>>>
> >>>>> Reported-at: https://bugzilla.redhat.com/1932642
> >>>>> Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for
deleted rows.")
> >>>>> Signed-off-by: Dumitru Ceara <dceara@redhat.com>
> >>>>> ---
> >>>>> v2:
> >>>>>     - Added ovsdb-idl.at test for strong references.
> >>>>> ---
> >>>>>    lib/ovsdb-idl.c    |    6 ++-
> >>>>>    tests/ovsdb-idl.at |  118
++++++++++++++++++++++++++++++++++++++++++++++++++++
> >>>>>    tests/test-ovsdb.c |   45 ++++++++++++++++++++
> >>>>>    3 files changed, 167 insertions(+), 2 deletions(-)
> >>>>>
> >>>>> diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
> >>>>> index 9e1e787..ecd4924 100644
> >>>>> --- a/lib/ovsdb-idl.c
> >>>>> +++ b/lib/ovsdb-idl.c
> >>>>> @@ -163,6 +163,7 @@ static void ovsdb_idl_row_unparse(struct
ovsdb_idl_row *);
> >>>>>    static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
> >>>>>    static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
> >>>>>    static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *,
bool destroy_dsts);
> >>>>> +static void ovsdb_idl_row_reparse_backrefs(struct ovsdb_idl_row
*row);
> >>>>>      static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
> >>>>>    static bool ovsdb_idl_txn_extract_mutations(struct ovsdb_idl_row
*,
> >>>>> @@ -367,6 +368,8 @@ ovsdb_idl_clear(struct ovsdb_idl *db)
> >>>>>                    ovsdb_idl_row_unparse(row);
> >>>>>                }
> >>>>>                LIST_FOR_EACH_SAFE (arc, next_arc, src_node,
&row->src_arcs) {
> >>>>> +                ovs_list_remove(&arc->src_node);
> >>>>> +                ovs_list_remove(&arc->dst_node);
> >>>>>                    free(arc);
> >>>>>                }
> >>>>>                /* No need to do anything with dst_arcs: some node
has those arcs
> >>>>> @@ -1234,6 +1237,7 @@ ovsdb_idl_track_clear__(struct ovsdb_idl
*idl, bool flush_all)
> >>>>>                    ovs_list_remove(&row->track_node);
> >>>>>                    ovs_list_init(&row->track_node);
> >>>>>                    if (ovsdb_idl_row_is_orphan(row)) {
> >>>>> +                    ovsdb_idl_row_reparse_backrefs(row);
> >>>>>                        ovsdb_idl_row_unparse(row);
> >>>>>                        if (row->tracked_old_datum) {
> >>>>>                            const struct ovsdb_idl_table_class
*class =
> >>>>> @@ -2156,8 +2160,6 @@ ovsdb_idl_delete_row(struct ovsdb_idl_row
*row)
> >>>>>        ovsdb_idl_row_clear_old(row);
> >>>>>        if (ovs_list_is_empty(&row->dst_arcs)) {
> >>>>>            ovsdb_idl_row_destroy(row);
> >>>>> -    } else {
> >>>>> -        ovsdb_idl_row_reparse_backrefs(row);
> >>>>>        }
> >>>>
> >>>> By removing the ovsdb_idl_row_reparse_backrefs() you're making an
assumption
> >>>> that we always have a modification event for all rows that are
referencing
> >>>> this one.  But that is not always the case.
> >>>>
> >>>> There is scenario where this re-parsing is needed.  In short, if
you're
> >>>> reducing the scope of conditional monitoring and some rows becomes
> >>>> orphan rows.  In this case ovsdb-server will not generate 'modify'
event
> >>>> for the row that references our newly orphan row, so it will not be
> >>>> re-parsed and these orphan rows will be accessible the row that
references
> >>>> them.
> >>>
> >>> You're right, thanks for spotting this and for the test case.
> >>>
> >>>>
> >>>> Here is a quick'n'dirty testcase, how to reproduce:
> >>>> diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
> >>>> index 702502280..c6bb3f348 100644
> >>>> --- a/tests/ovsdb-idl.at
> >>>> +++ b/tests/ovsdb-idl.at
> >>>> @@ -1174,7 +1174,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl,
initially populated],
> >>>>    dnl This test creates database with weak references and checks
that orphan
> >>>>    dnl rows created for weak references are not available for
iteration via
> >>>>    dnl list of tracked changes.
> >>>> -OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated,
orphan weak references],
> >>>> +OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated,
orphan weak references quickndirty],
> >>>>      [['["idltest",
> >>>>          {"op": "insert",
> >>>>           "table": "simple",
> >>>> @@ -1196,7 +1196,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl,
initially populated, orphan weak refer
> >>>>                                  ["named-uuid", "weak_row1"],
> >>>>                                  ["named-uuid", "weak_row2"]]
> >>>>                               ]}}]']],
> >>>> -  [['condition simple []' \
> >>>> +  [['condition simple [true]' \
> >>>>        'condition simple [["s","==","row1_s"]]' \
> >>>>        '["idltest",
> >>>>          {"op": "update",
> >>>> diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
> >>>> index 36fb9cc08..ace87b13d 100644
> >>>> --- a/tests/test-ovsdb.c
> >>>> +++ b/tests/test-ovsdb.c
> >>>> @@ -2157,6 +2157,7 @@ print_idl_row_singleton(const struct
idltest_singleton *sng, int step)
> >>>>    static void
> >>>>    print_idl(struct ovsdb_idl *idl, int step)
> >>>>    {
> >>>> +    const struct idltest_simple6 *s6;
> >>>>        const struct idltest_simple *s;
> >>>>        const struct idltest_link1 *l1;
> >>>>        const struct idltest_link2 *l2;
> >>>> @@ -2175,6 +2176,10 @@ print_idl(struct ovsdb_idl *idl, int step)
> >>>>            print_idl_row_link2(l2, step);
> >>>>            n++;
> >>>>        }
> >>>> +    IDLTEST_SIMPLE6_FOR_EACH (s6, idl) {
> >>>> +        print_idl_row_simple6(s6, step);
> >>>> +        n++;
> >>>> +    }
> >>>>        IDLTEST_SINGLETON_FOR_EACH (sng, idl) {
> >>>>            print_idl_row_singleton(sng, step);
> >>>>            n++;
> >>>> @@ -2638,8 +2643,12 @@ do_idl(struct ovs_cmdl_context *ctx)
> >>>>                  /* Print update. */
> >>>>                if (track) {
> >>>> -                print_idl_track(idl, step++);
> >>>> +                print_idl_track(idl, step);
> >>>> +                print_and_log("%03d: print_idl[1]", step);
> >>>> +                print_idl(idl, step);
> >>>>                    ovsdb_idl_track_clear(idl);
> >>>> +                print_and_log("%03d: print_idl[2]", step);
> >>>> +                print_idl(idl, step++);
> >>>>                } else {
> >>>>                    print_idl(idl, step++);
> >>>>                }
> >>>> ---
> >>>>
> >>>> By running make -j8 check TESTSUITEFLAGS='-k quickndirty -v', you
will have
> >>>> some output for the commands in the log.
> >>>>
> >>>>
> >>>> The first condition for a 'simple' table is 'true', so we will
receive all
> >>>> 3 rows from this table and also row from the 'simple6'.
> >>>> All 3 rows are in a weak references:
> >>>>
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <>
uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
> >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <>
uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
> >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <>
uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
> >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> >>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row:
name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada
1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
> >>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name
weak_ref
> >>>>
> >>>>
> >>>> All of them are accessible for a user before the track_clear():
> >>>>
> >>>> test-ovsdb|test_ovsdb|001: print_idl[1]
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <>
uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
> >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <>
uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
> >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <>
uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
> >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
> >>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row:
name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada
1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
> >>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name
weak_ref
> >>>>
> >>>> And after:
> >>>>
> >>>> test-ovsdb|test_ovsdb|001: print_idl[2]
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s <>
uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s <>
uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
> >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s <>
uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
> >>>> test-ovsdb|test_ovsdb|001: table simple6: name=first_row
weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada
1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
> >>>>
> >>>>
> >>>> Now condition is changed.  This makes 2 of 3 rows to become orphan
rows.
> >>>> Note that the update notification only removes orphan rows, but
doesn't
> >>>> modify the row in simple6, because rows wasn't actually removed, just
> >>>> not needed anymore:
> >>>>
> >>>> test-ovsdb|jsonrpc|unix:socket: send request,
method="monitor_cond_change",
params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}],
id=5
> >>>> test-ovsdb|jsonrpc|unix:socket: received notification,
method="update3",
params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"3aa79e4a-d752-4d53-8e16-b62887d73ad8":{"delete":null},"d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a":{"delete":null}}}]
> >>>> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
> >>>>
> >>>> Track list is empty, but 2 orphan rows should be on a track list as
deleted:
> >>>
> >>> This actually uncovers a new potential issue: the 2 orphan rows are
not on the tracked list even without my change.  That's because
ovsdb_idl_row_destroy() doesn't get called if the row becomes orphan so it
won't be added to 'row->table->track_list'.
> >>
> >> Hmm.  Thanks for sponning that.
> >
> > *spotting
> >
> >>
> >> On current master these 2 rows appears on a tracked list only after
> >> occasional unrelated modification of a row in 'simple6' and that is
> >> not good:
> >>
> >> test-ovsdb|test_ovsdb|002: change conditions
> >> test-ovsdb|jsonrpc|unix:socket: send request,
method="monitor_cond_change",
params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}],
id=5
> >> test-ovsdb|jsonrpc|unix:socket: received notification,
method="update3",
params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78":{"delete":null},"5a1d0257-fc27-4046-9e18-25539f122052":{"delete":null}}}]
> >> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
> >> test-ovsdb|test_ovsdb|003: empty
> >> test-ovsdb|test_ovsdb|003: print_idl[1]
> >> test-ovsdb|test_ovsdb|003: table simple: s=row1_s
uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
> >> test-ovsdb|test_ovsdb|003: table simple6: name=first_row
weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
> >> test-ovsdb|test_ovsdb|003: print_idl[2]
> >> test-ovsdb|test_ovsdb|003: table simple: s=row1_s
uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
> >> test-ovsdb|test_ovsdb|003: table simple6: name=first_row
weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
> >>
> >> test-ovsdb|jsonrpc|unix:socket: send request, method="transact",
params=["idltest",{"where":[],"row":{"name":"new_name"},"op":"update","table":"simple6"}],
id=6
> >> test-ovsdb|jsonrpc|unix:socket: received reply, result=[{"count":1}],
id=6
> >> test-ovsdb|test_ovsdb|004: {"error":null,"result":[{"count":1}]}
> >> test-ovsdb|jsonrpc|unix:socket: received notification,
method="update3",
params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"618cef34-e635-4841-9dc7-a14eb2b9627e":{"modify":{"name":"new_name"}}}}]
> >> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s
uuid=5a1d0257-fc27-4046-9e18-25539f122052
> >> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s
uuid=0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78
> >> test-ovsdb|test_ovsdb|005: table simple6: name=new_name
weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
> >> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
> >>
> >>
> >> It's probably because ovsdb_idl_row_reparse_backrefs() calls
> >> ovsdb_idl_row_clear_arcs() with destroy_dsts=false.  So, re-parsing
> >> invoked for two deleted rows doesn't actually destroy them.
> >>
> >>>
> >>>>
> >>>> test-ovsdb|test_ovsdb|003: empty
> >>>>
> >>>> But all 3 rows are still accessible for a user from the row in
'simple6' even
> >>>> though table 'simple' has only 1 row accessible:
> >>>>
> >>>> test-ovsdb|test_ovsdb|003: print_idl[1]
> >>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <>
uuid=b8315694-7855-43e7-a963-ca9a8666682a
> >>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row
weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8
b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
> >>>>
> >>>> And they are still here after the track_clear():
> >>>>
> >>>> test-ovsdb|test_ovsdb|003: print_idl[2]
> >>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <>
uuid=b8315694-7855-43e7-a963-ca9a8666682a
> >>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row
weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8
b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
> >>>>
> >>>>
> >>>> When we have unrelated update on a row in 'simple6' it finally gets
> >>>> re-parsed and we have tracked orphan rows as deleted and they are
> >>>> no longer accessible for a user from a row in 'simple6':
> >>>>
> >>>> test-ovsdb|jsonrpc|unix:socket: received notification,
method="update3",
params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"6fcb8599-4161-4314-901c-2ca4a18686c3":{"modify":{"name":"new_name"}}}}]
> >>>>
> >>>> Tracked:
> >>>>
> >>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s <>
uuid=d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a
> >>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s <>
uuid=3aa79e4a-d752-4d53-8e16-b62887d73ad8
> >>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name
weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
> >>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
> >>>>
> >>>> Before track_clear():
> >>>>
> >>>> test-ovsdb|test_ovsdb|005: print_idl[1]
> >>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <>
uuid=b8315694-7855-43e7-a963-ca9a8666682a
> >>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name
weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
> >>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
> >>>>
> >>>> After:
> >>>>
> >>>> test-ovsdb|test_ovsdb|005: print_idl[2]
> >>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <>
uuid=b8315694-7855-43e7-a963-ca9a8666682a
> >>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name
weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
> >>>>
> >>>
> >>> To cover all cases above I'm thinking of the following approach:
> >>>
> >>> 1. Delay ovsdb_idl_row_reparse_backrefs() for deleted rows until
ovsdb_idl_parse_update() has finished.
> >>>
> >>> 2. After ovsdb_idl_parse_update(), walk all deleted rows and reparse
backrefs but only for src rows that have not been deleted.

Sounds good. This step seems natural because in step 1)
ovsdb_idl_delete_row() will clear forward arcs for deleted rows, so in this
step 2) it won't reparse for deleted src rows.
I *hope* it covers all cases. This part of code becomes really tricky. I
think maybe the tracking code needs a refactor, but I am also worried about
creating even more problems. (I haven't checked yet how ddlog change
tracking is done).

> >>>
> >>> 3. IDL user (e.g., ovn-controller) processes tracked changes.  At
this point:
> >>> - non-deleted src rows with a weak reference to orphaned dst rows
have ref == NULL.
> >>> - deleted src rows with a weak/strong reference to orphaned dst rows
have ref != NULL but that's fine because they'll both be cleaned up at the
next step below.
> >>>
> >>> 4. ovsdb_idl_track_clear()
> >>>
> >>> What do you think?
> >>
> >> Sounds OK.  I'm not sure how the implementation should look like,
though.
> >> Will this also fix the issue with not reacking deleted orphaned rows
right away?
>
> I think so.
>
> If not, I'll add it as a third patch in the series.
>
> Thanks,
> Dumitru
>
Dumitru Ceara March 12, 2021, 12:11 p.m. UTC | #7
On 3/10/21 9:33 AM, Han Zhou wrote:
> 
> 
> On Tue, Mar 9, 2021 at 9:03 AM Dumitru Ceara <dceara@redhat.com 
> <mailto:dceara@redhat.com>> wrote:
>  >
>  > On 3/9/21 5:56 PM, Ilya Maximets wrote:
>  > > On 3/9/21 5:54 PM, Ilya Maximets wrote:
>  > >> On 3/9/21 5:26 PM, Dumitru Ceara wrote:
>  > >>> On 3/9/21 4:07 PM, Ilya Maximets wrote:
>  > >>>> On 3/3/21 3:40 PM, Dumitru Ceara wrote:
>  > >>>>> Considering two DB rows, 'a' from table A and 'b' from table B 
> (with
>  > >>>>> column 'ref_a' a reference to table A):
>  > >>>>> a = {A._uuid=<U1>}
>  > >>>>> b = {B._uuid=<U2>, B.ref_a=<U1>}
>  > >>>>>
>  > >>>>> Assuming both records are present in the IDL client's in-memory 
> view of
>  > >>>>> the database, depending whether row 'b' is also deleted in the same
>  > >>>>> transaction or not, deletion of row 'a' should generate the 
> following
>  > >>>>> tracked changes:
>  > >>>>>
>  > >>>>> 1. only row 'a' is deleted:
>  > >>>>> - for table A:
>  > >>>>>     - deleted records: a = {A._uuid=<U1>}
>  > >>>>> - for table B:
>  > >>>>>     - updated records: b = {B._uuid=<U2>, B.ref_a=[]}
>  > >>>>>
>  > >>>>> 2. row 'a' and row 'b' are deleted in the same update:
>  > >>>>> - for table A:
>  > >>>>>     - deleted records: a = {A._uuid=<U1>}
>  > >>>>> - for table B:
>  > >>>>>     - deleted records: b = {B._uuid=<U2>, B.ref_a=<U1>}
>  > >>>>>
>  > >>>>> To ensure this, we now delay reparsing row backrefs until the 
> row has
>  > >>>>> been removed from the table's hmap and until the IDL client has
>  > >>>>> processed all tracked changes (ovsdb_idl_track_clear() was called).
>  > >>>>>
>  > >>>>> Without this change, in scenario 2 above, the tracked changes 
> for table
>  > >>>>> B would be:
>  > >>>>> - deleted records: b = {B._uuid=<U2>, B.ref_a=[]}
>  > >>>>>
>  > >>>>> In particular, for strong references, row 'a' can never be 
> deleted in
>  > >>>>> a transaction that happens strictly before row 'b' is deleted.  
> In some
>  > >>>>> cases [0] both rows are deleted in the same transaction and having
>  > >>>>> B.ref_a=[] would violate the integrity of the database from client
>  > >>>>> perspective.  This would force the client to always validate that
>  > >>>>> strong reference fields are non-NULL.  This is not really an option
>  > >>>>> because the information in the original reference is required for
>  > >>>>> incrementally processing the record deletion.
>  > >>>>>
>  > >>>>> [0] with ovn-monitor-all=true, the following command triggers a 
> crash
>  > >>>>>       in ovn-controller because a strong reference field 
> becomes NULL:
>  > >>>>>       $ ovn-nbctl --wait=hv -- lr-add r -- lrp-add r rp 
> 00:00:00:00:00:01 1.0.0.1/24 <http://1.0.0.1/24>
>  > >>>>>       $ ovn-nbctl lr-del r
>  > >>>>>
>  > >>>>> Reported-at: https://bugzilla.redhat.com/1932642 
> <https://bugzilla.redhat.com/1932642>
>  > >>>>> Fixes: 72aeb243a52a ("ovsdb-idl: Tracking - preserve data for 
> deleted rows.")
>  > >>>>> Signed-off-by: Dumitru Ceara <dceara@redhat.com 
> <mailto:dceara@redhat.com>>
>  > >>>>> ---
>  > >>>>> v2:
>  > >>>>>     - Added ovsdb-idl.at <http://ovsdb-idl.at> test for strong 
> references.
>  > >>>>> ---
>  > >>>>>    lib/ovsdb-idl.c    |    6 ++-
>  > >>>>>    tests/ovsdb-idl.at <http://ovsdb-idl.at> |  118 
> ++++++++++++++++++++++++++++++++++++++++++++++++++++
>  > >>>>>    tests/test-ovsdb.c |   45 ++++++++++++++++++++
>  > >>>>>    3 files changed, 167 insertions(+), 2 deletions(-)
>  > >>>>>
>  > >>>>> diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
>  > >>>>> index 9e1e787..ecd4924 100644
>  > >>>>> --- a/lib/ovsdb-idl.c
>  > >>>>> +++ b/lib/ovsdb-idl.c
>  > >>>>> @@ -163,6 +163,7 @@ static void ovsdb_idl_row_unparse(struct 
> ovsdb_idl_row *);
>  > >>>>>    static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
>  > >>>>>    static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
>  > >>>>>    static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *, 
> bool destroy_dsts);
>  > >>>>> +static void ovsdb_idl_row_reparse_backrefs(struct 
> ovsdb_idl_row *row);
>  > >>>>>      static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
>  > >>>>>    static bool ovsdb_idl_txn_extract_mutations(struct 
> ovsdb_idl_row *,
>  > >>>>> @@ -367,6 +368,8 @@ ovsdb_idl_clear(struct ovsdb_idl *db)
>  > >>>>>                    ovsdb_idl_row_unparse(row);
>  > >>>>>                }
>  > >>>>>                LIST_FOR_EACH_SAFE (arc, next_arc, src_node, 
> &row->src_arcs) {
>  > >>>>> +                ovs_list_remove(&arc->src_node);
>  > >>>>> +                ovs_list_remove(&arc->dst_node);
>  > >>>>>                    free(arc);
>  > >>>>>                }
>  > >>>>>                /* No need to do anything with dst_arcs: some 
> node has those arcs
>  > >>>>> @@ -1234,6 +1237,7 @@ ovsdb_idl_track_clear__(struct ovsdb_idl 
> *idl, bool flush_all)
>  > >>>>>                    ovs_list_remove(&row->track_node);
>  > >>>>>                    ovs_list_init(&row->track_node);
>  > >>>>>                    if (ovsdb_idl_row_is_orphan(row)) {
>  > >>>>> +                    ovsdb_idl_row_reparse_backrefs(row);
>  > >>>>>                        ovsdb_idl_row_unparse(row);
>  > >>>>>                        if (row->tracked_old_datum) {
>  > >>>>>                            const struct ovsdb_idl_table_class 
> *class =
>  > >>>>> @@ -2156,8 +2160,6 @@ ovsdb_idl_delete_row(struct ovsdb_idl_row 
> *row)
>  > >>>>>        ovsdb_idl_row_clear_old(row);
>  > >>>>>        if (ovs_list_is_empty(&row->dst_arcs)) {
>  > >>>>>            ovsdb_idl_row_destroy(row);
>  > >>>>> -    } else {
>  > >>>>> -        ovsdb_idl_row_reparse_backrefs(row);
>  > >>>>>        }
>  > >>>>
>  > >>>> By removing the ovsdb_idl_row_reparse_backrefs() you're making 
> an assumption
>  > >>>> that we always have a modification event for all rows that are 
> referencing
>  > >>>> this one.  But that is not always the case.
>  > >>>>
>  > >>>> There is scenario where this re-parsing is needed.  In short, if 
> you're
>  > >>>> reducing the scope of conditional monitoring and some rows becomes
>  > >>>> orphan rows.  In this case ovsdb-server will not generate 
> 'modify' event
>  > >>>> for the row that references our newly orphan row, so it will not be
>  > >>>> re-parsed and these orphan rows will be accessible the row that 
> references
>  > >>>> them.
>  > >>>
>  > >>> You're right, thanks for spotting this and for the test case.
>  > >>>
>  > >>>>
>  > >>>> Here is a quick'n'dirty testcase, how to reproduce:
>  > >>>> diff --git a/tests/ovsdb-idl.at <http://ovsdb-idl.at> 
> b/tests/ovsdb-idl.at <http://ovsdb-idl.at>
>  > >>>> index 702502280..c6bb3f348 100644
>  > >>>> --- a/tests/ovsdb-idl.at <http://ovsdb-idl.at>
>  > >>>> +++ b/tests/ovsdb-idl.at <http://ovsdb-idl.at>
>  > >>>> @@ -1174,7 +1174,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, 
> initially populated],
>  > >>>>    dnl This test creates database with weak references and 
> checks that orphan
>  > >>>>    dnl rows created for weak references are not available for 
> iteration via
>  > >>>>    dnl list of tracked changes.
>  > >>>> -OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, 
> orphan weak references],
>  > >>>> +OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, 
> orphan weak references quickndirty],
>  > >>>>      [['["idltest",
>  > >>>>          {"op": "insert",
>  > >>>>           "table": "simple",
>  > >>>> @@ -1196,7 +1196,7 @@ OVSDB_CHECK_IDL_TRACK([track, simple idl, 
> initially populated, orphan weak refer
>  > >>>>                                  ["named-uuid", "weak_row1"],
>  > >>>>                                  ["named-uuid", "weak_row2"]]
>  > >>>>                               ]}}]']],
>  > >>>> -  [['condition simple []' \
>  > >>>> +  [['condition simple [true]' \
>  > >>>>        'condition simple [["s","==","row1_s"]]' \
>  > >>>>        '["idltest",
>  > >>>>          {"op": "update",
>  > >>>> diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
>  > >>>> index 36fb9cc08..ace87b13d 100644
>  > >>>> --- a/tests/test-ovsdb.c
>  > >>>> +++ b/tests/test-ovsdb.c
>  > >>>> @@ -2157,6 +2157,7 @@ print_idl_row_singleton(const struct 
> idltest_singleton *sng, int step)
>  > >>>>    static void
>  > >>>>    print_idl(struct ovsdb_idl *idl, int step)
>  > >>>>    {
>  > >>>> +    const struct idltest_simple6 *s6;
>  > >>>>        const struct idltest_simple *s;
>  > >>>>        const struct idltest_link1 *l1;
>  > >>>>        const struct idltest_link2 *l2;
>  > >>>> @@ -2175,6 +2176,10 @@ print_idl(struct ovsdb_idl *idl, int step)
>  > >>>>            print_idl_row_link2(l2, step);
>  > >>>>            n++;
>  > >>>>        }
>  > >>>> +    IDLTEST_SIMPLE6_FOR_EACH (s6, idl) {
>  > >>>> +        print_idl_row_simple6(s6, step);
>  > >>>> +        n++;
>  > >>>> +    }
>  > >>>>        IDLTEST_SINGLETON_FOR_EACH (sng, idl) {
>  > >>>>            print_idl_row_singleton(sng, step);
>  > >>>>            n++;
>  > >>>> @@ -2638,8 +2643,12 @@ do_idl(struct ovs_cmdl_context *ctx)
>  > >>>>                  /* Print update. */
>  > >>>>                if (track) {
>  > >>>> -                print_idl_track(idl, step++);
>  > >>>> +                print_idl_track(idl, step);
>  > >>>> +                print_and_log("%03d: print_idl[1]", step);
>  > >>>> +                print_idl(idl, step);
>  > >>>>                    ovsdb_idl_track_clear(idl);
>  > >>>> +                print_and_log("%03d: print_idl[2]", step);
>  > >>>> +                print_idl(idl, step++);
>  > >>>>                } else {
>  > >>>>                    print_idl(idl, step++);
>  > >>>>                }
>  > >>>> ---
>  > >>>>
>  > >>>> By running make -j8 check TESTSUITEFLAGS='-k quickndirty -v', 
> you will have
>  > >>>> some output for the commands in the log.
>  > >>>>
>  > >>>>
>  > >>>> The first condition for a 'simple' table is 'true', so we will 
> receive all
>  > >>>> 3 rows from this table and also row from the 'simple6'.
>  > >>>> All 3 rows are in a weak references:
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s 
> <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s 
> <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s 
> <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>  > >>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: 
> name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 
> 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>  > >>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name 
> weak_ref
>  > >>>>
>  > >>>>
>  > >>>> All of them are accessible for a user before the track_clear():
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|001: print_idl[1]
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s 
> <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s 
> <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s 
> <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: updated columns: s
>  > >>>> test-ovsdb|test_ovsdb|001: table simple6: inserted row: 
> name=first_row weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 
> 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>  > >>>> test-ovsdb|test_ovsdb|001: table simple6: updated columns: name 
> weak_ref
>  > >>>>
>  > >>>> And after:
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|001: print_idl[2]
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row0_s 
> <> uuid=2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row2_s 
> <> uuid=1fd88ded-06f0-4fee-a21b-4e552d8a2f50
>  > >>>> test-ovsdb|test_ovsdb|001: table simple: inserted row: s=row1_s 
> <> uuid=124ddccb-7eb8-4def-b768-887cb8d84ada
>  > >>>> test-ovsdb|test_ovsdb|001: table simple6: name=first_row 
> weak_ref=[124ddccb-7eb8-4def-b768-887cb8d84ada 
> 1fd88ded-06f0-4fee-a21b-4e552d8a2f50 2309fce8-a6dd-46e5-a603-fe1ea1ab6bb0]
>  > >>>>
>  > >>>>
>  > >>>> Now condition is changed.  This makes 2 of 3 rows to become 
> orphan rows.
>  > >>>> Note that the update notification only removes orphan rows, but 
> doesn't
>  > >>>> modify the row in simple6, because rows wasn't actually removed, 
> just
>  > >>>> not needed anymore:
>  > >>>>
>  > >>>> test-ovsdb|jsonrpc|unix:socket: send request, 
> method="monitor_cond_change", 
> params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], 
> id=5
>  > >>>> test-ovsdb|jsonrpc|unix:socket: received notification, 
> method="update3", 
> params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"3aa79e4a-d752-4d53-8e16-b62887d73ad8":{"delete":null},"d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a":{"delete":null}}}]
>  > >>>> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
>  > >>>>
>  > >>>> Track list is empty, but 2 orphan rows should be on a track list 
> as deleted:
>  > >>>
>  > >>> This actually uncovers a new potential issue: the 2 orphan rows 
> are not on the tracked list even without my change.  That's because 
> ovsdb_idl_row_destroy() doesn't get called if the row becomes orphan so 
> it won't be added to 'row->table->track_list'.
>  > >>
>  > >> Hmm.  Thanks for sponning that.
>  > >
>  > > *spotting
>  > >
>  > >>
>  > >> On current master these 2 rows appears on a tracked list only after
>  > >> occasional unrelated modification of a row in 'simple6' and that is
>  > >> not good:
>  > >>
>  > >> test-ovsdb|test_ovsdb|002: change conditions
>  > >> test-ovsdb|jsonrpc|unix:socket: send request, 
> method="monitor_cond_change", 
> params=[["monid","idltest"],["monid","idltest"],{"simple":[{"where":[["s","==","row1_s"]]}]}], 
> id=5
>  > >> test-ovsdb|jsonrpc|unix:socket: received notification, 
> method="update3", 
> params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple":{"0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78":{"delete":null},"5a1d0257-fc27-4046-9e18-25539f122052":{"delete":null}}}]
>  > >> test-ovsdb|jsonrpc|unix:socket: received reply, result={}, id=5
>  > >> test-ovsdb|test_ovsdb|003: empty
>  > >> test-ovsdb|test_ovsdb|003: print_idl[1]
>  > >> test-ovsdb|test_ovsdb|003: table simple: s=row1_s 
> uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
>  > >> test-ovsdb|test_ovsdb|003: table simple6: name=first_row 
> weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
>  > >> test-ovsdb|test_ovsdb|003: print_idl[2]
>  > >> test-ovsdb|test_ovsdb|003: table simple: s=row1_s 
> uuid=77db422f-042f-4e1f-9a40-6487e5cdd3ae
>  > >> test-ovsdb|test_ovsdb|003: table simple6: name=first_row 
> weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
>  > >>
>  > >> test-ovsdb|jsonrpc|unix:socket: send request, method="transact", 
> params=["idltest",{"where":[],"row":{"name":"new_name"},"op":"update","table":"simple6"}], 
> id=6
>  > >> test-ovsdb|jsonrpc|unix:socket: received reply, 
> result=[{"count":1}], id=6
>  > >> test-ovsdb|test_ovsdb|004: {"error":null,"result":[{"count":1}]}
>  > >> test-ovsdb|jsonrpc|unix:socket: received notification, 
> method="update3", 
> params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"618cef34-e635-4841-9dc7-a14eb2b9627e":{"modify":{"name":"new_name"}}}}]
>  > >> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s 
> uuid=5a1d0257-fc27-4046-9e18-25539f122052
>  > >> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s 
> uuid=0f94ec57-2fc2-4a70-a0cf-b96b11fe0d78
>  > >> test-ovsdb|test_ovsdb|005: table simple6: name=new_name 
> weak_ref=[77db422f-042f-4e1f-9a40-6487e5cdd3ae]
>  > >> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>  > >>
>  > >>
>  > >> It's probably because ovsdb_idl_row_reparse_backrefs() calls
>  > >> ovsdb_idl_row_clear_arcs() with destroy_dsts=false.  So, re-parsing
>  > >> invoked for two deleted rows doesn't actually destroy them.
>  > >>
>  > >>>
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|003: empty
>  > >>>>
>  > >>>> But all 3 rows are still accessible for a user from the row in 
> 'simple6' even
>  > >>>> though table 'simple' has only 1 row accessible:
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|003: print_idl[1]
>  > >>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> 
> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>  > >>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row 
> weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 
> b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>  > >>>>
>  > >>>> And they are still here after the track_clear():
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|003: print_idl[2]
>  > >>>> test-ovsdb|test_ovsdb|003: table simple: s=row1_s <> 
> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>  > >>>> test-ovsdb|test_ovsdb|003: table simple6: name=first_row 
> weak_ref=[3aa79e4a-d752-4d53-8e16-b62887d73ad8 
> b8315694-7855-43e7-a963-ca9a8666682a d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a]
>  > >>>>
>  > >>>>
>  > >>>> When we have unrelated update on a row in 'simple6' it finally gets
>  > >>>> re-parsed and we have tracked orphan rows as deleted and they are
>  > >>>> no longer accessible for a user from a row in 'simple6':
>  > >>>>
>  > >>>> test-ovsdb|jsonrpc|unix:socket: received notification, 
> method="update3", 
> params=[["monid","idltest"],"00000000-0000-0000-0000-000000000000",{"simple6":{"6fcb8599-4161-4314-901c-2ca4a18686c3":{"modify":{"name":"new_name"}}}}]
>  > >>>>
>  > >>>> Tracked:
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row0_s 
> <> uuid=d1b023f3-3b4d-4bb5-b4d9-cc9791afde7a
>  > >>>> test-ovsdb|test_ovsdb|005: table simple: deleted row: s=row2_s 
> <> uuid=3aa79e4a-d752-4d53-8e16-b62887d73ad8
>  > >>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name 
> weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>  > >>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>  > >>>>
>  > >>>> Before track_clear():
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|005: print_idl[1]
>  > >>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> 
> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>  > >>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name 
> weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>  > >>>> test-ovsdb|test_ovsdb|005: table simple6: updated columns: name
>  > >>>>
>  > >>>> After:
>  > >>>>
>  > >>>> test-ovsdb|test_ovsdb|005: print_idl[2]
>  > >>>> test-ovsdb|test_ovsdb|005: table simple: s=row1_s <> 
> uuid=b8315694-7855-43e7-a963-ca9a8666682a
>  > >>>> test-ovsdb|test_ovsdb|005: table simple6: name=new_name 
> weak_ref=[b8315694-7855-43e7-a963-ca9a8666682a]
>  > >>>>
>  > >>>
>  > >>> To cover all cases above I'm thinking of the following approach:
>  > >>>
>  > >>> 1. Delay ovsdb_idl_row_reparse_backrefs() for deleted rows until 
> ovsdb_idl_parse_update() has finished.
>  > >>>
>  > >>> 2. After ovsdb_idl_parse_update(), walk all deleted rows and 
> reparse backrefs but only for src rows that have not been deleted.
> 
> Sounds good. This step seems natural because in step 1) 
> ovsdb_idl_delete_row() will clear forward arcs for deleted rows, so in 
> this step 2) it won't reparse for deleted src rows.
> I *hope* it covers all cases. This part of code becomes really tricky. I 
> think maybe the tracking code needs a refactor, but I am also worried 
> about creating even more problems. (I haven't checked yet how ddlog 
> change tracking is done).
> 

Thanks, Han, Ilya and Numan for reviews!

I just sent a v3:
https://patchwork.ozlabs.org/project/openvswitch/list/?series=233663

Numan, I didn't add your ack because I had to make quite a few changes 
to address the bug Ilya pointed out above.  I'd appreciate it very much 
if you could have another look at v3.

Thanks,
Dumitru
diff mbox series

Patch

diff --git a/lib/ovsdb-idl.c b/lib/ovsdb-idl.c
index 9e1e787..ecd4924 100644
--- a/lib/ovsdb-idl.c
+++ b/lib/ovsdb-idl.c
@@ -163,6 +163,7 @@  static void ovsdb_idl_row_unparse(struct ovsdb_idl_row *);
 static void ovsdb_idl_row_clear_old(struct ovsdb_idl_row *);
 static void ovsdb_idl_row_clear_new(struct ovsdb_idl_row *);
 static void ovsdb_idl_row_clear_arcs(struct ovsdb_idl_row *, bool destroy_dsts);
+static void ovsdb_idl_row_reparse_backrefs(struct ovsdb_idl_row *row);
 
 static void ovsdb_idl_txn_abort_all(struct ovsdb_idl *);
 static bool ovsdb_idl_txn_extract_mutations(struct ovsdb_idl_row *,
@@ -367,6 +368,8 @@  ovsdb_idl_clear(struct ovsdb_idl *db)
                 ovsdb_idl_row_unparse(row);
             }
             LIST_FOR_EACH_SAFE (arc, next_arc, src_node, &row->src_arcs) {
+                ovs_list_remove(&arc->src_node);
+                ovs_list_remove(&arc->dst_node);
                 free(arc);
             }
             /* No need to do anything with dst_arcs: some node has those arcs
@@ -1234,6 +1237,7 @@  ovsdb_idl_track_clear__(struct ovsdb_idl *idl, bool flush_all)
                 ovs_list_remove(&row->track_node);
                 ovs_list_init(&row->track_node);
                 if (ovsdb_idl_row_is_orphan(row)) {
+                    ovsdb_idl_row_reparse_backrefs(row);
                     ovsdb_idl_row_unparse(row);
                     if (row->tracked_old_datum) {
                         const struct ovsdb_idl_table_class *class =
@@ -2156,8 +2160,6 @@  ovsdb_idl_delete_row(struct ovsdb_idl_row *row)
     ovsdb_idl_row_clear_old(row);
     if (ovs_list_is_empty(&row->dst_arcs)) {
         ovsdb_idl_row_destroy(row);
-    } else {
-        ovsdb_idl_row_reparse_backrefs(row);
     }
 }
 
diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
index 610680c..7025022 100644
--- a/tests/ovsdb-idl.at
+++ b/tests/ovsdb-idl.at
@@ -1269,6 +1269,124 @@  OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, orphan rows, cond
 010: done
 ]])
 
+dnl This test checks that deleting the destination of a reference updates the
+dnl source tracked record.
+OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, references, single delete],
+  [['["idltest",
+      {"op": "insert",
+       "table": "simple",
+       "row": {"s": "row0_s"},
+       "uuid-name": "uuid_row0_s"},
+      {"op": "insert",
+       "table": "simple6",
+       "row": {"name": "row0_s6",
+               "weak_ref": ["set",
+                             [["named-uuid", "uuid_row0_s"]]
+                           ]}}]']],
+  [['condition simple [true]; simple6 [true]' \
+    '["idltest",
+      {"op": "delete",
+       "table": "simple",
+       "where": []}]' \
+    '["idltest",
+      {"op": "insert",
+       "table": "simple",
+       "row": {"s": "row0_s"}}]']],
+  [[000: change conditions
+001: table simple6: inserted row: name=row0_s6 weak_ref=[<0>] uuid=<1>
+001: table simple6: updated columns: name weak_ref
+001: table simple: inserted row: i=0 r=0 b=false s=row0_s u=<2> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<0>
+001: table simple: updated columns: s
+002: {"error":null,"result":[{"count":1}]}
+003: table simple6: name=row0_s6 weak_ref=[] uuid=<1>
+003: table simple6: updated columns: weak_ref
+003: table simple: deleted row: i=0 r=0 b=false s=row0_s u=<2> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<0>
+004: {"error":null,"result":[{"uuid":["uuid","<3>"]}]}
+005: table simple: inserted row: i=0 r=0 b=false s=row0_s u=<2> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<3>
+005: table simple: updated columns: s
+006: done
+]])
+
+dnl This test checks that deleting both the destination and source of the
+dnl reference doesn't remove the reference the source tracked record.
+OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, weak references, multiple deletes],
+  [['["idltest",
+      {"op": "insert",
+       "table": "simple",
+       "row": {"s": "row0_s"},
+       "uuid-name": "uuid_row0_s"},
+      {"op": "insert",
+       "table": "simple6",
+       "row": {"name": "row0_s6",
+               "weak_ref": ["set",
+                             [["named-uuid", "uuid_row0_s"]]
+                           ]}}]']],
+  [['condition simple [true]; simple6 [true]' \
+    '["idltest",
+      {"op": "delete",
+       "table": "simple",
+       "where": []},
+      {"op": "delete",
+       "table": "simple6",
+       "where": []}]' \
+    '["idltest",
+      {"op": "insert",
+       "table": "simple",
+       "row": {"s": "row0_s"}}]']],
+  [[000: change conditions
+001: table simple6: inserted row: name=row0_s6 weak_ref=[<0>] uuid=<1>
+001: table simple6: updated columns: name weak_ref
+001: table simple: inserted row: i=0 r=0 b=false s=row0_s u=<2> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<0>
+001: table simple: updated columns: s
+002: {"error":null,"result":[{"count":1},{"count":1}]}
+003: table simple6: deleted row: name=row0_s6 weak_ref=[<0>] uuid=<1>
+003: table simple: deleted row: i=0 r=0 b=false s=row0_s u=<2> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<0>
+004: {"error":null,"result":[{"uuid":["uuid","<3>"]}]}
+005: table simple: inserted row: i=0 r=0 b=false s=row0_s u=<2> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<3>
+005: table simple: updated columns: s
+006: done
+]])
+
+dnl This test checks that deleting both the destination and source of the
+dnl reference doesn't remove the reference the source tracked record.
+OVSDB_CHECK_IDL_TRACK([track, simple idl, initially populated, strong references, multiple deletes],
+  [['["idltest",
+      {"op": "insert",
+       "table": "simple4",
+       "row": {"name": "row0_s4"},
+       "uuid-name": "uuid_row0_s4"},
+      {"op": "insert",
+       "table": "simple3",
+       "row": {"name": "row0_s3",
+               "uref": ["set",
+                         [["named-uuid", "uuid_row0_s4"]]
+                       ]}}]']],
+  [['condition simple [true]; simple3 [true]; simple4 [true]' \
+    '["idltest",
+      {"op": "delete",
+       "table": "simple3",
+       "where": []},
+      {"op": "delete",
+       "table": "simple4",
+       "where": []}]' \
+    '["idltest",
+      {"op": "insert",
+       "table": "simple",
+       "row": {"s": "row0_s"}}]']],
+  [[000: change conditions
+001: table simple3: inserted row: name=row0_s3 uset=[] uref=[[<0>]] uuid=<1>
+001: table simple3: updated columns: name uref
+001: table simple4: inserted row: name=row0_s4 uuid=<0>
+001: table simple4: updated columns: name
+002: {"error":null,"result":[{"count":1},{"count":1}]}
+003: table simple3: deleted row: name=row0_s3 uset=[] uref=[[<0>]] uuid=<1>
+003: table simple4: deleted row: name=row0_s4 uuid=<0>
+004: {"error":null,"result":[{"uuid":["uuid","<2>"]}]}
+005: table simple: inserted row: i=0 r=0 b=false s=row0_s u=<3> ia=[] ra=[] ba=[] sa=[] ua=[] uuid=<2>
+005: table simple: updated columns: s
+006: done
+]])
+
 OVSDB_CHECK_IDL_TRACK([track, simple idl, initially empty, various ops],
   [],
   [['["idltest",
diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
index f50e88d..36fb9cc 100644
--- a/tests/test-ovsdb.c
+++ b/tests/test-ovsdb.c
@@ -1947,6 +1947,23 @@  print_idl_row_updated_simple3(const struct idltest_simple3 *s3, int step)
 }
 
 static void
+print_idl_row_updated_simple4(const struct idltest_simple4 *s4, int step)
+{
+    struct ds updates = DS_EMPTY_INITIALIZER;
+    for (size_t i = 0; i < IDLTEST_SIMPLE4_N_COLUMNS; i++) {
+        if (idltest_simple4_is_updated(s4, i)) {
+            ds_put_format(&updates, " %s", idltest_simple4_columns[i].name);
+        }
+    }
+    if (updates.length) {
+        print_and_log("%03d: table %s: updated columns:%s",
+                      step, s4->header_.table->class_->name,
+                      ds_cstr(&updates));
+        ds_destroy(&updates);
+    }
+}
+
+static void
 print_idl_row_updated_simple6(const struct idltest_simple6 *s6, int step)
 {
     struct ds updates = DS_EMPTY_INITIALIZER;
@@ -2090,6 +2107,20 @@  print_idl_row_simple3(const struct idltest_simple3 *s3, int step)
 }
 
 static void
+print_idl_row_simple4(const struct idltest_simple4 *s4, int step)
+{
+    struct ds msg = DS_EMPTY_INITIALIZER;
+    ds_put_format(&msg, "name=%s", s4->name);
+
+    char *row_msg = format_idl_row(&s4->header_, step, ds_cstr(&msg));
+    print_and_log("%s", row_msg);
+    ds_destroy(&msg);
+    free(row_msg);
+
+    print_idl_row_updated_simple4(s4, step);
+}
+
+static void
 print_idl_row_simple6(const struct idltest_simple6 *s6, int step)
 {
     struct ds msg = DS_EMPTY_INITIALIZER;
@@ -2156,6 +2187,8 @@  print_idl(struct ovsdb_idl *idl, int step)
 static void
 print_idl_track(struct ovsdb_idl *idl, int step)
 {
+    const struct idltest_simple3 *s3;
+    const struct idltest_simple4 *s4;
     const struct idltest_simple6 *s6;
     const struct idltest_simple *s;
     const struct idltest_link1 *l1;
@@ -2174,6 +2207,14 @@  print_idl_track(struct ovsdb_idl *idl, int step)
         print_idl_row_link2(l2, step);
         n++;
     }
+    IDLTEST_SIMPLE3_FOR_EACH_TRACKED (s3, idl) {
+        print_idl_row_simple3(s3, step);
+        n++;
+    }
+    IDLTEST_SIMPLE4_FOR_EACH_TRACKED (s4, idl) {
+        print_idl_row_simple4(s4, step);
+        n++;
+    }
     IDLTEST_SIMPLE6_FOR_EACH_TRACKED (s6, idl) {
         print_idl_row_simple6(s6, step);
         n++;
@@ -2404,6 +2445,10 @@  find_table_class(const char *name)
         return &idltest_table_link1;
     } else if (!strcmp(name, "link2")) {
         return &idltest_table_link2;
+    } else if (!strcmp(name, "simple3")) {
+        return &idltest_table_simple3;
+    } else if (!strcmp(name, "simple4")) {
+        return &idltest_table_simple4;
     } else if (!strcmp(name, "simple6")) {
         return &idltest_table_simple6;
     }