Skip to content

Commit 1054e3f

Browse files
author
Michael Bahr
committed
init
1 parent 749772d commit 1054e3f

10 files changed

Lines changed: 330 additions & 0 deletions

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Distribution / packaging
2+
.Python
3+
*.pyc
4+
env/
5+
build/
6+
develop-eggs/
7+
dist/
8+
downloads/
9+
eggs/
10+
.eggs/
11+
lib/
12+
lib64/
13+
parts/
14+
sdist/
15+
var/
16+
*.egg-info/
17+
.installed.cfg
18+
*.egg
19+
20+
# Serverless directories
21+
.serverless
22+
.idea
23+
venv

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# aws-scheduler-testing
2+
3+
This is a project to help with testing and performance measurements of the [aws-scheduler](https://github.com/bahrmichael/aws-scheduler).
4+
5+
## Setup
6+
7+
### Prerequisites
8+
You must have the following tools installed:
9+
- serverless framework 1.48.3 or later
10+
- node
11+
- npm
12+
- python3
13+
- pip
14+
15+
Run `setup/init_table.py <stage>` to setup the database. Replace `<stage>` with the stage of your application (e.g. `dev`).
16+
17+
Run `setup/init_output_topic.py <stage>`. Replace `<stage>` with the stage of your application (e.g. `dev`).
18+
19+
### Deploy
20+
1. Navigate into the project folder
21+
2. With a tooling of your choice create and activate a venv
22+
3. `pip install -r requirements.txt`
23+
4. `npm i serverless-python-requirements`
24+
5. `sls deploy`
25+
6. Optional: `pip install matplotlib` if you want to plot the delays later
26+
27+
Wait for the deployment to finish. Test the service by first attaching a function to the output topic and then send a few events to the input topic.
28+
29+
## Run it
30+
31+
Once you created the database and topic and deployed the stack, run the producer with `python producer.py <your-output-topic> [amount] [input-topic]`. Replace `<your-output-topic>` with the arn of the topic you created during the setup. You may specify a second argument to set the number of events, the default is 100. You may specify a third argument to use a different input topic. You may also trigger the function from the aws lambda console.
32+
33+
Run `sls logs -f consumer -t` to tail the logs. You may receive an error that the log stream does not exist until the first event has arrived.
34+
35+
After 5 minutes all events should have arrived at the output topic and the measured delays are stored in the database. You can now run `python evaluate_measurement.py` to plot the delays.

consumer.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import time
2+
from datetime import datetime
3+
from uuid import uuid4
4+
5+
from pynamodb.exceptions import PutError
6+
7+
from model import MeasuredDuration
8+
9+
10+
def handle(event, context):
11+
delays = []
12+
for record in event['Records']:
13+
print(record)
14+
payload = record['Sns']['Message']
15+
execution_time = datetime.fromisoformat(payload)
16+
delta = int((datetime.utcnow() - execution_time).total_seconds() * 1000)
17+
print(f'delay: {delta}')
18+
delays.append(delta)
19+
20+
with MeasuredDuration.batch_write() as batch:
21+
retries = []
22+
for delay in delays:
23+
item = MeasuredDuration()
24+
item.id = str(uuid4())
25+
item.delay = delay
26+
try:
27+
batch.save(item)
28+
except PutError as e:
29+
print(e)
30+
time.sleep(.200)
31+
retries.append(delay)
32+
33+
while len(retries) > 0:
34+
delay = retries.pop(0)
35+
item = MeasuredDuration()
36+
item.id = str(uuid4())
37+
item.delay = delay
38+
try:
39+
batch.save(item)
40+
except PutError as e:
41+
print(e)
42+
time.sleep(.200)
43+
retries.append(delay)
44+
45+
print('Processed %d records' % len(event['Records']))

evaluate_measurements.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import matplotlib.pyplot as plt
2+
from model import MeasuredDuration
3+
4+
5+
def download():
6+
delays = []
7+
counter = 0
8+
for item in MeasuredDuration.scan():
9+
delays.append(item.delay)
10+
counter += 1
11+
if counter % 1000 == 0:
12+
print(f'{counter}')
13+
return delays
14+
15+
16+
if __name__ == '__main__':
17+
data = download()
18+
19+
print(f'total: {len(data)}')
20+
plt.hist(data, bins=100)
21+
22+
plt.title(f'Regular Scaled ({len(data)} events)')
23+
plt.xlabel("Delay after scheduled time (milliseconds)")
24+
plt.ylabel("Number of events")
25+
plt.savefig('regular.png')
26+
27+
plt.yscale('log')
28+
plt.xscale('log')
29+
plt.title(f'Log Scaled ({len(data)} events)')
30+
plt.xlabel("Delay after scheduled time (milliseconds)")
31+
plt.ylabel("Number of events")
32+
plt.savefig('log.png')
33+
34+
print('Diagrams saved to regular.png and log.png')

init_output_topic.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import sys
2+
3+
import boto3
4+
5+
client = boto3.client('sns')
6+
7+
8+
if __name__ == '__main__':
9+
10+
if len(sys.argv) < 2:
11+
print('Missing argument for stage.')
12+
exit()
13+
stage = sys.argv[1]
14+
15+
name = f'scheduler-output-{stage}'
16+
17+
print(f'Creating topic {name}')
18+
create_response = client.create_topic(
19+
Name=name,
20+
)
21+
22+
arn = create_response['TopicArn']
23+
print(f'Created topic {name} with arn {arn}')
24+
25+
if len(sys.argv) == 3:
26+
role = sys.argv[2]
27+
else:
28+
role = 'arn:aws:sts::256608350746:assumed-role/aws-scheduler-prod-us-east-1-lambdaRole/aws-scheduler-prod-emitter'
29+
print(f'Granting publish rights to {name} topic for role {role}')
30+
31+
permission_response = client.add_permission(
32+
TopicArn=arn,
33+
Label=f'{name}-publish-access',
34+
AWSAccountId=[str(role)],
35+
ActionName=['Publish']
36+
)
37+
38+
print('Done')

init_table.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import boto3
2+
3+
client = boto3.client('dynamodb')
4+
5+
6+
def create():
7+
name = 'aws-scheduler-testing'
8+
response = client.list_tables()
9+
if name in response['TableNames']:
10+
print('Table %s already exists. Please delete it first.' % name)
11+
return
12+
client.create_table(
13+
TableName=name,
14+
AttributeDefinitions=[
15+
{
16+
'AttributeName': 'id',
17+
'AttributeType': 'S'
18+
}
19+
],
20+
KeySchema=[
21+
{
22+
'AttributeName': 'id',
23+
'KeyType': 'HASH'
24+
}
25+
],
26+
BillingMode='PAY_PER_REQUEST',
27+
)
28+
print('%s created' % name)
29+
30+
31+
if __name__ == '__main__':
32+
create()

model.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pynamodb.attributes import (
2+
UnicodeAttribute,
3+
NumberAttribute)
4+
from pynamodb.models import Model
5+
6+
7+
class MeasuredDuration(Model):
8+
9+
class Meta:
10+
table_name = 'aws-scheduler-testing'
11+
12+
id = UnicodeAttribute(hash_key=True)
13+
delay = NumberAttribute()

producer.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import json
2+
import os
3+
import sys
4+
from datetime import datetime, timedelta
5+
from random import randrange
6+
7+
import boto3
8+
9+
client = boto3.client('sns')
10+
11+
12+
def handle(event, context):
13+
input_topic = os.environ.get('INPUT_TOPIC_ARN')
14+
target = os.environ.get('OUTPUT_TOPIC_ARN')
15+
run(100, input_topic, target)
16+
17+
18+
def run(target, input_topic, amount):
19+
start = datetime.utcnow()
20+
for i in range(0, amount):
21+
delay = randrange(60 * 5)
22+
scheduled_time = (datetime.utcnow() + timedelta(seconds=delay)).isoformat()
23+
payload = {
24+
'payload': scheduled_time,
25+
'date': scheduled_time,
26+
'target': target
27+
}
28+
29+
client.publish(
30+
TopicArn=input_topic,
31+
Message=json.dumps(payload)
32+
)
33+
34+
if i % 10 == 0:
35+
total_millis = (datetime.utcnow() - start).total_seconds() * 1000
36+
print(f'total: {i}, {int(total_millis / i)} per event')
37+
38+
39+
if __name__ == '__main__':
40+
arguments = sys.argv[1:]
41+
if len(arguments) >= 1:
42+
target = arguments[0]
43+
else:
44+
print('You must specify the output topic as the first parameter')
45+
exit()
46+
47+
if len(arguments) >= 3:
48+
input_topic = arguments[2]
49+
else:
50+
input_topic = 'arn:aws:sns:us-east-1:256608350746:scheduler-input-prod'
51+
52+
if len(arguments) >= 2:
53+
amount = int(arguments[1])
54+
else:
55+
amount = 100
56+
57+
run(target, input_topic, amount)

requirements.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
boto3==1.9.197
2+
botocore==1.12.197
3+
docutils==0.14
4+
jmespath==0.9.4
5+
pynamodb==3.4.1
6+
python-dateutil==2.8.0
7+
s3transfer==0.2.1
8+
six==1.12.0
9+
urllib3==1.25.3

serverless.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
service: aws-scheduler-testing
2+
3+
provider:
4+
name: aws
5+
runtime: python3.7
6+
stage: ${opt:stage, 'dev'}
7+
region: ${opt:region, 'us-east-1'}
8+
iamRoleStatements:
9+
- Effect: Allow
10+
Action:
11+
- dynamodb:PutItem
12+
Resource: { "Fn::Join": [":", ["arn:aws:dynamodb:${self:provider.region}", { "Ref": "AWS::AccountId" }, "table/aws-scheduler-testing" ] ] }
13+
14+
custom:
15+
output_topic:
16+
name:
17+
arn: { "Fn::Join": [":", ["arn:aws:sns:${self:provider.region}", { "Ref": "AWS::AccountId" }, "table/scheduler-output-${self:provider.stage}" ] ] }
18+
19+
functions:
20+
consumer:
21+
handler: consumer.handle
22+
events:
23+
- sns:
24+
arn:
25+
Fn::Join:
26+
- ':'
27+
- - 'arn:aws:sns'
28+
- Ref: 'AWS::Region'
29+
- Ref: 'AWS::AccountId'
30+
- "${self:custom.inbound.name}"
31+
topicName: "${self:custom.inbound.name}"
32+
producer:
33+
handler: producer.handle
34+
environment:
35+
INPUT_TOPIC_ARN: 'arn:aws:sns:us-east-1:256608350746:scheduler-input-prod'
36+
OUTPUT_TOPIC_ARN: { "Fn::Join": [":", ["arn:aws:sns:${self:provider.region}", { "Ref": "AWS::AccountId" }, "table/scheduler-output-${self:provider.stage}" ] ] }
37+
38+
plugins:
39+
- serverless-python-requirements
40+
41+
package:
42+
exclude:
43+
- venv/**
44+
- node_modules/**

0 commit comments

Comments
 (0)