diff mbox series

[ovs-dev,v6] python: Send notifications after the transaction ends

Message ID 20210309143416.14334-1-twilson@redhat.com
State Accepted
Headers show
Series [ovs-dev,v6] python: Send notifications after the transaction ends | expand

Commit Message

Terry Wilson March 9, 2021, 2:34 p.m. UTC
The Python IDL notification mechanism was sending a notification
for each processed update in a transaction as it was processed.
This causes issues with multi-row changes that contain references
to each other.

For example, if a Logical_Router_Port is created along with a
Gateway_Chassis, and the LRP.gateway_chassis set to that GC, then
when the notify() passes the CREATE event for the LRP, the GC will
not yet have been processed, so __getattr__ when _uuid_to_row fails
to find the GC, will return the default value for LRP.gateway_chassis
which is [].

This patch has the process_update methods return the notifications
that would be produced when a row changes, so they can be queued
and sent after all rows have been processed.

Fixes: d7d417fcddf9 ("Allow subclasses of Idl to define a notification hook")
Signed-off-by: Terry Wilson <twilson@redhat.com>
---
 python/ovs/db/idl.py | 39 ++++++++++++++++++++++++---------------
 tests/ovsdb-idl.at   | 22 ++++++++++++++++++++++
 tests/test-ovsdb.py  |  8 ++++++--
 3 files changed, 52 insertions(+), 17 deletions(-)

Comments

Brian Haley March 9, 2021, 4:40 p.m. UTC | #1
Acked-by: Brian Haley <haleyb.dev@gmail.com>

-Brian

