OpenStack Identity (Keystone)
Go to file
Lance Bragstad 105f95795f Implement more robust connection handling for asynchronous LDAP calls
Keystone's paging implementation contains a memory leak. The issue is
noticeable if you integrate keystone with an LDAP server that supports
paging and set `keystone.conf [ldap] page_size` to a low integer
(e.g., 5).

Keystone's LDAP backend uses `python-ldap` to interact with LDAP
servers. For paged requests, it uses `search_ext()`, which is an
asynchronous API [0]. The server responds with a message ID, which the
client uses to retrieve all data for the request. In keystone's case,
the `search_ext()` method is invoked with a page control that tells
the server to deliver responses in increments according to the page
size configured with `keystone.conf [ldap] page_size`. So long as the
client has the original connection used to fetch the message ID, it
can request the rest of the information associated to the request.

Keystone's paging implementation loops continuously for paged
requests. It takes the message ID it gets from `search_ext()` and
calls `result3()`, asking the server for the data associated with that
specific message. Keystone continues to do this until the server sends
an indicator that it has no more data relevant to the query (via a
cookie). The `search_ext()` and `result3()` methods must use the same
LDAP connection.

Given the above information, keystone uses context managers to provide
connections. This is relevant when deploying connection pools, where
certain connections are re-used from a pool. Keystone relies on Python
context managers to handle connections, which is pretty typical
use-case for context managers. Connection managers allow us to do the
following (assuming pseudocode):

  with self.get_connection as conn:
      response = conn.search_s()
      return format(response)

The above snippet assumes the `get_connection` method provides a
connection object and a callable that implements `search_s`. Upon
exiting the `with` statement, the connection is disconnected, or put
back into the pool, or whatever the implementation of the context
manager decides to do. Most connections in the LDAP backend are
handled in this fashion.

Unfortunately, the LDAP driver is somewhat oblivious to paging, it's
control implementation, or the fact that it uses an asynchronous API.
Instead, the driver leaves it up to the handler objects it uses for
connections to determine if the request should be controlled via
paging. This is an anti-pattern since the backend establishes the
connection for the request but doesn't ensure that connection is
safely handled for asynchronous APIs.

This forces the `search_ext()` and `result3()` implementations in the
PooledLDAPHandler to know how to handle connections and context
managers, since it needs to ensure the same connection is used for
paged requests. The current code tried to clean up the context
manager responsible for connections after the results are collected
from the server using the message ID. I believe it does this because
it needs to get a new connection for each message in the paged
results, even though it already operates from within a connection
established via a context manager and the PooledLDAPHandler almost
always returns the same connection object from the pool. The code
tries to use a weak reference to create a callback that tears down the
context manager when nothing else references it. At a high-level, the
idea is similar to the following pseudocode:

  with self.get_connection as conn:
      while True:
	ldap_data = []
	context_manager = self.get_connection()
	connection = context_manager.__enter__()
	message_id = connection.search_ext()
	results = connection.result3(message_id)
	ldap_data.append(results)
	context_manager.__exit__()

I wasn't able to see the callback get invoked or work as described in
comments, resulting in memory bloat, especially with low page sizes
which results in more requests. A weak reference invokes the callback
when the weak reference is called, but there are no other references
to the original object [1]. In our case, I don't think we invoke that
path because we don't actually do anything with the weak reference. We
assume it's going to run the callback when the object is garbage
collected.

This commit attempts to address this issue by using the concept of a
finalizer [2], which was designed for similar cases. It also attempts
to hide the cleanup implementation in the AsynchronousMessage object,
so that callers don't have to worry about making sure they invoke the
finalizer.

An alternative approach would be to push more of the paging logic and
implementation up into the LDAP driver. This would make it easier to
put the entire asynchronous API flow for paging into a `with`
statement and relying on the normal behavior of context managers to
clean up accordingly. This approach would remove the manual cleanup
invocation, regardless of using weak references or finalizer objects.
However, this approach would likely require a non-trivial amount of
design work to refactor the entire LDAP backend. The LDAP backend has
other issues that would complicate the re-design process:

  - Handlers and connection are generalized to mean the same thing
  - Method names don't follow a convention
  - Domain-specific language from python-ldap bleeds into keystone's
    implementation (e.g., get_all, _ldap_get_all, add_member) at
    different points in the backend (e.g., UserApi (BaseLdap), GroupApi
    (BaseLdap), KeystoneLDAPHandler, PooledLDAPHandler,
    PythonLDAPHandler)
  - Backend contains dead code from when keystone supported writeable
    LDAP backends
  - Responsibility for connections and connection handling is spread
    across objects (BaseLdap, LDAPHandler)
  - Handlers will invoke methods differently based on configuration at
    runtime, which is a sign that the relationship between the driver,
    handlers, and connection objects isn't truely polymorphic

