[openstack-dev] [neutron][ml2] Port binding information, transactions, and concurrency

Robert Kukura rkukura at redhat.com
Wed Feb 5 07:16:14 UTC 2014


A couple of interrelated issues with the ML2 plugin's port binding have
been discussed over the past several months in the weekly ML2 meetings.
These effect drivers being implemented for icehouse, and therefore need
to be addressed in icehouse:

* MechanismDrivers need detailed information about all binding changes,
including unbinding on port deletion
(https://bugs.launchpad.net/neutron/+bug/1276395)
* MechanismDrivers' bind_port() methods are currently called inside
transactions, but in some cases need to make remote calls to controllers
or devices (https://bugs.launchpad.net/neutron/+bug/1276391)
* Semantics of concurrent port binding need to be defined if binding is
moved outside the triggering transaction.

I've taken the action of writing up a unified proposal for resolving
these issues, which follows...

1) An original_bound_segment property will be added to PortContext. When
the MechanismDriver update_port_precommit() and update_port_postcommit()
methods are called and a binding previously existed (whether its being
torn down or not), this property will provide access to the network
segment used by the old binding. In these same cases, the portbinding
extension attributes (such as binding:vif_type) for the old binding will
be available via the PortContext.original property. It may be helpful to
also add bound_driver and original_bound_driver properties to
PortContext that behave similarly to bound_segment and
original_bound_segment.

2) The MechanismDriver.bind_port() method will no longer be called from
within a transaction. This will allow drivers to make remote calls on
controllers or devices from within this method without holding a DB
transaction open during those calls. Drivers can manage their own
transactions within bind_port() if needed, but need to be aware that
these are independent from the transaction that triggered binding, and
concurrent changes to the port could be occurring.

3) Binding will only occur after the transaction that triggers it has
been completely processed and committed. That initial transaction will
unbind the port if necessary. Four cases for the initial transaction are
possible:

3a) In a port create operation, whether the binding:host_id is supplied
or not, all drivers' port_create_precommit() methods will be called, the
initial transaction will be committed, and all drivers'
port_create_postcommit() methods will be called. The drivers will see
this as creation of a new unbound port, with PortContext properties as
shown. If a value for binding:host_id was supplied, binding will occur
afterwards as described in 4 below.

PortContext.original: None
PortContext.original_bound_segment: None
PortContext.original_bound_driver: None
PortContext.current['binding:host_id']: supplied value or None
PortContext.current['binding:vif_type']: 'unbound'
PortContext.bound_segment: None
PortContext.bound_driver: None

3b) Similarly, in a port update operation on a previously unbound port,
all drivers' port_update_precommit() and port_update_postcommit()
methods will be called, with PortContext properies as shown. If a value
for binding:host_id was supplied, binding will occur afterwards as
described in 4 below.

PortContext.original['binding:host_id']: previous value or None
PortContext.original['binding:vif_type']: 'unbound' or 'binding_failed'
PortContext.original_bound_segment: None
PortContext.original_bound_driver: None
PortContext.current['binding:host_id']: current value or None
PortContext.current['binding:vif_type']: 'unbound'
PortContext.bound_segment: None
PortContext.bound_driver: None

3c) In a port update operation on a previously bound port that does not
trigger unbinding or rebinding, all drivers' update_port_precommit() and
update_port_postcommit() methods will be called with PortContext
properties reflecting unchanged binding states as shown.

PortContext.original['binding:host_id']: previous value
PortContext.original['binding:vif_type']: previous value
PortContext.original_bound_segment: previous value
PortContext.original_bound_driver: previous value
PortContext.current['binding:host_id']: previous value
PortContext.current['binding:vif_type']: previous value
PortContext.bound_segment: previous value
PortContext.bound_driver: previous value

3d) In a the port update operation on a previously bound port that does
trigger unbinding or rebinding, all drivers' update_port_precommit() and
update_port_postcommit() methods will be called with PortContext
properties reflecting the previously bound and currently unbound binding
states as shown. If a value for binding:host_id was supplied, binding
will occur afterwards as described in 4 below.

PortContext.original['binding:host_id']: previous value
PortContext.original['binding:vif_type']: previous value
PortContext.original_bound_segment: previous value
PortContext.original_bound_driver: previous value
PortContext.current['binding:host_id']: new or current value
PortContext.current['binding:vif_type']: 'unbound'
PortContext.bound_segment: None
PortContext.bound_driver: None

4) If a port create or update operation triggers binding or rebinding,
it is attempted after the initial transaction is processed and committed
as described in 3 above. The binding process itself is just as before,
except it happens after and outside the transaction. Since binding now
occurs outside the transaction, its possible that multiple threads or
processes could concurrently attempt to bind the same port, although
this is should be a rare occurrence. Rather than trying to prevent this
with some sort of distributed lock or complicated state machine,
concurrent attempts to bind are allowed to proceed in parallel. When a
thread completes its attempt to bind (either successfully or
unsuccessfully) it then performs a second transaction to update the DB
with the result of its binding attempt. When doing so, it checks to see
if some other thread has already committed relevant changes to the port
between the two transactions. There are three possible cases:

4a) If the thread's binding attempt succeeded, and no other thread has
committed either a new binding or changes that invalidate this thread's
new binding between the two transactions, the thread commits its own
binding results, calling all drivers' update_port_precommit() and
update_port_postcommit() methods with PortContext properties reflecting
the new binding as shown. It then returns the updated port dictionary to
the caller.

PortContext.original['binding:host_id']: previous value
PortContext.original['binding:vif_type']: 'unbound'
PortContext.original_bound_segment: None
PortContext.original_bound_driver: None
PortContext.current['binding:host_id']: previous value
PortContext.current['binding:vif_type']: new value
PortContext.bound_segment: new value
PortContext.bound_driver: new value

4b) If the thread's binding attempt either succeeded or failed, but some
other thread has committed a new successful binding between the two
transactions, the thread returns a port dictionary with attributes based
on the DB state from the new transaction, including the other thread's
binding and any other port state changes. No further calls to mechanism
drivers are needed here since they are the responsibility of the other
thread that bound the port.

4c) If some other thread committed changes to the port's
binding-relevant state but has not committed a successful binding, then
this thread attempts to bind again using that updated state, repeating 4.

5) Port deletion no longer does anything special to unbind the port. All
drivers' delete_port_precommit() and delete_port_postcommit() methods
are called with PortContext properties reflecting the binding state
before deletion as shown.

PortContext.original: None
PortContext.original_bound_segment: None
PortContext.original_bound_driver: None
PortContext.current['binding:host_id']: previous value or None
PortContext.current['binding:vif_type']: previous value
PortContext.bound_segment: previous value
PortContext.bound_driver: previous value

6) In order to ensure successful bindings are created and returned
whenever possible, the get port and get ports operations also attempt to
bind the port as in 4 above when binding:host_id is available but there
is no existing successful binding in the DB.

7) We can either eliminate MechanismDriver.unbind_port(), or call it on
the previously bound driver within the transaction in 3d and 5 above. If
we do keep it, the old binding state must be consistently reflected in
the PortContext as either current or original state, TBD. Since all
drivers see unbinding as a port update where current_bound_segment is
None and original_bound_segment is not None, calling unbind_port() seems
redundant.

8) If bindings shouldn't spontaneously become invalid, maybe we can
eliminate MechanismDriver.validate_bound_port().


I've provided a lot of details, and the above may seem complicated. But
I think its actually much more consistent and predictable than the
current port binding code, and implementation should be straightforward.

-Bob



More information about the OpenStack-dev mailing list