Skip to content

Commit

Permalink
Changed the API function that handles processing of k8s user app stat…
Browse files Browse the repository at this point in the history
…us codes from the event listener to now update the new field k8s_user_app_status rather than app_status.
  • Loading branch information
alfredeen committed Dec 13, 2024
1 parent 5754844 commit e6a2fcc
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 45 deletions.
123 changes: 120 additions & 3 deletions apps/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime
from enum import Enum
from typing import Optional
from typing import Any, Optional

import regex as re
from django.core.exceptions import ObjectDoesNotExist, ValidationError
Expand All @@ -9,7 +9,7 @@
from apps.types_.subdomain import SubdomainCandidateName, SubdomainTuple
from studio.utils import get_logger

from .models import Apps, AppStatus, BaseAppInstance, Subdomain
from .models import Apps, AppStatus, BaseAppInstance, K8sUserAppStatus, Subdomain

logger = get_logger(__name__)

Expand Down Expand Up @@ -104,6 +104,89 @@ class HandleUpdateStatusResponseCode(Enum):

def handle_update_status_request(
release: str, new_status: str, event_ts: datetime, event_msg: Optional[str] = None
) -> HandleUpdateStatusResponseCode:
"""
Helper function to handle update k8s user app status requests by determining if the request should be performed or
ignored.
Technically this function either updates or creates and persists a new K8sUserAppStatus object.
:param release str: The release id of the app instance, stored in the AppInstance.k8s_values dict in the subdomain.
:param new_status str: The new status code. Trimmed to max 15 chars if needed.
:param event_ts timestamp: A JSON-formatted timestamp in UTC, e.g. 2024-01-25T16:02:50.00Z.
:param event_msg json dict: An optional json dict containing pod-msg and/or container-msg.
:returns: A value from the HandleUpdateStatusResponseCode enum.
Raises an ObjectDoesNotExist exception if the app instance does not exist.
"""

if len(new_status) > 20:
new_status = new_status[:20]

try:
# Begin by verifying that the requested app instance exists
# We wrap the select and update tasks in a select_for_update lock
# to avoid race conditions.

# release takes on the value of the subdomain
subdomain = Subdomain.objects.get(subdomain=release)

with transaction.atomic():
instance = BaseAppInstance.objects.select_for_update().filter(subdomain=subdomain).last()
if instance is None:
logger.info(f"The specified app instance identified by release {release} was not found")
raise ObjectDoesNotExist

logger.debug(f"The app instance identified by release {release} exists. App name={instance.name}")

# Also get the latest k8s_user_app_status object for this app instance
if instance.k8s_user_app_status is None:
# Missing k8s_user_app_status so create one now
logger.debug(f"AppInstance {release} does not have an associated K8sUserAppStatus. Creating one now.")
k8s_user_app_status = K8sUserAppStatus.objects.create()
update_k8s_user_app_status(instance, k8s_user_app_status, new_status, event_ts, event_msg)
return HandleUpdateStatusResponseCode.CREATED_FIRST_STATUS
else:
k8s_user_app_status = instance.k8s_user_app_status

logger.debug(
f"K8sUserAppStatus object was created or updated with status {k8s_user_app_status.status}, \
ts={k8s_user_app_status.time}, {k8s_user_app_status.info}"
)

# Now determine whether to update the state and status

# Compare timestamps
time_ftm = "%Y-%m-%d %H:%M:%S"
if event_ts <= k8s_user_app_status.time:
msg = "The incoming event-ts is older than the current status ts so nothing to do."
msg += f"event_ts={event_ts.strftime(time_ftm)} vs \
k8s_user_app_status.time={str(k8s_user_app_status.time.strftime(time_ftm))}"
logger.debug(msg)
return HandleUpdateStatusResponseCode.NO_ACTION

# The event is newer than the existing persisted object

if new_status == instance.k8s_user_app_status.status:
# The same status. Simply update the time.
logger.debug(f"The same status {new_status}. Simply update the time.")
update_status_time(k8s_user_app_status, event_ts, event_msg)
return HandleUpdateStatusResponseCode.UPDATED_TIME_OF_STATUS

# Different status and newer time
logger.debug(
f"Different status and newer time. New status={new_status} vs Old={instance.k8s_user_app_status.status}"
)
status_object = instance.k8s_user_app_status
update_k8s_user_app_status(instance, status_object, new_status, event_ts, event_msg)
return HandleUpdateStatusResponseCode.UPDATED_STATUS

except Exception as err:
logger.error(f"Unable to fetch or update the specified app instance with release={release}. {err}, {type(err)}")
raise


# TODO: Consider removing after refactoring.
def _handle_update_status_request_old(
release: str, new_status: str, event_ts: datetime, event_msg: Optional[str] = None
) -> HandleUpdateStatusResponseCode:
"""
Helper function to handle update app status requests by determining if the
Expand All @@ -117,6 +200,8 @@ def handle_update_status_request(
Raises an ObjectDoesNotExist exception if the app instance does not exist.
"""

raise Exception("This method has been deprecated. To be removed.")