While keeping the logic for properly handling context managers and
connections in the Handlers might not be ideal, it is a relatively
minimal fix in comparison to a re-design or backend refactor. These
issues can be considered during a refactor of the LDAP backend if or
when the community decides to re-design the LDAP backend.

[0] https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#ldap.LDAPObject.search_ext
[1] https://docs.python.org/3/library/weakref.html#weakref.ref
[2] https://docs.python.org/3/library/weakref.html#finalizer-objects

Closes-Bug: 1896125
Change-Id: Ia45a45ff852d0d4e3a713dae07a46d4ff8d370f3
2020-11-03 00:15:43 +00:00
api-ref/source Add default roles and scope checking to project tags 2019-09-19 02:48:39 +00:00
config-generator Move policy generator config to config-generator/ 2017-04-21 21:47:32 +00:00
devstack Add voting k2k tests 2020-02-04 22:47:52 +00:00
doc Merge "Fix relative links" 2019-09-24 18:33:34 +00:00
etc Remove policy.v3cloudsample.json 2019-10-09 17:49:28 +00:00
examples/pki Remove support for PKI and PKIz tokens 2016-11-01 22:05:01 +00:00
httpd Remove admin interface in sample Apache file 2018-03-24 12:56:02 +01:00
keystone Implement more robust connection handling for asynchronous LDAP calls 2020-11-03 00:15:43 +00:00
keystone_tempest_plugin Replace git.openstack.org URLs with opendev.org URLs 2019-04-24 11:51:00 +08:00
playbooks/legacy/keystone-dsvm-grenade-multinode OpenDev Migration Patch 2019-04-19 19:30:29 +00:00
rally-jobs fix rally docs url 2018-05-21 16:24:51 +08:00
releasenotes Implement more robust connection handling for asynchronous LDAP calls 2020-11-03 00:15:43 +00:00
tools Fix E731 flake8 2019-06-19 17:40:01 +05:30
.coveragerc Change ignore-errors to ignore_errors 2015-09-21 14:27:58 +00:00
.gitignore Tell reno to ignore the kilo branch 2020-02-21 18:56:24 +00:00
.gitreview Update .gitreview for stable/train 2019-09-27 09:11:27 +00:00
.mailmap update mailmap with gyee's new email 2015-11-03 16:12:01 -08:00
.stestr.conf Migrate to stestr 2017-09-22 11:07:09 -05:00
.zuul.yaml Make opensuse jobs nonvoting 2020-10-26 14:54:56 -07:00
CONTRIBUTING.rst Use https for docs.openstack.org references 2017-01-30 16:05:08 -08:00
HACKING.rst Merge "Update links in keystone" 2017-10-06 16:10:56 +00:00
LICENSE Added Apache 2.0 License information. 2012-02-15 17:48:33 -08:00
README.rst Update api-ref location 2019-07-23 06:53:33 +02:00
babel.cfg setting up babel for i18n work 2012-06-21 18:03:09 -07:00
bindep.txt Fix bindep for SUSE 2019-02-14 16:50:33 +01:00
lower-constraints.txt Make opensuse jobs nonvoting 2020-10-26 14:54:56 -07:00
reno.yaml Tell reno to ignore the kilo branch 2020-02-21 18:56:24 +00:00
requirements.txt Add access rules to token validation 2019-09-14 03:14:36 -07:00
setup.cfg Stop explicitly requiring pycodestyle 2020-05-14 06:56:06 -07:00
setup.py Updated from global requirements 2017-03-06 01:10:37 +00:00
test-requirements.txt Stop explicitly requiring pycodestyle 2020-05-14 06:56:06 -07:00
tox.ini Constraint dependencies for docs build 2020-02-19 20:17:57 -05:00

README.rst

Team and repository tags

image

OpenStack Keystone

Keystone provides authentication, authorization and service discovery mechanisms via HTTP primarily for use by projects in the OpenStack family. It is most commonly deployed as an HTTP interface to existing identity systems, such as LDAP.

Developer documentation, the source of which is in doc/source/, is published at:

https://docs.openstack.org/keystone/latest

The API reference and documentation are available at:

https://docs.openstack.org/api-ref/identity

The canonical client library is available at:

https://opendev.org/openstack/python-keystoneclient

Documentation for cloud administrators is available at:

https://docs.openstack.org/

The source of documentation for cloud administrators is available at:

https://opendev.org/openstack/openstack-manuals

Information about our team meeting is available at:

https://wiki.openstack.org/wiki/Meetings/KeystoneMeeting

Release notes is available at:

https://docs.openstack.org/releasenotes/keystone

Bugs and feature requests are tracked on Launchpad at:

https://bugs.launchpad.net/keystone

Future design work is tracked at:

https://specs.openstack.org/openstack/keystone-specs

Contributors are encouraged to join IRC (#openstack-keystone on freenode):

https://wiki.openstack.org/wiki/IRC

For information on contributing to Keystone, see CONTRIBUTING.rst.