-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#64] Queue and Storage triggers for AWS, GCP and Azure #201
base: master
Are you sure you want to change the base?
Changes from all commits
4bd6f44
728288e
9c3a016
63ab522
94d3449
0f7454a
8f96eda
107b53f
ba67b4a
94a675a
bb0ade5
97d6345
be4e4f9
debbda0
3e52f3a
4bd7a20
a466aab
0b310c1
f8f3162
dac2840
7de164e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,14 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import boto3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class queue: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def __init__(self, queue_name: str, account_id: str, region: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.client = boto3.client('sqs', region_name=region) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.queue_url = f"https://sqs.{region}.amazonaws.com/{account_id}/{queue_name}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def send_message(self, message: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.client.send_message( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
QueueUrl=self.queue_url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
MessageBody=message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+3
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename the class and add error handling. Consider renaming the class from Suggested class name change and error handling: -import boto3
+import boto3
+from botocore.exceptions import ClientError
-class queue:
+class AWSQueue:
client = None
def __init__(self, queue_name: str, account_id: str, region: str):
self.client = boto3.client('sqs', region_name=region)
self.queue_url = f"https://sqs.{region}.amazonaws.com/{account_id}/{queue_name}"
def send_message(self, message: str):
try:
self.client.send_message(
QueueUrl=self.queue_url,
MessageBody=message,
)
except ClientError as e:
print(f"An error occurred: {e}") Committable suggestion
Suggested change
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,18 +1,70 @@ | ||||
|
||||
import datetime, io, json, os, uuid | ||||
import base64 | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused import. The - import base64 Committable suggestion
Suggested change
ToolsRuff
|
||||
import datetime, io, json, logging, os, uuid | ||||
|
||||
from azure.identity import ManagedIdentityCredential | ||||
from azure.storage.queue import QueueClient | ||||
|
||||
import azure.functions as func | ||||
|
||||
|
||||
# TODO: usual trigger | ||||
# implement support for blob and others | ||||
def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: | ||||
def handler_http(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: | ||||
income_timestamp = datetime.datetime.now().timestamp() | ||||
|
||||
req_json = req.get_json() | ||||
if 'connection_string' in req_json: | ||||
os.environ['STORAGE_CONNECTION_STRING'] = req_json['connection_string'] | ||||
|
||||
req_json['request-id'] = context.invocation_id | ||||
req_json['income-timestamp'] = income_timestamp | ||||
|
||||
return func.HttpResponse(measure(req_json), mimetype="application/json") | ||||
|
||||
def handler_queue(msg: func.QueueMessage, context: func.Context): | ||||
income_timestamp = datetime.datetime.now().timestamp() | ||||
|
||||
logging.info('Python queue trigger function processed a queue item.') | ||||
payload = msg.get_json() | ||||
|
||||
payload['request-id'] = context.invocation_id | ||||
payload['income-timestamp'] = income_timestamp | ||||
|
||||
stats = measure(payload) | ||||
|
||||
queue_name = f"{os.getenv('WEBSITE_SITE_NAME')}-result" | ||||
storage_account = os.getenv('STORAGE_ACCOUNT') | ||||
logging.info(queue_name) | ||||
logging.info(storage_account) | ||||
|
||||
from . import queue | ||||
queue_client = queue.queue(queue_name, storage_account) | ||||
queue_client.send_message(stats) | ||||
|
||||
def handler_storage(blob: func.InputStream, context: func.Context): | ||||
income_timestamp = datetime.datetime.now().timestamp() | ||||
|
||||
logging.info('Python Blob trigger function processed %s', blob.name) | ||||
payload = json.loads(blob.readline().decode('utf-8')) | ||||
|
||||
payload['request-id'] = context.invocation_id | ||||
payload['income-timestamp'] = income_timestamp | ||||
|
||||
stats = measure(payload) | ||||
|
||||
queue_name = f"{os.getenv('WEBSITE_SITE_NAME')}-result" | ||||
storage_account = os.getenv('STORAGE_ACCOUNT') | ||||
logging.info(queue_name) | ||||
logging.info(storage_account) | ||||
|
||||
from . import queue | ||||
queue_client = queue.queue(queue_name, storage_account) | ||||
queue_client.send_message(stats) | ||||
|
||||
# Contains generic logic for gathering measurements for the function at hand, | ||||
# given a request JSON. Used by all handlers, regardless of the trigger. | ||||
def measure(req_json) -> str: | ||||
req_id = req_json['request-id'] | ||||
|
||||
begin = datetime.datetime.now() | ||||
# We are deployed in the same directory | ||||
from . import function | ||||
|
@@ -30,7 +82,6 @@ def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: | |||
from . import storage | ||||
storage_inst = storage.storage.get_instance() | ||||
b = req_json.get('logs').get('bucket') | ||||
req_id = context.invocation_id | ||||
storage_inst.upload_stream(b, '{}.json'.format(req_id), | ||||
io.BytesIO(json.dumps(log_data).encode('utf-8'))) | ||||
results_end = datetime.datetime.now() | ||||
|
@@ -58,8 +109,7 @@ def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: | |||
cold_marker = True | ||||
is_cold_worker = True | ||||
|
||||
return func.HttpResponse( | ||||
json.dumps({ | ||||
return json.dumps({ | ||||
'begin': begin.strftime('%s.%f'), | ||||
'end': end.strftime('%s.%f'), | ||||
'results_time': results_time, | ||||
|
@@ -68,8 +118,5 @@ def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: | |||
'is_cold_worker': is_cold_worker, | ||||
'container_id': container_id, | ||||
'environ_container_id': os.environ['CONTAINER_NAME'], | ||||
'request_id': context.invocation_id | ||||
}), | ||||
mimetype="application/json" | ||||
) | ||||
|
||||
'request_id': req_id | ||||
}) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,15 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from azure.identity import ManagedIdentityCredential | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from azure.storage.queue import QueueClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class queue: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def __init__(self, queue_name: str, storage_account: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
account_url = f"https://{storage_account}.queue.core.windows.net" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
managed_credential = ManagedIdentityCredential() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.client = QueueClient(account_url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
queue_name=queue_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
credential=managed_credential) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def send_message(self, message: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.client.send_message(message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling in The Consider wrapping the call in a try-except block: def send_message(self, message: str):
+ try:
self.client.send_message(message)
+ except Exception as e:
+ logging.error(f"Failed to send message: {e}")
+ raise Committable suggestion
Suggested change
Comment on lines
+4
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactor class name and improve thread safety. The class name Here's a suggested refactor: -class queue:
+class Queue:
- client = None
+
def __init__(self, queue_name: str, storage_account: str):
+ self.client = None
account_url = f"https://{storage_account}.queue.core.windows.net"
managed_credential = ManagedIdentityCredential()
- self.client = QueueClient(account_url,
+ self.client = QueueClient(account_url,
queue_name=queue_name,
credential=managed_credential) Committable suggestion
Suggested change
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,16 +1,75 @@ | ||||
import datetime, io, json, os, uuid, sys | ||||
import base64, datetime, io, json, os, uuid, sys | ||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '.python_packages/lib/site-packages')) | ||||
from google.cloud import storage as gcp_storage | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused import. The - from google.cloud import storage as gcp_storage Committable suggestion
Suggested change
ToolsRuff
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '.python_packages/lib/site-packages')) | ||||
|
||||
def handler(req): | ||||
def handler_http(req): | ||||
income_timestamp = datetime.datetime.now().timestamp() | ||||
req_id = req.headers.get('Function-Execution-Id') | ||||
|
||||
|
||||
req_json = req.get_json() | ||||
req_json['request-id'] = req_id | ||||
req_json['income-timestamp'] = income_timestamp | ||||
|
||||
return measure(req_json), 200, {'ContentType': 'application/json'} | ||||
|
||||
def handler_queue(data, context): | ||||
income_timestamp = datetime.datetime.now().timestamp() | ||||
|
||||
serialized_payload = data.get('data') | ||||
payload = json.loads(base64.b64decode(serialized_payload).decode("utf-8")) | ||||
|
||||
payload['request-id'] = context.event_id | ||||
payload['income-timestamp'] = income_timestamp | ||||
|
||||
stats = measure(payload) | ||||
|
||||
# Retrieve the project id and construct the result queue name. | ||||
project_id = context.resource.split("/")[1] | ||||
topic_name = f"{context.resource.split('/')[3]}-result" | ||||
|
||||
from function import queue | ||||
queue_client = queue.queue(topic_name, project_id) | ||||
queue_client.send_message(stats) | ||||
|
||||
def handler_storage(data, context): | ||||
income_timestamp = datetime.datetime.now().timestamp() | ||||
|
||||
bucket_name = data.get('bucket') | ||||
name = data.get('name') | ||||
filepath = '/tmp/bucket_contents' | ||||
|
||||
from function import storage | ||||
storage_inst = storage.storage.get_instance() | ||||
storage_inst.download(bucket_name, name, filepath) | ||||
|
||||
payload = {} | ||||
|
||||
with open(filepath, 'r') as fp: | ||||
payload = json.load(fp) | ||||
|
||||
payload['request-id'] = context.event_id | ||||
payload['income-timestamp'] = income_timestamp | ||||
|
||||
stats = measure(payload) | ||||
|
||||
# Retrieve the project id and construct the result queue name. | ||||
from google.auth import default | ||||
# Used to be an env var, now we need an additional request to the metadata | ||||
# server to retrieve it. | ||||
_, project_id = default() | ||||
topic_name = f"{context.resource['name'].split('/')[3]}-result" | ||||
|
||||
from function import queue | ||||
queue_client = queue.queue(topic_name, project_id) | ||||
queue_client.send_message(stats) | ||||
|
||||
Comment on lines
+36
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Function The function correctly handles storage events, including retrieving and processing data from a Google Cloud Storage bucket. Consider adding error handling for potential issues during data retrieval and processing to enhance robustness. Consider adding error handling around the storage interactions: try:
storage_inst.download(bucket_name, name, filepath)
except Exception as e:
# Handle exceptions appropriately
logging.error(f"Error downloading from bucket: {e}")
return {'error': str(e)}, 500 |
||||
# Contains generic logic for gathering measurements for the function at hand, | ||||
# given a request JSON. Used by all handlers, regardless of the trigger. | ||||
def measure(req_json) -> str: | ||||
req_id = req_json['request-id'] | ||||
|
||||
begin = datetime.datetime.now() | ||||
# We are deployed in the same directorygit status | ||||
from function import function | ||||
|
@@ -61,4 +120,4 @@ def handler(req): | |||
'request_id': req_id, | ||||
'cold_start_var': cold_start_var, | ||||
'container_id': container_id, | ||||
}), 200, {'ContentType': 'application/json'} | ||||
}) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,14 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from google.cloud import pubsub_v1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class queue: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def __init__(self, topic_name: str, project_id: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.client = pubsub_v1.PublisherClient() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.topic_name = 'projects/{project_id}/topics/{topic}'.format( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
project_id=project_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
topic=topic_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def send_message(self, message: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.client.publish(self.topic_name, message.encode("utf-8")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+3
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename the class and add error handling. Consider renaming the class from Suggested class name change and error handling: -from google.cloud import pubsub_v1
+from google.cloud import pubsub_v1
+from google.api_core.exceptions import GoogleAPICallError, RetryError
-class queue:
+class GCPQueue:
client = None
def __init__(self, topic_name: str, project_id: str):
self.client = pubsub_v1.PublisherClient()
self.topic_name = 'projects/{project_id}/topics/{topic}'.format(
project_id=project_id,
topic=topic_name,
)
def send_message(self, message: str):
try:
self.client.publish(self.topic_name, message.encode("utf-8"))
except (GoogleAPICallError, RetryError) as e:
print(f"An error occurred: {e}") Committable suggestion
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -267,7 +267,8 @@ Check other platforms to see how configuration is defined, for example, for AWS: | |
"deployment": { | ||
"files": [ | ||
"handler.py", | ||
"storage.py" | ||
"storage.py", | ||
"queue.py" | ||
Comment on lines
+270
to
+271
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review the addition of The addition of Ensure that the documentation explicitly mentions how |
||
], | ||
"packages": [] | ||
} | ||
|
@@ -303,6 +304,7 @@ Implement this step in the following function: | |
language_version: str, | ||
benchmark: str, | ||
is_cached: bool, | ||
trigger: Optional[Trigger.TriggerType], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review the addition of the The addition of the Verify that all references to this function in the documentation and codebase have been updated to reflect this new parameter. Additionally, provide examples or scenarios where this parameter would be used, enhancing the understanding and applicability of this change. |
||
) -> Tuple[str, int] | ||
``` | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One question here: are we certain we always receive a single event? do we need to add loop here?