This is an automated email from the ASF dual-hosted git repository.
yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
The following commit(s) were added to refs/heads/main by this push:
new f299283b5 refactor(portal): remove the legacy Airavata Thrift client
and its dead admin views (#192)
f299283b5 is described below
commit f299283b59c037da2244341d8f4ce64a4d6e8812
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Jun 9 03:07:04 2026 -0400
refactor(portal): remove the legacy Airavata Thrift client and its dead
admin views (#192)
With the functional Thrift->gRPC migration complete, nothing reads
request.airavata_client except two admin-only per-protocol detail views that
never worked on the gRPC stack:
- GlobusJobSubmissionView called request.airavata_client.getClo, a typo that
always AttributeError'd (the view was already broken).
- UnicoreDataMovementView used getUnicoreDataMovement, for which the gRPC
SDK
has no facade getter.
Delete both views (+ their URL routes and the now-unused
GlobusJobSubmission /
UnicoreDataMovement Thrift-model imports), which removes the last
request.airavata_client consumers. Then remove the legacy Thrift client
itself:
- AiravataClientMiddleware (and its registration in settings.MIDDLEWARE)
- the entire django_airavata/utils.py Thrift-client module (the airavata API
Thrift pool, transports, connection helpers) -- nothing imports it anymore
- the thrift_connector dependency and the now-unused
THRIFT_CLIENT_POOL_KEEPALIVE
setting
What stays (interim, removed in the later serializer/terminology pass): the
thrift_utils-generated DRF serializers + grpc_adapters/grpc_requests still
read
Thrift model TYPES (airavata.model.*.ttypes), so the
airavata-python-sdk==2.2.7
and thrift libs remain for the model types only -- no Thrift
client/transport.
manage.py check clean; the request pipeline serves gRPC requests without
AiravataClientMiddleware (verified live); no remaining
request.airavata_client
references; deleted views gone with sibling per-protocol views intact.
---
.../django_airavata/airavata_grpc.py | 15 +-
.../django_airavata/apps/api/urls.py | 4 -
.../django_airavata/apps/api/views.py | 35 +---
.../django_airavata/middleware.py | 59 +-----
airavata-django-portal/django_airavata/settings.py | 13 +-
airavata-django-portal/django_airavata/utils.py | 231 ---------------------
airavata-django-portal/requirements.txt | 1 -
7 files changed, 16 insertions(+), 342 deletions(-)
diff --git a/airavata-django-portal/django_airavata/airavata_grpc.py
b/airavata-django-portal/django_airavata/airavata_grpc.py
index 203d80e80..4f281063e 100644
--- a/airavata-django-portal/django_airavata/airavata_grpc.py
+++ b/airavata-django-portal/django_airavata/airavata_grpc.py
@@ -1,12 +1,9 @@
-"""New-stack gRPC Airavata client (airavata-python-sdk ``AiravataClient``).
-
-Track D: the portal is migrating from the legacy Thrift API to the new Airavata
-gRPC/REST server. This module builds the gRPC ``AiravataClient`` from a
request's
-Keycloak access token. It is intentionally additive — the gRPC client
-(``request.airavata``) coexists with the legacy Thrift client
-(``request.airavata_client``) while ``apps/api`` views are repointed resource
-family by resource family. The Thrift client and ``thrift_utils`` are removed
once
-nothing depends on them.
+"""gRPC Airavata client (airavata-python-sdk ``AiravataClient``).
+
+Builds the gRPC ``AiravataClient`` (``request.airavata``) from a request's
Keycloak
+access token. The ``apps/api`` views/serializers talk to Airavata through this
+client's facade sub-clients; the interim ``thrift_utils`` serializer layer
(which
+still reads Thrift model types) is removed in the later serializer/terminology
pass.
Per the migration principle, the "talk to Airavata + transform" grunt belongs
in
``airavata-python-sdk`` (the facade sub-clients on ``AiravataClient``),
keeping the
diff --git a/airavata-django-portal/django_airavata/apps/api/urls.py
b/airavata-django-portal/django_airavata/apps/api/urls.py
index 5b864b578..a83e67bf3 100644
--- a/airavata-django-portal/django_airavata/apps/api/urls.py
+++ b/airavata-django-portal/django_airavata/apps/api/urls.py
@@ -62,8 +62,6 @@ urlpatterns = [
name="local_job_submission"),
re_path(r'^job/submission/cloud', views.CloudJobSubmissionView.as_view(),
name="cloud_job_submission"),
- re_path(r'^job/submission/globus', views.GlobusJobSubmissionView.as_view(),
- name="globus_job_submission"),
re_path(r'^job/submission/ssh', views.SshJobSubmissionView.as_view(),
name="ssh_job_submission"),
re_path(r'^job/submission/unicore',
views.UnicoreJobSubmissionView.as_view(),
@@ -72,8 +70,6 @@ urlpatterns = [
name="grid_ftp_data_movement"),
re_path(r'^data/movement/local', views.LocalDataMovementView.as_view(),
name="local_ftp_data_movement"),
- re_path(r'^data/movement/unicore', views.UnicoreDataMovementView.as_view(),
- name="unicore_ftp_data_movement"),
re_path(r'^data/movement/scp', views.ScpDataMovementView.as_view(),
name="scp_ftp_data_movement"),
re_path(r'^gateway-resource-profile',
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py
b/airavata-django-portal/django_airavata/apps/api/views.py
index 79a2ef19c..2de39dd25 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -9,7 +9,6 @@ from urllib.parse import quote
from airavata.model.appcatalog.computeresource.ttypes import (
CloudJobSubmission,
- GlobusJobSubmission,
LOCALSubmission,
SSHJobSubmission,
UnicoreJobSubmission
@@ -19,8 +18,7 @@ from airavata.model.credential.store.ttypes import SummaryType
from airavata.model.data.movement.ttypes import (
GridFTPDataMovement,
LOCALDataMovement,
- SCPDataMovement,
- UnicoreDataMovement
+ SCPDataMovement
)
from airavata.model.experiment.ttypes import (
ExperimentModel,
@@ -803,22 +801,6 @@ class CloudJobSubmissionView(APIView):
instance=job_submission).data)
-class GlobusJobSubmissionView(APIView):
- renderer_classes = (JSONRenderer,)
-
- def get(self, request, format=None):
- # TODO: the gRPC compute facade has no Globus job-submission getter and
- # the legacy Thrift call here (`getClo`) is broken, so this admin-only
- # detail view stays unmigrated until backend support lands.
- job_submission_id = request.query_params["id"]
- job_submission = request.airavata_client.getClo(
- request.authz_token, job_submission_id)
- return Response(
- thrift_utils.create_serializer(
- GlobusJobSubmission,
- instance=job_submission).data)
-
-
class SshJobSubmissionView(APIView):
renderer_classes = (JSONRenderer,)
@@ -871,21 +853,6 @@ class ScpDataMovementView(APIView):
instance=data_movement).data)
-class UnicoreDataMovementView(APIView):
- renderer_classes = (JSONRenderer,)
-
- def get(self, request, format=None):
- # TODO: the gRPC storage facade has no UNICORE data-movement getter
yet,
- # so this admin-only detail view stays on Thrift until the SDK adds
one.
- data_movement_id = request.query_params["id"]
- data_movement = request.airavata_client.getUnicoreDataMovement(
- request.authz_token, data_movement_id)
- return Response(
- thrift_utils.create_serializer(
- UnicoreDataMovement,
- instance=data_movement).data)
-
-
class LocalDataMovementView(APIView):
renderer_classes = (JSONRenderer,)
diff --git a/airavata-django-portal/django_airavata/middleware.py
b/airavata-django-portal/django_airavata/middleware.py
index 74506d692..d2006f3de 100644
--- a/airavata-django-portal/django_airavata/middleware.py
+++ b/airavata-django-portal/django_airavata/middleware.py
@@ -1,63 +1,16 @@
import logging
-import thrift
-import thrift.transport.TTransport
-from django.shortcuts import render
-
-from . import utils
-
logger = logging.getLogger(__name__)
-class AiravataClientMiddleware:
- def __init__(self, get_response):
- self.get_response = get_response
-
- def __call__(self, request):
- # Track D: attach the Thrift client lazily so requests that only use
the
- # gRPC client (request.airavata) never open a Thrift connection. The
- # connection is acquired from the pool on first access and released
after
- # the response. Views/serializers still on Thrift work unchanged.
- from django.utils.functional import SimpleLazyObject
-
- opened = []
-
- def _client():
- ctx = utils.airavata_api_client_pool.connection()
- client = ctx.__enter__()
- opened.append(ctx)
- return client
-
- request.airavata_client = SimpleLazyObject(_client)
- try:
- return self.get_response(request)
- finally:
- for ctx in opened:
- ctx.__exit__(None, None, None)
-
- def process_exception(self, request, exception):
- if isinstance(exception,
thrift.transport.TTransport.TTransportException):
- return render(
- request,
- 'django_airavata/error_page.html',
- status=500,
- context={
- 'title': 'Airavata is down',
- 'text': """The Airavata API server is not reachable.
Please try again."""})
- else:
- return None
-
-
def airavata_grpc_client(get_response):
- """Attach the new-stack gRPC ``AiravataClient`` as ``request.airavata``.
+ """Attach the gRPC ``AiravataClient`` as ``request.airavata``.
- Track D: additive — coexists with the legacy Thrift
``request.airavata_client``
- while ``apps/api`` views are repointed from Thrift to gRPC.
``request.airavata``
- is a lazy object: the client (and the ``airavata_sdk`` import) is built
only
- when a view first accesses it, carrying the user's Keycloak token from
- ``request.authz_token``. The channel is closed after the response if it was
- used. Views that never touch ``request.airavata`` incur no cost and do not
- require the SDK to be importable.
+ ``request.airavata`` is a lazy object: the client (and the ``airavata_sdk``
+ import) is built only when a view first accesses it, carrying the user's
+ Keycloak token from ``request.authz_token``. The channel is closed after
the
+ response if it was used. Views that never touch ``request.airavata`` incur
no
+ cost and do not require the SDK to be importable.
Usage in a view::
diff --git a/airavata-django-portal/django_airavata/settings.py
b/airavata-django-portal/django_airavata/settings.py
index c951a101b..4e1fcb37f 100644
--- a/airavata-django-portal/django_airavata/settings.py
+++ b/airavata-django-portal/django_airavata/settings.py
@@ -70,12 +70,10 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_airavata.apps.auth.middleware.authz_token_middleware',
- 'django_airavata.middleware.AiravataClientMiddleware',
- # Track D: new gRPC AiravataClient (request.airavata), additive alongside
the
- # Thrift request.airavata_client. Must come after authz_token_middleware
(uses
- # request.authz_token for the access token).
+ # gRPC AiravataClient (request.airavata). Must come after
authz_token_middleware
+ # (uses request.authz_token for the access token).
'django_airavata.middleware.airavata_grpc_client',
- # Needs to come after authz_token_middleware and airavata_client.
+ # Needs to come after authz_token_middleware and airavata_grpc_client.
'django_airavata.apps.auth.middleware.gateway_groups_middleware',
'django_airavata.apps.auth.middleware.user_profile_completeness_check',
]
@@ -262,11 +260,6 @@ AUTHENTICATION_OPTIONS = {
# for the access token parameter (defaults to 'access_token').
ACCESS_TOKEN_REDIRECT_ALLOWED_URIS = []
-# Seconds each connection in the pool is able to stay alive. If open connection
-# has lived longer than this period, it will be closed.
-# (https://github.com/Thriftpy/thrift_connector)
-THRIFT_CLIENT_POOL_KEEPALIVE = 5
-
# Webpack loader
WEBPACK_LOADER = {
'COMMON': {
diff --git a/airavata-django-portal/django_airavata/utils.py
b/airavata-django-portal/django_airavata/utils.py
deleted file mode 100644
index e71b4e7d2..000000000
--- a/airavata-django-portal/django_airavata/utils.py
+++ /dev/null
@@ -1,231 +0,0 @@
-import logging
-import ssl
-import queue
-from django.conf import settings
-from contextlib import contextmanager
-
-import thrift_connector.connection_pool as connection_pool
-from airavata.api import Airavata
-from thrift.protocol import TBinaryProtocol
-from thrift.protocol.TMultiplexedProtocol import TMultiplexedProtocol
-from thrift.transport import TSocket, TSSLSocket, TTransport
-
-log = logging.getLogger(__name__)
-
-
-class ThriftConnectionException(Exception):
- pass
-
-
-class ThriftClientException(Exception):
- pass
-
-
-def get_unsecure_transport(hostname, port):
- # Create a socket to the Airavata Server
- transport = TSocket.TSocket(hostname, port)
-
- # Use Buffered Protocol to speedup over raw sockets
- transport = TTransport.TBufferedTransport(transport)
- return transport
-
-
-def get_secure_transport(hostname, port):
-
- # Create a socket to the Airavata Server
- transport = TSSLSocket.TSSLSocket(
- hostname,
- port,
- cert_reqs=ssl.CERT_REQUIRED,
- ca_certs=settings.CA_CERTS_PATH,
- )
- return TTransport.TBufferedTransport(transport)
-
-
-def get_transport(hostname, port, secure=True):
- if secure:
- transport = get_secure_transport(hostname, port)
- else:
- transport = get_unsecure_transport(hostname, port)
- return transport
-
-
-def create_airavata_client(transport):
-
- # Airavata currently uses Binary Protocol
- protocol = TBinaryProtocol.TBinaryProtocol(transport)
-
- # Create a Airavata client to use the protocol encoder
- client = Airavata.Client(protocol)
- return client
-
-
-def get_binary_protocol(transport):
- return TBinaryProtocol.TBinaryProtocol(transport)
-
-
-def get_airavata_client():
- """Get Airavata API client as context manager (use in `with statement`)."""
- return get_thrift_client(settings.AIRAVATA_API_HOST,
- settings.AIRAVATA_API_PORT,
- settings.AIRAVATA_API_SECURE,
- create_airavata_client)
-
-
-@contextmanager
-def get_thrift_client(host, port, is_secure, client_generator):
- transport = get_transport(host, port, is_secure)
- client = client_generator(transport)
-
- try:
- transport.open()
- log.debug("Thrift connection opened to {}:{}, "
- "secure={}".format(host, port, is_secure))
- try:
- yield client
- except Exception as e:
- log.exception("Thrift client error occurred")
- raise ThriftClientException(
- "Thrift client error occurred: " + str(e)) from e
- finally:
- if transport.isOpen():
- transport.close()
- log.debug("Thrift connection closed to {}:{}, "
- "secure={}".format(host, port, is_secure))
- except ThriftClientException as tce:
- # Allow thrift client errors to bubble up
- raise tce
- except Exception as e:
- msg = "Failed to open thrift connection to {}:{}, secure={}".format(
- host, port, is_secure)
- log.debug(msg)
- raise ThriftConnectionException(msg) from e
-
-
-@contextmanager
-def simple_thrift_connection(pool):
- """Context manager for borrowing a connection from the pool."""
- conn = pool.get_connection()
- try:
- yield conn['client']
- finally:
- pool.return_connection(conn)
-
-
-class SimpleThriftPool:
- """
- A thread-safe Thrift connection pool that uses raw Thrift and the
TBufferedTransport.
- """
-
- def __init__(self, service, host, port, size=5, secure=False,
ca_certs=None):
- self._service = service
- self._host = host
- self._port = port
- self._size = size
- self._secure = secure
- self._ca_certs = ca_certs or settings.CA_CERTS_PATH
- self._pool = queue.Queue(maxsize=size)
- self._initialize_pool()
-
- def _initialize_pool(self):
- for _ in range(self._size):
- self._pool.put(self._create_connection())
-
- def _create_connection(self):
- if self._secure:
- socket = TSSLSocket.TSSLSocket(
- host=self._host,
- port=self._port,
- cert_reqs=ssl.CERT_REQUIRED,
- ca_certs=self._ca_certs,
- )
- else:
- socket = TSocket.TSocket(host=self._host, port=self._port)
- transport = TTransport.TBufferedTransport(socket)
- protocol = TBinaryProtocol.TBinaryProtocol(transport)
- client = self._service.Client(protocol)
- transport.open()
- return {'client': client, 'transport': transport}
-
- def get_connection(self):
- conn = self._pool.get()
- if not self._is_alive(conn):
- log.debug("Stale connection detected, creating new one")
- try:
- conn['transport'].close()
- except Exception:
- pass
- conn = self._create_connection()
- return conn
-
- def _is_alive(self, conn):
- try:
- if not conn['transport'].isOpen():
- return False
- conn['client'].getAPIVersion()
- return True
- except Exception:
- return False
-
- def return_connection(self, conn):
- try:
- if conn['transport'].isOpen():
- conn['transport'].close()
- except Exception:
- pass
- self._pool.put(self._create_connection())
-
- def connection(self):
- return simple_thrift_connection(self)
-
-
-class CustomThriftClient(connection_pool.ThriftClient):
- secure = False
- validate = False
-
- @classmethod
- def get_socket_factory(cls):
- if not cls.secure:
- return super().get_socket_factory()
- else:
- def factory(host, port):
- return TSSLSocket.TSSLSocket(
- host,
- port,
- cert_reqs=ssl.CERT_REQUIRED,
- ca_certs=settings.CA_CERTS_PATH,
- )
-
- return factory
-
- def ping(self):
- try:
- self.client.getAPIVersion()
- except Exception as e:
- log.debug("getAPIVersion failed: {}".format(str(e)))
- raise
-
-
-class MultiplexThriftClientMixin:
- service_name = None
-
- @classmethod
- def get_protoco_factory(cls):
- def factory(transport):
- protocol = TBinaryProtocol.TBinaryProtocol(transport)
- multiplex_prot = TMultiplexedProtocol(protocol, cls.service_name)
- return multiplex_prot
- return factory
-
-
-class AiravataAPIThriftClient(CustomThriftClient):
- secure = settings.AIRAVATA_API_SECURE
-
-
-airavata_api_client_pool = SimpleThriftPool(
- Airavata,
- settings.AIRAVATA_API_HOST,
- settings.AIRAVATA_API_PORT,
- secure=settings.AIRAVATA_API_SECURE,
- ca_certs=settings.CA_CERTS_PATH,
-)
diff --git a/airavata-django-portal/requirements.txt
b/airavata-django-portal/requirements.txt
index 13442761d..49045bbff 100644
--- a/airavata-django-portal/requirements.txt
+++ b/airavata-django-portal/requirements.txt
@@ -3,7 +3,6 @@ Django==5.2.15
requests==2.25.1
requests-oauthlib==0.7.0
thrift==0.22.0
-thrift_connector==0.24
djangorestframework==3.16.1
django-webpack-loader==0.6.0
logging-formatter-anticrlf==1.2