Skip to content

Commit ddbb29a

Browse files
committed
[OVN] Retry retrieving LSP hosting information
There's a sync issue while trying to fetch the hosting information for the LSP before we write it to the OVN database, sometimes the information is not yet present and we end up with an empty string ("") for the host attribute of portbindings. This patch adds a retry mechanism to solve this sync issue. Change-Id: I52ec4b346271889ebaa7b7f84981eae5503d02d3 Related-Bug: #2020058 Signed-off-by: Lucas Alvares Gomes <[email protected]> (cherry picked from commit 3044b93)
1 parent aa6674b commit ddbb29a

File tree

2 files changed

+123
-1
lines changed

2 files changed

+123
-1
lines changed

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@
4242
from ovsdbapp.backend.ovs_idl import idlutils
4343
import tenacity
4444

45+
from neutron._i18n import _
4546
from neutron.common.ovn import acl as ovn_acl
4647
from neutron.common.ovn import constants as ovn_const
4748
from neutron.common.ovn import utils
4849
from neutron.common import utils as common_utils
4950
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
5051
from neutron.db import ovn_revision_numbers_db as db_rev
5152
from neutron.db import segments_db
53+
from neutron.plugins.ml2 import db as ml2_db
5254
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
5355
import placement as placement_extension
5456
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
@@ -232,6 +234,29 @@ def _get_port_dhcp_options(self, port, ip_version):
232234
external_ids=subnet_dhcp_options['external_ids'])
233235
return {'cmd': add_dhcp_opts_cmd}
234236

237+
@tenacity.retry(retry=tenacity.retry_if_exception_type(RuntimeError),
238+
wait=tenacity.wait_random(min=2, max=3),
239+
stop=tenacity.stop_after_attempt(3),
240+
reraise=True)
241+
def _wait_for_port_bindings_host(self, context, port_id):
242+
db_port = ml2_db.get_port(context, port_id)
243+
# This is already checked previously but, just to stay on
244+
# the safe side in case the port is deleted mid-operation
245+
if not db_port:
246+
raise RuntimeError(
247+
_('No port found with ID %s') % port_id)
248+
249+
if not db_port.port_bindings:
250+
raise RuntimeError(
251+
_('No port bindings information found for '
252+
'port %s') % port_id)
253+
254+
if not db_port.port_bindings[0].host:
255+
raise RuntimeError(
256+
_('No hosting information found for port %s') % port_id)
257+
258+
return db_port
259+
235260
def update_lsp_host_info(self, context, db_port, up=True):
236261
"""Update the binding hosting information for the LSP.
237262
@@ -247,8 +272,19 @@ def update_lsp_host_info(self, context, db_port, up=True):
247272
if up:
248273
if not db_port.port_bindings:
249274
return
250-
host = db_port.port_bindings[0].host
251275

276+
if not db_port.port_bindings[0].host:
277+
# NOTE(lucasgomes): There might be a sync issue between
278+
# the moment that this port was fetched from the database
279+
# and the hosting information being set, retry a few times
280+
try:
281+
db_port = self._wait_for_port_bindings_host(
282+
context, db_port.id)
283+
except RuntimeError as e:
284+
LOG.warning(e)
285+
return
286+
287+
host = db_port.port_bindings[0].host
252288
ext_ids = ('external_ids',
253289
{ovn_const.OVN_HOST_ID_EXT_ID_KEY: host})
254290
cmd.append(

neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_client.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from neutron.common.ovn import constants
1919
from neutron.conf.plugins.ml2 import config as ml2_conf
2020
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
21+
from neutron.plugins.ml2 import db as ml2_db
2122
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
2223
from neutron.tests import base
2324
from neutron.tests.unit.services.logapi.drivers.ovn \
@@ -26,6 +27,8 @@
2627
from neutron_lib import constants as const
2728
from neutron_lib.services.logapi import constants as log_const
2829

30+
from tenacity import wait_none
31+
2932

3033
class TestOVNClientBase(base.BaseTestCase):
3134

@@ -45,6 +48,9 @@ def setUp(self):
4548
self.get_plugin = mock.patch(
4649
'neutron_lib.plugins.directory.get_plugin').start()
4750

51+
# Disable tenacity wait for UT
52+
self.ovn_client._wait_for_port_bindings_host.retry.wait = wait_none()
53+
4854
def test__add_router_ext_gw_default_route(self):
4955
plugin = mock.MagicMock()
5056
self.get_plugin.return_value = plugin
@@ -114,6 +120,45 @@ def test_update_lsp_host_info_up(self):
114120
'Logical_Switch_Port', port_id,
115121
('external_ids', {constants.OVN_HOST_ID_EXT_ID_KEY: host_id}))
116122

123+
def test_update_lsp_host_info_up_retry(self):
124+
context = mock.MagicMock()
125+
host_id = 'fake-binding-host-id'
126+
port_id = 'fake-port-id'
127+
db_port_no_host = mock.Mock(
128+
id=port_id, port_bindings=[mock.Mock(host="")])
129+
db_port = mock.Mock(
130+
id=port_id, port_bindings=[mock.Mock(host=host_id)])
131+
132+
with mock.patch.object(
133+
self.ovn_client, '_wait_for_port_bindings_host') as mock_wait:
134+
mock_wait.return_value = db_port
135+
self.ovn_client.update_lsp_host_info(context, db_port_no_host)
136+
137+
# Assert _wait_for_port_bindings_host was called
138+
mock_wait.assert_called_once_with(context, port_id)
139+
140+
# Assert host_id was set
141+
self.nb_idl.db_set.assert_called_once_with(
142+
'Logical_Switch_Port', port_id,
143+
('external_ids', {constants.OVN_HOST_ID_EXT_ID_KEY: host_id}))
144+
145+
def test_update_lsp_host_info_up_retry_fail(self):
146+
context = mock.MagicMock()
147+
port_id = 'fake-port-id'
148+
db_port_no_host = mock.Mock(
149+
id=port_id, port_bindings=[mock.Mock(host="")])
150+
151+
with mock.patch.object(
152+
self.ovn_client, '_wait_for_port_bindings_host') as mock_wait:
153+
mock_wait.side_effect = RuntimeError("boom")
154+
self.ovn_client.update_lsp_host_info(context, db_port_no_host)
155+
156+
# Assert _wait_for_port_bindings_host was called
157+
mock_wait.assert_called_once_with(context, port_id)
158+
159+
# Assert host_id was NOT set
160+
self.assertFalse(self.nb_idl.db_set.called)
161+
117162
def test_update_lsp_host_info_down(self):
118163
context = mock.MagicMock()
119164
port_id = 'fake-port-id'
@@ -125,6 +170,47 @@ def test_update_lsp_host_info_down(self):
125170
'Logical_Switch_Port', port_id, 'external_ids',
126171
constants.OVN_HOST_ID_EXT_ID_KEY, if_exists=True)
127172

173+
@mock.patch.object(ml2_db, 'get_port')
174+
def test__wait_for_port_bindings_host(self, mock_get_port):
175+
context = mock.MagicMock()
176+
host_id = 'fake-binding-host-id'
177+
port_id = 'fake-port-id'
178+
db_port_no_host = mock.Mock(
179+
id=port_id, port_bindings=[mock.Mock(host="")])
180+
db_port = mock.Mock(
181+
id=port_id, port_bindings=[mock.Mock(host=host_id)])
182+
183+
mock_get_port.side_effect = (db_port_no_host, db_port)
184+
185+
ret = self.ovn_client._wait_for_port_bindings_host(
186+
context, port_id)
187+
188+
self.assertEqual(ret, db_port)
189+
190+
expected_calls = [mock.call(context, port_id),
191+
mock.call(context, port_id)]
192+
mock_get_port.assert_has_calls(expected_calls)
193+
194+
@mock.patch.object(ml2_db, 'get_port')
195+
def test__wait_for_port_bindings_host_fail(self, mock_get_port):
196+
context = mock.MagicMock()
197+
port_id = 'fake-port-id'
198+
db_port_no_pb = mock.Mock(id=port_id, port_bindings=[])
199+
db_port_no_host = mock.Mock(
200+
id=port_id, port_bindings=[mock.Mock(host="")])
201+
202+
mock_get_port.side_effect = (
203+
db_port_no_pb, db_port_no_host, db_port_no_host)
204+
205+
self.assertRaises(
206+
RuntimeError, self.ovn_client._wait_for_port_bindings_host,
207+
context, port_id)
208+
209+
expected_calls = [mock.call(context, port_id),
210+
mock.call(context, port_id),
211+
mock.call(context, port_id)]
212+
mock_get_port.assert_has_calls(expected_calls)
213+
128214

129215
class TestOVNClientFairMeter(TestOVNClientBase,
130216
test_log_driver.TestOVNDriverBase):

0 commit comments

Comments
 (0)