Skip to content

Commit 796896a

Browse files
authored
Merge pull request #35 from TaskarCenterAtUW/feature-1102
Implemented automatic message lock renewal and enhanced concurrent message processing
2 parents 5ff74f8 + 9c812a5 commit 796896a

9 files changed

Lines changed: 208 additions & 72 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ ipython_config.py
9393

9494
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
9595
__pypackages__/
96-
version.py
9796
.build/
9897
.env
9998
service_details.json

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Change log
22

3+
# Version 0.0.21
4+
### New Features and Enhancements
5+
- **Message Lock Renewal:** Implemented a mechanism to automatically renew message locks during processing. This ensures that messages remain active and are not returned to the queue for reprocessing while they are being handled.
6+
- **Concurrent Message Processing:** Enhanced the system to process messages concurrently using a number of worker threads equal to the number of available CPU cores by default. Users can override this default by specifying the `max_concurrent_messages` parameter, for example, `core.get_topic(topic_name=topic_name, max_concurrent_messages=10)`. This optimization leverages system resources for improved performance and throughput.
7+
- **Completion Acknowledgement:** Updated the processing flow to wait until message processing is fully completed before sending the acknowledgement of message completion. This change ensures reliable processing and accurate message handling.
8+
- **Version Tracking:** Introduced a `version.py` file to maintain and track the package version. This addition facilitates version control and package management.
9+
- **Unit Test Updates:** Updated unit test cases to cover the new features and enhancements, ensuring robust testing and quality assurance.
10+
- **Documentation Update:** Updated the README file to reflect the new features and enhancements, providing clearer guidance and information for users.
11+
12+
13+
314
# 0.0.18
415
- Adds extra checks in service bus for retries
516
- Additional logging done if there is no network and service bus crashes

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Eg.
2525
```python
2626
from python_ms_core import Core
2727
core = Core() or Core(config='Local')
28+
29+
# To check the package version
30+
Core.__version__ # 0.0.21
2831
```
2932
The method analyzes the `.env` variables and does a health check on what components are available
3033

@@ -109,7 +112,8 @@ Topic can be accessed by the core method `get_topic`. This method takes two para
109112
from python_ms_core import Core
110113

111114
core = Core()
112-
topic = core.get_topic(topic_name='topicName')
115+
topic = core.get_topic(topic_name='topicName') # By default, process messages concurrently which are available CPU cores
116+
topic = core.get_topic(topic_name='topicName', max_concurrent_messages=10) # Process 10 messages concurrently
113117

114118
```
115119

@@ -293,7 +297,7 @@ The project is configured with `python` to figure out the coverage of the unit t
293297
- The terminal will show the output of coverage like this
294298
```shell
295299

296-
> coverage run --source=src/python_ms_core -m unittest discover -v tests/unit_tests
300+
> python -m coverage run --source=src/python_ms_core -m unittest discover -v tests/unit_tests
297301
test_has_permission (test_auth.abstract.test_authorizer_abstraction.TestAuthorizerAbstract) ... ok
298302
test_get_search_params (test_auth.models.test_permission_request.TestPermissionRequest) ... ok
299303
test_has_permission_with_invalid_permissions (test_auth.provider.test_hosted_authorizer.TestHostedAuthorizer) ... ok

freeze_version.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import git
44
from datetime import date
5+
from src.python_ms_core import Core
56

67
project_path = os.path.dirname(os.path.abspath(__file__))
78
version_file_path = '{}/version.py'.format(project_path)
@@ -11,7 +12,7 @@
1112

1213
build_date = date.today().strftime('%Y-%m-%d')
1314

14-
version = '0.0.20'
15+
version = Core.__version__
1516

1617
with open(version_file_path, 'w+') as version_file:
1718
version_file.write("version = '{}'\n".format(version))

src/example.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,45 @@
11
# Testing code
2-
import sys
32
import os
43
import time
54
import uuid
65
import random
76
import datetime
87
from io import BytesIO, StringIO
8+
import threading
99

1010
from python_ms_core import Core
1111
from python_ms_core.core.queue.models.queue_message import QueueMessage
1212
from python_ms_core.core.auth.models.permission_request import PermissionRequest
1313

1414
core = Core()
15-
print('Hello')
16-
17-
topic = 'gtfs-pathways-upload'
18-
subscription = 'log'
19-
some_other_sub = 'usdufs'
15+
print(f'Core version: {Core.__version__}')
16+
topic = 'temp-request'
17+
subscription = 'temp'
18+
some_other_sub = 'temp'
2019

2120

