This repository has been archived by the owner on Jan 11, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
app.py
259 lines (208 loc) · 8.06 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
from flask import Flask, render_template, request, jsonify
import requests
from microservice import Microservice
import re
import json
from datetime import datetime
app = Flask(__name__)
connected_apps = set()
cache = {}
# Route for "/" (frontend):
@app.route('/')
def index():
return render_template("index.html")
# Route for old (plain) frontend:
@app.route('/plain')
def index_plain():
return render_template("index_plain.html")
@app.route('/microservice', methods=['PUT'])
def add_microservice():
# Verify all required keys are present in JSON:
required_keys = ['port', 'ip', 'name', 'creator', 'tile']
for required_key in required_keys:
if required_key not in request.json:
return f'Required key {required_key} not present in payload JSON.', 400
if isinstance(request.json, str):
return f'Required key {required_key} has invalid type in payload JSON (should be str).', 400
# Add the microservice:
m = Microservice(
request.json['ip'] + ':' + request.json['port'],
request.json.get('dependencies', []),
request.json['name'],
request.json['creator'],
request.json['tile'],
)
print('connection received from: ' + m.ip)
connected_apps.add(m)
return 'Success', 200
@app.route('/microservice', methods=['DELETE'])
def remove_microservice():
print(f'delete request received from: {request.host}')
previous_len = len(connected_apps)
j = request.json
if 'ip' not in j or 'port' not in j:
return 'Invalid Input', 400
ip = j['ip'] + ':' + j['port']
m = Microservice(ip, [])
connected_apps.discard(m)
if len(connected_apps) == previous_len:
return 'Not Found', 404
return 'Success', 200
@app.route('/status', methods=["GET"])
def list_all_connected_services():
status = [{
'name': service.name,
'creator': service.creator,
'ip': service.ip,
'dependencies': [depend.ip for depend in service.dependencies] if service.dependencies is not None else []
} for service in connected_apps]
return jsonify(status), 200
# Route for "/MIX" (middleware):
@app.route('/MIX', methods=["POST"])
def POST_MIX():
global connected_apps
# process form data
location = request.form['location']
match = re.match(r"\s*([+-]?([0-9]*[.])?[0-9]+)[,\s]+([+-]?([0-9]*[.])?[0-9]+)\s*", location)
if match is None:
return 'Invalid input', 400
lat = float(match.group(1))
lon = float(match.group(3))
if abs(lat) > 90:
return 'Invalid latitude', 400
if abs(lon) > 180:
return 'Invalid longitude', 400
# aggregate JSON from all IMs
r = []
for im in connected_apps.copy():
# get the IM response:
try:
j = process_request(im, lat, lon)
except Exception as e:
print(im)
print(e)
connected_apps.discard(im)
continue
# add metadata about the IM service:
j.update({
'_metadata': {
'name': im.name,
'creator': im.creator,
'tile': im.tile,
'max-age': im.max_age
}
})
r.append(j)
return jsonify(r), 200
def get_dependencies(dependency_info: [dict]) -> [Microservice]:
"""
Convert a json list of dependencies into a list of Microservice objects,
by searching for the appropriate Microservices in connected_apps.
"""
dependency_list = []
for dependency in dependency_info:
# search for a matching IM
if 'name' in dependency and 'creator' in dependency:
for im in connected_apps:
if im.name == dependency['name'] and im.creator == dependency['creator']:
dependency_list.append(im)
break
else:
raise ValueError('Dependency not found')
elif 'ip' in dependency and 'port' in dependency:
for im in connected_apps:
if im.ip == dependency['ip'] + ':' + dependency['port']:
dependency_list.append(im)
break
else:
raise ValueError('Dependency not found')
else:
raise ValueError('Not enough dependency information')
return dependency_list
def process_request(service: Microservice, lat: float, lon: float, visited=tuple()) -> dict:
"""
Return the json output of a microservice, recursively calling this function for dependencies.
Any cache hits will be returned.
"""
# cache check
if cache_hit((lat, lon), service):
return cache[(lat, lon)][service.ip][0]
# first time dependency search
if service.dependencies is None:
try:
service.dependencies = get_dependencies(service.dependency_info)
except ValueError as e:
print(f'{e} for service {service.ip}')
return {}
# aggregate all dependency data (starting with lat, lon) and send as a request to our IM
dependency_results = {'latitude': lat, 'longitude': lon}
for dependency in service.dependencies:
if dependency in visited:
# check for circular dependencies
print(f'Circular dependency: asking for {dependency} on top of {list(visited)}')
return {}
else:
# concatenate results to dependency_results
dependency_results.update(process_request(dependency, lat, lon, visited + (dependency,)))
return make_im_request(service, dependency_results, lat, lon)
def make_im_request(service: Microservice, j: dict, lat: float, lon: float) -> dict:
"""
Return the json response after a GET request to a service (with a json input j).
"""
try:
r = requests.get(service.ip, json=j, timeout=2)
except requests.exceptions.RequestException:
print(f'service {service.name} at {service.ip} not connecting. removed from MIX!')
connected_apps.discard(service)
return {}
if 500 > r.status_code >= 400:
print(f'service {service.name} at {service.ip} returned error code {str(r.status_code)}')
return {}
elif r.status_code >= 500:
print(f'service {service.name} at {service.ip} returned error code {str(r.status_code)} - removed from MIX!')
connected_apps.discard(service)
return {}
add_entry_to_cache((lat, lon), service, r)
return r.json()
def parse_cache_header(header: str) -> float:
"""
Return the age from a Cache-Control header string.
raises ValueError if the Cache-Control is not recognized.
"""
m = re.match(r"max-age=(\d+)", header)
if m is None:
raise ValueError
return float(m.group(1))
def add_entry_to_cache(latlon: tuple, service: Microservice, response) -> None:
"""
Add a microservice response to cache.
"""
# set max_age for a service if it has not been set already
if service.max_age is None:
if 'Cache-Control' not in response.headers or 'max-age' not in response.headers['Cache-Control']:
service.max_age = 0 # no cache
else:
try:
service.max_age = parse_cache_header(response.headers['Cache-Control'])
except ValueError:
print(f'Bad Cache-Control for service {service.name} at {service.ip} - falling back to no-cache.')
service.max_age = 0
# enter the service response json into our cache
if latlon not in cache:
cache[latlon] = {service.ip: (response.json(), datetime.now())}
else:
cache[latlon][service.ip] = (response.json(), datetime.now())
def cache_hit(latlon: tuple, service: Microservice) -> bool:
"""
Return whether a cached response is available.
"""
if service.max_age == 0 or latlon not in cache or service.ip not in cache.get(latlon):
print('cache miss! entry not in cache')
return False
curr_time = datetime.now()
timediff = curr_time - cache[latlon][service.ip][1]
if timediff.total_seconds() < service.max_age:
print('cache hit!')
return True
print('cache miss! exceeded max_age')
return False