On 3/9/21 9:34 AM, Terry Wilson wrote:
> The Python IDL notification mechanism was sending a notification
> for each processed update in a transaction as it was processed.
> This causes issues with multi-row changes that contain references
> to each other.
> 
> For example, if a Logical_Router_Port is created along with a
> Gateway_Chassis, and the LRP.gateway_chassis set to that GC, then
> when the notify() passes the CREATE event for the LRP, the GC will
> not yet have been processed, so __getattr__ when _uuid_to_row fails
> to find the GC, will return the default value for LRP.gateway_chassis
> which is [].
> 
> This patch has the process_update methods return the notifications
> that would be produced when a row changes, so they can be queued
> and sent after all rows have been processed.
> 
> Fixes: d7d417fcddf9 ("Allow subclasses of Idl to define a notification hook")
> Signed-off-by: Terry Wilson <twilson@redhat.com>
> ---
>   python/ovs/db/idl.py | 39 ++++++++++++++++++++++++---------------
>   tests/ovsdb-idl.at   | 22 ++++++++++++++++++++++
>   tests/test-ovsdb.py  |  8 ++++++--
>   3 files changed, 52 insertions(+), 17 deletions(-)
> 
> diff --git a/python/ovs/db/idl.py b/python/ovs/db/idl.py
> index 5850ac7ab..4226d1cb2 100644
> --- a/python/ovs/db/idl.py
> +++ b/python/ovs/db/idl.py
> @@ -12,6 +12,7 @@
>   # See the License for the specific language governing permissions and
>   # limitations under the License.
>   
> +import collections
>   import functools
>   import uuid
>   
> @@ -39,6 +40,10 @@ OVSDB_UPDATE2 = 1
>   CLUSTERED = "clustered"
>   
>   
> +Notice = collections.namedtuple('Notice', ('event', 'row', 'updates'))
> +Notice.__new__.__defaults__ = (None,)  # default updates=None
> +
> +
>   class Idl(object):
>       """Open vSwitch Database Interface Definition Language (OVSDB IDL).
>   
> @@ -614,6 +619,7 @@ class Idl(object):
>               raise error.Error("<table-updates> is not an object",
>                                 table_updates)
>   
> +        notices = []
>           for table_name, table_update in table_updates.items():
>               table = tables.get(table_name)
>               if not table:
> @@ -639,7 +645,9 @@ class Idl(object):
>                                         % (table_name, uuid_string))
>   
>                   if version == OVSDB_UPDATE2:
> -                    if self.__process_update2(table, uuid, row_update):
> +                    changes = self.__process_update2(table, uuid, row_update)
> +                    if changes:
> +                        notices.append(changes)
>                           self.change_seqno += 1
>                       continue
>   
> @@ -652,17 +660,20 @@ class Idl(object):
>                       raise error.Error('<row-update> missing "old" and '
>                                         '"new" members', row_update)
>   
> -                if self.__process_update(table, uuid, old, new):
> +                changes = self.__process_update(table, uuid, old, new)
> +                if changes:
> +                    notices.append(changes)
>                       self.change_seqno += 1
> +        for notice in notices:
> +            self.notify(*notice)
>   
>       def __process_update2(self, table, uuid, row_update):
> +        """Returns Notice if a column changed, False otherwise."""
>           row = table.rows.get(uuid)
> -        changed = False
>           if "delete" in row_update:
>               if row:
>                   del table.rows[uuid]
> -                self.notify(ROW_DELETE, row)
> -                changed = True
> +                return Notice(ROW_DELETE, row)
>               else:
>                   # XXX rate-limit
>                   vlog.warn("cannot delete missing row %s from table"
> @@ -681,29 +692,27 @@ class Idl(object):
>               changed = self.__row_update(table, row, row_update)
>               table.rows[uuid] = row
>               if changed:
> -                self.notify(ROW_CREATE, row)
> +                return Notice(ROW_CREATE, row)
>           elif "modify" in row_update:
>               if not row:
>                   raise error.Error('Modify non-existing row')
>   
>               old_row = self.__apply_diff(table, row, row_update['modify'])
> -            self.notify(ROW_UPDATE, row, Row(self, table, uuid, old_row))
> -            changed = True
> +            return Notice(ROW_UPDATE, row, Row(self, table, uuid, old_row))
>           else:
>               raise error.Error('<row-update> unknown operation',
>                                 row_update)
> -        return changed
> +        return False
>   
>       def __process_update(self, table, uuid, old, new):
> -        """Returns True if a column changed, False otherwise."""
> +        """Returns Notice if a column changed, False otherwise."""
>           row = table.rows.get(uuid)
>           changed = False
>           if not new:
>               # Delete row.
>               if row:
>                   del table.rows[uuid]
> -                changed = True
> -                self.notify(ROW_DELETE, row)
> +                return Notice(ROW_DELETE, row)
>               else:
>                   # XXX rate-limit
>                   vlog.warn("cannot delete missing row %s from table %s"
> @@ -723,7 +732,7 @@ class Idl(object):
>               if op == ROW_CREATE:
>                   table.rows[uuid] = row
>               if changed:
> -                self.notify(ROW_CREATE, row)
> +                return Notice(ROW_CREATE, row)
>           else:
>               op = ROW_UPDATE
>               if not row:
> @@ -737,8 +746,8 @@ class Idl(object):
>               if op == ROW_CREATE:
>                   table.rows[uuid] = row
>               if changed:
> -                self.notify(op, row, Row.from_json(self, table, uuid, old))
> -        return changed
> +                return Notice(op, row, Row.from_json(self, table, uuid, old))
> +        return False
>   
>       def __check_server_db(self):
>           """Returns True if this is a valid server database, False otherwise."""
> diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
> index 4b4791a7d..e00e67e94 100644
> --- a/tests/ovsdb-idl.at
> +++ b/tests/ovsdb-idl.at
> @@ -1486,6 +1486,28 @@ m4_define([OVSDB_CHECK_IDL_NOTIFY],
>      [OVSDB_CHECK_IDL_PY([$1], [], [$2], [$3], [notify $4], [$5])
>       OVSDB_CHECK_IDL_SSL_PY([$1], [], [$2], [$3], [notify $4], [$5])])
>   
> +OVSDB_CHECK_IDL_NOTIFY([simple link idl verify notify],
> +  [['track-notify' \
> +    '["idltest",
> +       {"op": "insert",
> +       "table": "link1",
> +       "row": {"i": 1, "k": ["named-uuid", "l1row"], "l2": ["set", [["named-uuid", "l2row"]]]},
> +       "uuid-name": "l1row"},
> +      {"op": "insert",
> +       "table": "link2",
> +       "uuid-name": "l2row",
> +       "row": {"i": 2, "l1": ["set", [["named-uuid", "l1row"]]]}}]']],
> +[[000: empty
> +000: event:create, row={uuid=<0>}, updates=None
> +000: event:create, row={uuid=<1>}, updates=None
> +001: {"error":null,"result":[{"uuid":["uuid","<2>"]},{"uuid":["uuid","<3>"]}]}
> +002: event:create, row={i=1 uuid=<2> l2=[<3>]}, updates=None
> +002: event:create, row={i=2 uuid=<3> l1=[<2>]}, updates=None
> +002: i=1 k=1 ka=[] l2=2 uuid=<2>
> +002: i=2 l1=1 uuid=<3>
> +003: done
> +]])
> +
>   OVSDB_CHECK_IDL_NOTIFY([simple idl verify notify],
>     [['track-notify' \
>       '["idltest",
> diff --git a/tests/test-ovsdb.py b/tests/test-ovsdb.py
> index a19680274..9d3228f23 100644
> --- a/tests/test-ovsdb.py
> +++ b/tests/test-ovsdb.py
> @@ -162,6 +162,8 @@ def get_simple_printable_row_string(row, columns):
>               if isinstance(value, dict):
>                   value = sorted((row_to_uuid(k), row_to_uuid(v))
>                                  for k, v in value.items())
> +            if isinstance(value, (list, tuple)):
> +                value = sorted((row_to_uuid(v) for v in value))
>               s += "%s=%s " % (column, value)
>       s = s.strip()
>       s = re.sub('""|,|u?\'', "", s)
> @@ -172,9 +174,10 @@ def get_simple_printable_row_string(row, columns):
>       return s
>   
>   
> -def get_simple_table_printable_row(row):
> +def get_simple_table_printable_row(row, *additional_columns):
>       simple_columns = ["i", "r", "b", "s", "u", "ia",
>                         "ra", "ba", "sa", "ua", "uuid"]
> +    simple_columns.extend(additional_columns)
>       return get_simple_printable_row_string(row, simple_columns)
>   
>   
> @@ -637,7 +640,8 @@ def do_idl(schema_file, remote, *commands):
>       def mock_notify(event, row, updates=None):
>           output = "%03d: " % step
>           output += "event:" + str(event) + ", row={"
> -        output += get_simple_table_printable_row(row) + "}, updates="
> +        output += get_simple_table_printable_row(row,
> +                                                 'l2', 'l1') + "}, updates="
>           if updates is None:
>               output += "None"
>           else:
>
Dumitru Ceara March 12, 2021, 7:37 p.m. UTC | #2
On 3/9/21 3:34 PM, Terry Wilson wrote:
> The Python IDL notification mechanism was sending a notification
> for each processed update in a transaction as it was processed.
> This causes issues with multi-row changes that contain references
> to each other.
> 
> For example, if a Logical_Router_Port is created along with a
> Gateway_Chassis, and the LRP.gateway_chassis set to that GC, then
> when the notify() passes the CREATE event for the LRP, the GC will
> not yet have been processed, so __getattr__ when _uuid_to_row fails
> to find the GC, will return the default value for LRP.gateway_chassis
> which is [].
> 
> This patch has the process_update methods return the notifications
> that would be produced when a row changes, so they can be queued
> and sent after all rows have been processed.
> 
> Fixes: d7d417fcddf9 ("Allow subclasses of Idl to define a notification hook")
> Signed-off-by: Terry Wilson <twilson@redhat.com>
> ---

I'm not too familiar with the python IDL code but the change looks good 
to me.

I also verified and the new tests exercises the scenario described 
above.  All other IDL tests also passed.

Acked-by: Dumitru Ceara <dceara@redhat.com>

Thanks,
Dumitru
Ilya Maximets March 15, 2021, 4:33 p.m. UTC | #3
On 3/9/21 3:34 PM, Terry Wilson wrote:
> The Python IDL notification mechanism was sending a notification
> for each processed update in a transaction as it was processed.
> This causes issues with multi-row changes that contain references
> to each other.
> 
> For example, if a Logical_Router_Port is created along with a
> Gateway_Chassis, and the LRP.gateway_chassis set to that GC, then
> when the notify() passes the CREATE event for the LRP, the GC will
> not yet have been processed, so __getattr__ when _uuid_to_row fails
> to find the GC, will return the default value for LRP.gateway_chassis
> which is [].
> 
> This patch has the process_update methods return the notifications
> that would be produced when a row changes, so they can be queued
> and sent after all rows have been processed.
> 
> Fixes: d7d417fcddf9 ("Allow subclasses of Idl to define a notification hook")
> Signed-off-by: Terry Wilson <twilson@redhat.com>
> ---
>  python/ovs/db/idl.py | 39 ++++++++++++++++++++++++---------------
>  tests/ovsdb-idl.at   | 22 ++++++++++++++++++++++
>  tests/test-ovsdb.py  |  8 ++++++--
>  3 files changed, 52 insertions(+), 17 deletions(-)

Thanks, Terry, Brian, Dumitru and Flavio!
Applied to master and backported down to 2.13.

Best regards, Ilya Maximets.
Terry Wilson March 15, 2021, 8:39 p.m. UTC | #4
On Mon, Mar 15, 2021 at 11:50 AM Ilya Maximets <i.maximets@ovn.org> wrote:

> Thanks, Terry, Brian, Dumitru and Flavio!
> Applied to master and backported down to 2.13.
>
> Best regards, Ilya Maximets.

Thanks! Can I get it backported down to 2.11 at least? I think the
issue goes back to at least 2.10. There's probably very minor
conflicts--I'm more than happy to do the backports myself if they'll
be accepted. 2.11 is what the upstream OpenStack neutron CI uses for
the stable/train branch, which Red Hat OSP 16.1/16.2 use.

Terry
Ilya Maximets March 15, 2021, 9:30 p.m. UTC | #5
On 3/15/21 9:39 PM, Terry Wilson wrote:
> On Mon, Mar 15, 2021 at 11:50 AM Ilya Maximets <i.maximets@ovn.org> wrote:
> 
>> Thanks, Terry, Brian, Dumitru and Flavio!
>> Applied to master and backported down to 2.13.
>>
>> Best regards, Ilya Maximets.
> 
> Thanks! Can I get it backported down to 2.11 at least? I think the
> issue goes back to at least 2.10. There's probably very minor
> conflicts--I'm more than happy to do the backports myself if they'll
> be accepted. 2.11 is what the upstream OpenStack neutron CI uses for
> the stable/train branch, which Red Hat OSP 16.1/16.2 use.
> 
> Terry
> 

Conflict on 2.12 is very simple, so I can fix it and apply.
No problem.

For 2.11 is a little bit trickier, so if you will provide the backport,
I can apply it.  However, I should mention that branch 2.11 officially
reached its EOL with the release of OVS 2.15.  And while I can apply
the patch to branch-2.11, Open vSwitch team will not provide any new
official upstream releases from this branch.  v2.11.7 was the last one.

Best regards, Ilya Maximets.
diff mbox series

Patch

diff --git a/python/ovs/db/idl.py b/python/ovs/db/idl.py
index 5850ac7ab..4226d1cb2 100644
--- a/python/ovs/db/idl.py
+++ b/python/ovs/db/idl.py
@@ -12,6 +12,7 @@ 
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import collections
 import functools
 import uuid
 
@@ -39,6 +40,10 @@  OVSDB_UPDATE2 = 1
 CLUSTERED = "clustered"
 
 
+Notice = collections.namedtuple('Notice', ('event', 'row', 'updates'))
+Notice.__new__.__defaults__ = (None,)  # default updates=None
+
+
 class Idl(object):
     """Open vSwitch Database Interface Definition Language (OVSDB IDL).
 