2221
def publish_messages(topic_name):
2322
topic_object = core.get_topic(topic_name=topic_name)
2423
queue_message = QueueMessage.data_from({
2524
'message': str(uuid.uuid4().hex),
26-
'data': {'a': random.randint(0, 1000)}
25+
'data': {'a': random.randint(60, 120)}
2726
})
2827
topic_object.publish(data=queue_message)
2928
print('Message Published')
3029

3130

31+
def long_running_task(sleep_time):
32+
# Simulate a long-running task
33+
time.sleep(sleep_time)
34+
35+
3236
def subscribe(topic_name, subscription_name):
3337
def process(message):
34-
print(f'Message Received: {message}')
35-
# Spawn and thread process it -> 1 hr no issues
36-
# return
38+
print(f'Message Received: {message.data}')
39+
long_running_thread = threading.Thread(target=long_running_task, args=(message.data['a'],))
40+
long_running_thread.start()
41+
long_running_thread.join()
42+
print(f' > Message Completed: {message.data}')
3743

3844
topic_object = core.get_topic(topic_name=topic_name)
3945
try:
@@ -43,6 +49,8 @@ def process(message):
4349

4450

4551
subscribe(topic, subscription)
52+
# for x in range(10):
53+
# publish_messages(topic_name=topic)
4654

4755
# azure_client = core.get_storage_client()
4856

src/python_ms_core/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import logging
23
from .core.logger.logger import Logger
34
from .core.logger.local_logger import LocalLogger
@@ -8,6 +9,7 @@
89
from .core.auth.provider.hosted.hosted_authorizer import HostedAuthorizer
910
from .core.auth.provider.simulated.simulated_authorizer import SimulatedAuthorizer
1011
from .core.config.config import CoreConfig, LocalConfig, AuthConfig, UnknownConfig
12+
from .version import __version__
1113

1214
LOCAL_ENV = 'LOCAL'
1315
AZURE_ENV = 'AZURE'
@@ -30,6 +32,9 @@ def __init__(self, config=None):
3032
self.config = CoreConfig()
3133
self.__check_health()
3234

35+
def __version__(self):
36+
return
37+
3338
def get_logger(self):
3439
logger_config = self.config.logger()
3540
if logger_config.provider.upper() == LOCAL_ENV:
@@ -39,12 +44,12 @@ def get_logger(self):
3944
else:
4045
logging.error(f'Failed to initialize core.get_logger for provider: {logger_config.provider}')
4146

42-
def get_topic(self, topic_name: str):
47+
def get_topic(self, topic_name: str, max_concurrent_messages=os.cpu_count()):
4348
topic_config = self.config.topic()
4449
if topic_config.provider.upper() == LOCAL_ENV:
4550
return LocalTopic(config=topic_config, topic_name=topic_name)
4651
elif topic_config.provider.upper() == AZURE_ENV:
47-
return Topic(config=topic_config, topic_name=topic_name)
52+
return Topic(config=topic_config, topic_name=topic_name, max_concurrent_messages=max_concurrent_messages)
4853
else:
4954
logging.error(f'Failed to initialize core.get_topic for provider: {topic_config.provider}')
5055

@@ -114,3 +119,6 @@ def __check_health(self):
114119
print('\x1b[32m Logger configured \x1b[0m')
115120
print('\x1b[32m ------------------------- \x1b[0m')
116121
return True
122+
123+
124+
Core.__version__ = __version__

src/python_ms_core/core/topic/topic.py

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,96 @@
11
import json
22
import logging
3-
import threading
43
import time
54
from .config.topic_config import Config
6-
from .abstract.topic_abstract import TopicAbstract
75
from ..resource_errors import ExceptionHandler
6+
from concurrent.futures import ThreadPoolExecutor
7+
from .abstract.topic_abstract import TopicAbstract
88
from ..queue.models.queue_message import QueueMessage
99

10-
logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',datefmt='%Y-%m-%d %H:%M:%S')
10+
logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
1111
logger = logging.getLogger('Topic')
1212
logger.setLevel(logging.INFO)
1313

1414

1515
class Callback:
16-
def __init__(self, fn=None):
16+
def __init__(self, fn=None, max_concurrent_messages=1):
1717
self._function_to_call = fn
18+
self._max_concurrent_messages = max_concurrent_messages
19+
self._renewal_interval = 30 # seconds
20+
self.message_processing = 0
21+
self.executor = ThreadPoolExecutor(max_workers=max_concurrent_messages)
22+
23+
def _renew_message_lock(self, message, receiver):
24+
while True:
25+
try:
26+
time.sleep(self._renewal_interval)
27+
if not message._lock_expired:
28+
receiver.renew_message_lock(message)
29+
except Exception as e:
30+
break
1831

19-
# old method to fetch messages. Not used anymore
20-
def messages(self, provider, topic, subscription):
21-
with provider.client:
22-
topic_receiver = provider.client.get_subscription_receiver(topic, subscription_name=subscription)
23-
logger.info(f'Started receiver for {subscription}')
24-
with topic_receiver:
25-
for message in topic_receiver:
26-
try:
27-
queue_message = QueueMessage.data_from(str(message))
28-
self._function_to_call(queue_message)
29-
except Exception as e:
30-
print(f'Error: {e}, Invalid message received: {message}')
31-
finally:
32-
topic_receiver.complete_message(message)
33-
logger.info('Completed topic receiver')
34-
3532
# Sends data to the callback function
36-
def process_message(self, message:str):
37-
queue_message = QueueMessage.data_from(message)
33+
def process_message(self, message, receiver):
34+
queue_message = QueueMessage.data_from(str(message))
35+
secondary_executor = ThreadPoolExecutor(max_workers=1)
36+
if not message._lock_expired:
37+
secondary_executor.submit(self._renew_message_lock, message, receiver)
3838
self._function_to_call(queue_message)
39-
39+
4040
# Starts listening to the messages
4141
def start_listening(self, provider, topic, subscription):
42-
with provider.client: # service bus client
42+
with provider.client: # service bus client
4343
logger.info('Initiating receiver')
44-
topic_receiver = provider.client.get_subscription_receiver(topic, subscription_name=subscription) # servicebusclientsubscriptionreceiver
45-
logger.info('Done')
44+
topic_receiver = provider.client.get_subscription_receiver(
45+
topic_name=topic,
46+
subscription_name=subscription
47+
)
48+
logger.info('Receiver started')
4649
with topic_receiver:
4750
while True:
48-
try:
49-
for message in topic_receiver:
50-
try:
51-
self.process_message(message=str(message)) # sync call. [By default 1minute ] -> lock renewal for 300 seconds
52-
except Exception as e:
53-
print(f'Error : {e}, Invalid message received : {message}')
54-
finally:
55-
topic_receiver.complete_message(message)
56-
except Exception as et:
57-
print(f'Error in service bus connection : {et}')
58-
# Change mode from PEEK_LOCK to RECEIVE_AND_DELETE
59-
logger.info('Topic receiver invalidated')
51+
available_slots = self._max_concurrent_messages - self.message_processing
52+
if available_slots > 0:
53+
try:
54+
messages = topic_receiver.receive_messages(
55+
max_message_count=available_slots,
56+
max_wait_time=5
57+
)
58+
if not messages:
59+
continue
60+
for message in messages:
61+
self.message_processing += 1
62+
self.executor.submit(self._process_message_in_thread, message, topic_receiver)
63+
except Exception as et:
64+
logger.error(f'Error in service bus connection: {et}')
65+
else:
66+
time.sleep(10) # Short sleep to prevent tight loop if no slots available
67+
68+
logger.info('Receiver stopped')
69+
70+
def _process_message_in_thread(self, message, topic_receiver):
71+
try:
72+
self.process_message(message=message, receiver=topic_receiver)
73+
except Exception as e:
74+
logger.error(f'Error: {e}, Invalid message received: {message}')
75+
finally:
76+
try:
77+
topic_receiver.complete_message(message) # Mark the message as complete
78+
except Exception as err:
79+
logger.error(f'Error completing the message: {err}')
80+
self.message_processing -= 1
6081

6182

6283
class Topic(TopicAbstract):
63-
def __init__(self, config=None, topic_name=None):
84+
def __init__(self, config=None, topic_name=None, max_concurrent_messages=1):
6485
self.topic = topic_name
6586
self.provider = Config(config=config, topic_name=topic_name)
87+
self.max_concurrent_messages = max_concurrent_messages
6688

6789
@ExceptionHandler.decorated
6890
def subscribe(self, subscription=None, callback=None):
6991
if subscription is not None:
70-
cb = Callback(callback)
71-
thread = threading.Thread(target=cb.start_listening, args=(self.provider, self.topic, subscription))
72-
thread.start()
73-
time.sleep(5)
92+
cb = Callback(callback, max_concurrent_messages=self.max_concurrent_messages)
93+
cb.start_listening(self.provider, self.topic, subscription)
7494
else:
7595
logging.error(
7696
f'Unimplemented initialize for core {self.provider.provider}, Subscription name is required!')

src/python_ms_core/version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = '0.0.21'

0 commit comments

Comments
 (0)