if len(new_status) > 15:
new_status = new_status[:15]

Expand Down Expand Up @@ -183,6 +268,38 @@ def handle_update_status_request(
raise


@transaction.atomic
def update_k8s_user_app_status(
appinstance: BaseAppInstance,
status_object: K8sUserAppStatus,
status: str,
status_ts: datetime = None,
event_msg: str = None,
):
"""
Helper function to update the k8s user app status of an appinstance and a status object.
"""
# Persist a new app statuss object
status_object.status = status
status_object.time = status_ts
status_object.info = event_msg
status_object.save()

# Must re-save the app statuss object with the new event ts
status_object.time = status_ts

if event_msg is None:
status_object.save(update_fields=["time"])
else:
status_object.info = event_msg
status_object.save(update_fields=["time", "info"])

# Update the app instance object
appinstance.k8s_user_app_status = status_object
appinstance.save(update_fields=["k8s_user_app_status"])


# TODO: This may no longer be needed after refactoring.
@transaction.atomic
def update_status(appinstance, status_object, status, status_ts=None, event_msg=None):
"""
Expand All @@ -209,7 +326,7 @@ def update_status(appinstance, status_object, status, status_ts=None, event_msg=


@transaction.atomic
def update_status_time(status_object, status_ts, event_msg=None):
def update_status_time(status_object: Any, status_ts: datetime, event_msg: str = None):
"""
Helper function to update the time of an app status event.
"""
Expand Down
93 changes: 51 additions & 42 deletions apps/tests/test_update_status_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from projects.models import Project

from ..helpers import HandleUpdateStatusResponseCode, handle_update_status_request
from ..models import AppCategories, Apps, AppStatus, JupyterInstance, Subdomain
from ..models import AppCategories, Apps, JupyterInstance, K8sUserAppStatus, Subdomain

utc = pytz.UTC

Expand All @@ -34,7 +34,7 @@ class UpdateAppStatusTestCase(TestCase):
"""Test case for requests operating on an existing app instance."""

ACTUAL_RELEASE_NAME = "test-release-name"
INITIAL_STATUS = "Created"
INITIAL_STATUS = "Unknown"
INITIAL_EVENT_TS = utc.localize(datetime.now())

def setUp(self) -> None:
Expand Down Expand Up @@ -64,18 +64,19 @@ def setUp(self) -> None:
)
print(f"######## {self.INITIAL_EVENT_TS}")

def setUpCreateAppStatus(self):
app_status = AppStatus.objects.create(status=self.INITIAL_STATUS)
app_status.time = self.INITIAL_EVENT_TS
app_status.save()
self.app_instance.app_status = app_status
self.app_instance.save(update_fields=["app_status"])
print(f"######## {app_status.time}")
def setUpCreateK8sUserAppStatus(self):
k8s_user_app_status = K8sUserAppStatus.objects.create(status=self.INITIAL_STATUS)
k8s_user_app_status.time = self.INITIAL_EVENT_TS
k8s_user_app_status.save()
self.app_instance.k8s_user_app_status = k8s_user_app_status
self.app_instance.save(update_fields=["k8s_user_app_status"])
print(f"######## {k8s_user_app_status.time}")

def test_handle_old_event_time_should_ignore_update(self):
self.setUpCreateAppStatus()
older_ts = self.app_instance.app_status.time - timedelta(seconds=1)
actual = handle_update_status_request(self.ACTUAL_RELEASE_NAME, "NewStatus", older_ts)
self.setUpCreateK8sUserAppStatus()
older_ts = self.app_instance.k8s_user_app_status.time - timedelta(seconds=1)
new_status = "PodInitializing"
actual = handle_update_status_request(self.ACTUAL_RELEASE_NAME, new_status, older_ts)

assert actual == HandleUpdateStatusResponseCode.NO_ACTION

Expand All @@ -84,14 +85,14 @@ def test_handle_old_event_time_should_ignore_update(self):
k8s_values__contains={"release": self.ACTUAL_RELEASE_NAME}
).last()

assert actual_app_instance.app_status.status == self.INITIAL_STATUS
actual_appstatus = actual_app_instance.app_status
assert actual_appstatus.status == self.INITIAL_STATUS
assert actual_appstatus.time == self.INITIAL_EVENT_TS
assert actual_app_instance.k8s_user_app_status.status == self.INITIAL_STATUS
actual_k8suser_appstatus = actual_app_instance.k8s_user_app_status
assert actual_k8suser_appstatus.status == self.INITIAL_STATUS
assert actual_k8suser_appstatus.time == self.INITIAL_EVENT_TS

def test_handle_same_status_newer_time_should_update_time(self):
self.setUpCreateAppStatus()
newer_ts = self.app_instance.app_status.time + timedelta(seconds=1)
self.setUpCreateK8sUserAppStatus()
newer_ts = self.app_instance.k8s_user_app_status.time + timedelta(seconds=1)
actual = handle_update_status_request(self.ACTUAL_RELEASE_NAME, self.INITIAL_STATUS, newer_ts)

assert actual == HandleUpdateStatusResponseCode.UPDATED_TIME_OF_STATUS
Expand All @@ -101,15 +102,16 @@ def test_handle_same_status_newer_time_should_update_time(self):
k8s_values__contains={"release": self.ACTUAL_RELEASE_NAME}
).last()

assert actual_app_instance.app_status.status == self.INITIAL_STATUS
actual_appstatus = actual_app_instance.app_status
assert actual_appstatus.status == self.INITIAL_STATUS
assert actual_appstatus.time == newer_ts
assert actual_app_instance.k8s_user_app_status.status == self.INITIAL_STATUS
actual_k8suser_appstatus = actual_app_instance.k8s_user_app_status
assert actual_k8suser_appstatus.status == self.INITIAL_STATUS
assert actual_k8suser_appstatus.time == newer_ts

def test_handle_different_status_newer_time_should_update_status(self):
self.setUpCreateAppStatus()
newer_ts = self.app_instance.app_status.time + timedelta(seconds=1)
new_status = self.INITIAL_STATUS + "-test01"
self.setUpCreateK8sUserAppStatus()
newer_ts = self.app_instance.k8s_user_app_status.time + timedelta(seconds=1)
# new_status = self.INITIAL_STATUS + "-test01"
new_status = "PodInitializing"
actual = handle_update_status_request(self.ACTUAL_RELEASE_NAME, new_status, newer_ts)

assert actual == HandleUpdateStatusResponseCode.UPDATED_STATUS
Expand All @@ -119,14 +121,15 @@ def test_handle_different_status_newer_time_should_update_status(self):
k8s_values__contains={"release": self.ACTUAL_RELEASE_NAME}
).last()

assert actual_app_instance.app_status.status == new_status
actual_appstatus = actual_app_instance.app_status
assert actual_appstatus.status == new_status
assert actual_appstatus.time == newer_ts
assert actual_app_instance.k8s_user_app_status.status == new_status
actual_k8suser_appstatus = actual_app_instance.k8s_user_app_status
assert actual_k8suser_appstatus.status == new_status
assert actual_k8suser_appstatus.time == newer_ts

def test_handle_missing_app_status_should_create_and_update_status(self):
def test_handle_missing_k8s_user_app_status_should_create_and_update_status(self):
newer_ts = self.INITIAL_EVENT_TS + timedelta(seconds=1)
new_status = self.INITIAL_STATUS + "-test02"
# new_status = self.INITIAL_STATUS + "-test02"
new_status = "PodInitializing"
actual = handle_update_status_request(self.ACTUAL_RELEASE_NAME, new_status, newer_ts)

assert actual == HandleUpdateStatusResponseCode.CREATED_FIRST_STATUS
Expand All @@ -136,16 +139,22 @@ def test_handle_missing_app_status_should_create_and_update_status(self):
k8s_values__contains={"release": self.ACTUAL_RELEASE_NAME}
).last()

assert actual_app_instance.app_status.status == new_status
actual_appstatus = actual_app_instance.app_status
assert actual_appstatus.status == new_status
assert actual_appstatus.time == newer_ts
assert actual_app_instance.k8s_user_app_status.status == new_status
actual_k8suser_appstatus = actual_app_instance.k8s_user_app_status
assert actual_k8suser_appstatus.status == new_status
assert actual_k8suser_appstatus.time == newer_ts

@pytest.mark.skip("Skipped because the k8s_user_app_status field is now restricted to a domain of values.")
def test_handle_long_status_text_should_trim_status(self):
"""
This test verifies that the status code can be trimmed to a max length of chars and used.
TODO: Revisit:
NOTE: This is undergoing refactoring and this test may no longer be valid.
"""
newer_ts = self.INITIAL_EVENT_TS + timedelta(seconds=1)
new_status = "LongStatusText-ThisPartLongerThan15Chars"
expected_status_text = new_status[:15]
assert len(expected_status_text) == 15
new_status = "LongStatusText-ThisPartLongerThan20Chars"
expected_status_text = new_status[:20]
assert len(expected_status_text) == 20
actual = handle_update_status_request(self.ACTUAL_RELEASE_NAME, new_status, newer_ts)

assert actual == HandleUpdateStatusResponseCode.CREATED_FIRST_STATUS
Expand All @@ -155,10 +164,10 @@ def test_handle_long_status_text_should_trim_status(self):
k8s_values__contains={"release": self.ACTUAL_RELEASE_NAME}
).last()

assert actual_app_instance.app_status.status == expected_status_text
actual_appstatus = actual_app_instance.app_status
assert actual_appstatus.status == expected_status_text
assert actual_appstatus.time == newer_ts
assert actual_app_instance.k8s_user_app_status.status == expected_status_text
actual_k8suser_appstatus = actual_app_instance.k8s_user_app_status
assert actual_k8suser_appstatus.status == expected_status_text
assert actual_k8suser_appstatus.time == newer_ts


'''
Expand Down

0 comments on commit e6a2fcc

Please sign in to comment.