@@ -614,6 +619,7 @@  class Idl(object):
             raise error.Error("<table-updates> is not an object",
                               table_updates)
 
+        notices = []
         for table_name, table_update in table_updates.items():
             table = tables.get(table_name)
             if not table:
@@ -639,7 +645,9 @@  class Idl(object):
                                       % (table_name, uuid_string))
 
                 if version == OVSDB_UPDATE2:
-                    if self.__process_update2(table, uuid, row_update):
+                    changes = self.__process_update2(table, uuid, row_update)
+                    if changes:
+                        notices.append(changes)
                         self.change_seqno += 1
                     continue
 
@@ -652,17 +660,20 @@  class Idl(object):
                     raise error.Error('<row-update> missing "old" and '
                                       '"new" members', row_update)
 
-                if self.__process_update(table, uuid, old, new):
+                changes = self.__process_update(table, uuid, old, new)
+                if changes:
+                    notices.append(changes)
                     self.change_seqno += 1
+        for notice in notices:
+            self.notify(*notice)
 
     def __process_update2(self, table, uuid, row_update):
+        """Returns Notice if a column changed, False otherwise."""
         row = table.rows.get(uuid)
-        changed = False
         if "delete" in row_update:
             if row:
                 del table.rows[uuid]
-                self.notify(ROW_DELETE, row)
-                changed = True
+                return Notice(ROW_DELETE, row)
             else:
                 # XXX rate-limit
                 vlog.warn("cannot delete missing row %s from table"
@@ -681,29 +692,27 @@  class Idl(object):
             changed = self.__row_update(table, row, row_update)
             table.rows[uuid] = row
             if changed:
-                self.notify(ROW_CREATE, row)
+                return Notice(ROW_CREATE, row)
         elif "modify" in row_update:
             if not row:
                 raise error.Error('Modify non-existing row')
 
             old_row = self.__apply_diff(table, row, row_update['modify'])
-            self.notify(ROW_UPDATE, row, Row(self, table, uuid, old_row))
-            changed = True
+            return Notice(ROW_UPDATE, row, Row(self, table, uuid, old_row))
         else:
             raise error.Error('<row-update> unknown operation',
                               row_update)
-        return changed
+        return False
 
     def __process_update(self, table, uuid, old, new):
-        """Returns True if a column changed, False otherwise."""
+        """Returns Notice if a column changed, False otherwise."""
         row = table.rows.get(uuid)
         changed = False
         if not new:
             # Delete row.
             if row:
                 del table.rows[uuid]
-                changed = True
-                self.notify(ROW_DELETE, row)
+                return Notice(ROW_DELETE, row)
             else:
                 # XXX rate-limit
                 vlog.warn("cannot delete missing row %s from table %s"
@@ -723,7 +732,7 @@  class Idl(object):
             if op == ROW_CREATE:
                 table.rows[uuid] = row
             if changed:
-                self.notify(ROW_CREATE, row)
+                return Notice(ROW_CREATE, row)
         else:
             op = ROW_UPDATE
             if not row:
@@ -737,8 +746,8 @@  class Idl(object):
             if op == ROW_CREATE:
                 table.rows[uuid] = row
             if changed:
-                self.notify(op, row, Row.from_json(self, table, uuid, old))
-        return changed
+                return Notice(op, row, Row.from_json(self, table, uuid, old))
+        return False
 
     def __check_server_db(self):
         """Returns True if this is a valid server database, False otherwise."""
diff --git a/tests/ovsdb-idl.at b/tests/ovsdb-idl.at
index 4b4791a7d..e00e67e94 100644
--- a/tests/ovsdb-idl.at
+++ b/tests/ovsdb-idl.at
@@ -1486,6 +1486,28 @@  m4_define([OVSDB_CHECK_IDL_NOTIFY],
    [OVSDB_CHECK_IDL_PY([$1], [], [$2], [$3], [notify $4], [$5])
     OVSDB_CHECK_IDL_SSL_PY([$1], [], [$2], [$3], [notify $4], [$5])])
 
+OVSDB_CHECK_IDL_NOTIFY([simple link idl verify notify],
+  [['track-notify' \
+    '["idltest",
+       {"op": "insert",
+       "table": "link1",
+       "row": {"i": 1, "k": ["named-uuid", "l1row"], "l2": ["set", [["named-uuid", "l2row"]]]},
+       "uuid-name": "l1row"},
+      {"op": "insert",
+       "table": "link2",
+       "uuid-name": "l2row",
+       "row": {"i": 2, "l1": ["set", [["named-uuid", "l1row"]]]}}]']],
+[[000: empty
+000: event:create, row={uuid=<0>}, updates=None
+000: event:create, row={uuid=<1>}, updates=None
+001: {"error":null,"result":[{"uuid":["uuid","<2>"]},{"uuid":["uuid","<3>"]}]}
+002: event:create, row={i=1 uuid=<2> l2=[<3>]}, updates=None
+002: event:create, row={i=2 uuid=<3> l1=[<2>]}, updates=None
+002: i=1 k=1 ka=[] l2=2 uuid=<2>
+002: i=2 l1=1 uuid=<3>
+003: done
+]])
+
 OVSDB_CHECK_IDL_NOTIFY([simple idl verify notify],
   [['track-notify' \
     '["idltest",
diff --git a/tests/test-ovsdb.py b/tests/test-ovsdb.py
index a19680274..9d3228f23 100644
--- a/tests/test-ovsdb.py
+++ b/tests/test-ovsdb.py
@@ -162,6 +162,8 @@  def get_simple_printable_row_string(row, columns):
             if isinstance(value, dict):
                 value = sorted((row_to_uuid(k), row_to_uuid(v))
                                for k, v in value.items())
+            if isinstance(value, (list, tuple)):
+                value = sorted((row_to_uuid(v) for v in value))
             s += "%s=%s " % (column, value)
     s = s.strip()
     s = re.sub('""|,|u?\'', "", s)
@@ -172,9 +174,10 @@  def get_simple_printable_row_string(row, columns):
     return s
 
 
-def get_simple_table_printable_row(row):
+def get_simple_table_printable_row(row, *additional_columns):
     simple_columns = ["i", "r", "b", "s", "u", "ia",
                       "ra", "ba", "sa", "ua", "uuid"]
+    simple_columns.extend(additional_columns)
     return get_simple_printable_row_string(row, simple_columns)
 
 
@@ -637,7 +640,8 @@  def do_idl(schema_file, remote, *commands):
     def mock_notify(event, row, updates=None):
         output = "%03d: " % step
         output += "event:" + str(event) + ", row={"
-        output += get_simple_table_printable_row(row) + "}, updates="
+        output += get_simple_table_printable_row(row,
+                                                 'l2', 'l1') + "}, updates="
         if updates is None:
             output += "None"
         else: