Skip to content

Singleton Login Class#988

Merged
ebattat merged 1 commit intoredhat-performance:mainfrom
ebattat:singleton_login_class
Feb 24, 2025
Merged

Singleton Login Class#988
ebattat merged 1 commit intoredhat-performance:mainfrom
ebattat:singleton_login_class

Conversation

@ebattat
Copy link
Copy Markdown
Member

@ebattat ebattat commented Feb 23, 2025

Type of change

Note: Fill x in []

  • bug
  • enhancement
  • documentation
  • dependencies

Description

Currently, the login method is placed under the OC class, and each OC instance creation triggers a login call.
I have built a dedicated Singleton Login class to ensure that the login is called only once per run.
Additionally, I have removed the kubeconfig_path variable because the API server URL is not needed when running oc login.
Adding relevant test cases for SingletonLogin class.

For security reasons, all pull requests need to be approved first before running any automated CI

@ebattat ebattat added the bug Something isn't working label Feb 23, 2025
@ebattat ebattat self-assigned this Feb 23, 2025
@ebattat ebattat added the ok-to-test PR ok to test label Feb 23, 2025
@ebattat ebattat force-pushed the singleton_login_class branch from 719f5b1 to 9f7104e Compare February 24, 2025 12:16
Copy link
Copy Markdown
Member

@RobertKrawitz RobertKrawitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only substantive comments are about not making ResetSingletonLogin public (it's only used for testing) and the possibility of a race condition surrounding that. But you also need to make sure the tests run sequentially, not concurrently. Python (or at least CPython) is currently only single threaded, but if multi-threaded Python comes along and pytest allows concurrent test execution, you need to do what you can to ensure that that can't happen here.

Otherwise, minor changes.


**mandatory:** KUBEADMIN_PASSWORD=$KUBEADMIN_PASSWORD

**mandatory:** KUBECONFIG_PATH=$KUBECONFIG_PATH [config path inside the container]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment that KUBECONFIG_PATH is no longer required and why

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added it recently due to 'oc login' issue, so its a new environment variable that is not required.

from benchmark_runner.common.oc.oc_exceptions import LoginFailed


class SingletonLogin:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name it SingletonOCLogin

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

cls._instance = None

def __new__(cls, oc_instance):
if cls._instance is None: # First check (no locking)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a race condition here. Consider what happens if:

  1. cls._instance == instance1
  2. Someone calls new to create instance2
  3. The unprotected cls._instance is None returns false.
  4. instance1 calls reset_instance
  5. The new call now returns None rather than a new instance.

The only user of reset_instance I see here is the test code. Perhaps the test code should have a subclass inheriting from the singleton class that provides the reset method, so nothing else inadvertently calls it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is only one place in the code for this login.
However, if it occurs, there is a locking line afterward, so there is no option to create two instances.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reset function provides a way to clear that login, hence the race condition.

What is the purpose of checking outside of the lock rather than just inside?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only lock only after getting instance: if cls._instance is None:
I dont think we should lock every instance

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? What harm does it do? And the lock is only while getting the instance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locking before checking, loos like this:

def __new__(cls, oc_instance):
    with cls._lock:  # Acquire the lock before checking
        if cls._instance is None:
            cls._instance = super(SingletonOCLogin, cls).__new__(cls)
            cls._instance.__init_instance(oc_instance)
    return cls._instance

The best practice is to check before locking.
It is not necessary to lock the instance before checking, as this may introduce a slight performance overhead.
The double-checked locking pattern is already optimal for ensuring thread safety while minimizing locking overhead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, checking before locking (double-checked locking) is wrong unless there's an underlying guarantee of atomicity (such as the C/C++ volatile keyword). What happens if the lock is taken before checking and taking the lock (or worse yet, checking, another thread takes the lock, makes a change, and releases the lock with the protected object changed)? In this case, it would result in two calls to oc login.

Another thing that could go wrong is that the second thread might get a partially initialized object (what happens if the object is created and assigned before the object's init is called?).

Locking overhead is minimal in this case, given that at present only one thread will be running and that oc login takes much longer than taking and releasing the lock.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the design pattern recommendation, the check should be done before acquiring the lock. For more details, read this article.


**mandatory:** KUBEADMIN_PASSWORD=$KUBEADMIN_PASSWORD

**mandatory:** KUBECONFIG_PATH=$KUBECONFIG_PATH [config path inside the container]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document that this has been removed and why.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added it recently due to 'oc login' issue, so its a new environment variable that is not required.


**optional:** KUBEADMIN_PASSWORD=$KUBEADMIN_PASSWORD

**mandatory:** KUBECONFIG_PATH=$KUBECONFIG_PATH [config path inside the container]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document that this has been removed and why.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added it recently due to 'oc login' issue, so its a new environment variable that is not required.

Comment on lines +17 to +18


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create the TestSingletonLogin (or TestSingletonOCLogin) class here. Maybe ResetSingletonOCLogin would be an even better name for the class since it's only being used for that purpose; everything else just uses SingletonLogin/SingletonOCLogin.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the file name to test_singleton_oc_login.py.
No need to create a class TestSingletonOCLogin, I am using pytest and not unittest.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to avoid having that dangerous reset member available. It's only needed for testing and should not be accessible outside of testing.

TestSingletonOCLogin is not correct, as pytest keys off the name. Perhaps ResetSingletonOCLogin would be a better name for it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add the following fixture to pytest

@pytest.fixture(autouse=True)
def reset_singleton():
    """Fixture to reset SingletonOCLogin before each test."""
    with SingletonOCLogin._lock:
        SingletonOCLogin._instance = None

and delete reset_instance method from SingletonOCLogin class


    @classmethod
    def reset_instance(cls):
        with cls._lock:  # Acquire the lock for instance reset
            cls._instance = None
        

Copy link
Copy Markdown
Member

@RobertKrawitz RobertKrawitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment for requested changes.

@ebattat ebattat force-pushed the singleton_login_class branch 4 times, most recently from 0132c12 to 80124fd Compare February 24, 2025 17:50
Comment on lines +15 to +19
if cls._instance is None: # First check (no locking)
with cls._lock: # Acquire the lock for instance creation
if cls._instance is None: # Second check (with locking)
cls._instance = super(SingletonOCLogin, cls).__new__(cls)
cls._instance.__init_instance(oc_instance)
Copy link
Copy Markdown
Member

@RobertKrawitz RobertKrawitz Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to remove the double-checked lock. Period.

@ebattat ebattat force-pushed the singleton_login_class branch from 80124fd to 887f5e5 Compare February 24, 2025 19:54
@ebattat ebattat force-pushed the singleton_login_class branch from 887f5e5 to 8746f40 Compare February 24, 2025 19:59
@ebattat ebattat merged commit 210f73b into redhat-performance:main Feb 24, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ok-to-test PR ok to test

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants