Commit 591253988601fcc7b3dcaff9bc6c70803f6beec3

Authored by Antoine Goutenoir
1 parent d5474086
Exists in master

Improve resilience.

Showing 1 changed file with 275 additions and 268 deletions   Show diff stats
flaskr/controllers/main_controller.py
@@ -240,318 +240,325 @@ def compute(): # process the queue of estimation requests @@ -240,318 +240,325 @@ def compute(): # process the queue of estimation requests
240 _estimation.warnings = _warning_message 240 _estimation.warnings = _warning_message
241 db.session.commit() 241 db.session.commit()
242 242
243 - response = "" 243 + try:
  244 + response = ""
244 245
245 - count_working = Estimation.query \  
246 - .filter_by(status=StatusEnum.working) \  
247 - .count() 246 + count_working = Estimation.query \
  247 + .filter_by(status=StatusEnum.working) \
  248 + .count()
248 249
249 - if 0 < count_working:  
250 - return _respond("Already working on estimation.") 250 + if 0 < count_working:
  251 + return _respond("Already working on estimation.")
251 252
252 - try:  
253 - estimation = Estimation.query \  
254 - .filter_by(status=StatusEnum.pending) \  
255 - .order_by(Estimation.id.asc()) \  
256 - .first()  
257 - except sqlalchemy.orm.exc.NoResultFound:  
258 - return _respond("No estimation in the queue.")  
259 - except Exception as e:  
260 - return _respond("Database error: %s" % (e,)) 253 + try:
  254 + estimation = Estimation.query \
  255 + .filter_by(status=StatusEnum.pending) \
  256 + .order_by(Estimation.id.asc()) \
  257 + .first()
  258 + except sqlalchemy.orm.exc.NoResultFound:
  259 + return _respond("No estimation in the queue.")
  260 + except Exception as e:
  261 + return _respond("Database error: %s" % (e,))
  262 +
  263 + if not estimation:
  264 + return _respond("No estimation in the queue.")
  265 +
  266 + estimation.status = StatusEnum.working
  267 + db.session.commit()
261 268
262 - if not estimation:  
263 - return _respond("No estimation in the queue.") 269 + response += u"Processing estimation `%s`...\n" % (
  270 + estimation.public_id
  271 + )
264 272
265 - estimation.status = StatusEnum.working  
266 - db.session.commit() 273 + failed_addresses = []
  274 + geocoder = CachedGeocoder()
267 275
268 - response += u"Processing estimation `%s`...\n" % (  
269 - estimation.public_id  
270 - ) 276 + # GEOCODE ORIGINS #########################################################
271 277
272 - failed_addresses = []  
273 - geocoder = CachedGeocoder() 278 + origins_addresses = estimation.origin_addresses.strip().split("\n")
  279 + origins = []
274 280
275 - # GEOCODE ORIGINS ######################################################### 281 + for i in range(len(origins_addresses)):
276 282
277 - origins_addresses = estimation.origin_addresses.strip().split("\n")  
278 - origins = [] 283 + origin_address = origins_addresses[i].strip()
  284 + if origin_address in failed_addresses:
  285 + continue
279 286
280 - for i in range(len(origins_addresses)): 287 + try:
  288 + origin = geocoder.geocode(origin_address.encode('utf-8'))
  289 + except geopy.exc.GeopyError as e:
  290 + response += u"Failed to geocode origin `%s`.\n%s\n" % (
  291 + origin_address, e,
  292 + )
  293 + _handle_warning(estimation, response)
  294 + failed_addresses.append(origin_address)
  295 + continue
281 296
282 - origin_address = origins_addresses[i].strip()  
283 - if origin_address in failed_addresses:  
284 - continue 297 + if origin is None:
  298 + response += u"Failed to geocode origin `%s`.\n" % (
  299 + origin_address,
  300 + )
  301 + _handle_warning(estimation, response)
  302 + failed_addresses.append(origin_address)
  303 + continue
285 304
286 - try:  
287 - origin = geocoder.geocode(origin_address.encode('utf-8'))  
288 - except geopy.exc.GeopyError as e:  
289 - response += u"Failed to geocode origin `%s`.\n%s\n" % (  
290 - origin_address, e,  
291 - )  
292 - _handle_warning(estimation, response)  
293 - failed_addresses.append(origin_address)  
294 - continue 305 + origins.append(origin)
295 306
296 - if origin is None:  
297 - response += u"Failed to geocode origin `%s`.\n" % (  
298 - origin_address, 307 + response += u"Origin: %s == %s (%f, %f)\n" % (
  308 + origin_address, origin.address,
  309 + origin.latitude, origin.longitude,
299 ) 310 )
300 - _handle_warning(estimation, response)  
301 - failed_addresses.append(origin_address)  
302 - continue  
303 311
304 - origins.append(origin) 312 + # GEOCODE DESTINATIONS ####################################################
305 313
306 - response += u"Origin: %s == %s (%f, %f)\n" % (  
307 - origin_address, origin.address,  
308 - origin.latitude, origin.longitude,  
309 - ) 314 + destinations_addresses = estimation.destination_addresses.strip().split("\n")
  315 + destinations = []
310 316
311 - # GEOCODE DESTINATIONS #################################################### 317 + for i in range(len(destinations_addresses)):
312 318
313 - destinations_addresses = estimation.destination_addresses.strip().split("\n")  
314 - destinations = [] 319 + destination_address = destinations_addresses[i].strip()
  320 + if destination_address in failed_addresses:
  321 + continue
315 322
316 - for i in range(len(destinations_addresses)): 323 + try:
  324 + destination = geocoder.geocode(destination_address.encode('utf-8'))
  325 + except geopy.exc.GeopyError as e:
  326 + response += u"Failed to geocode destination `%s`.\n%s\n" % (
  327 + destination_address, e,
  328 + )
  329 + _handle_warning(estimation, response)
  330 + failed_addresses.append(destination_address)
  331 + continue
317 332
318 - destination_address = destinations_addresses[i].strip()  
319 - if destination_address in failed_addresses:  
320 - continue 333 + if destination is None:
  334 + response += u"Failed to geocode destination `%s`.\n" % (
  335 + destination_address,
  336 + )
  337 + _handle_warning(estimation, response)
  338 + failed_addresses.append(destination_address)
  339 + continue
321 340
322 - try:  
323 - destination = geocoder.geocode(destination_address.encode('utf-8'))  
324 - except geopy.exc.GeopyError as e:  
325 - response += u"Failed to geocode destination `%s`.\n%s\n" % (  
326 - destination_address, e,  
327 - )  
328 - _handle_warning(estimation, response)  
329 - failed_addresses.append(destination_address)  
330 - continue 341 + # print(repr(destination.raw))
331 342
332 - if destination is None:  
333 - response += u"Failed to geocode destination `%s`.\n" % (  
334 - destination_address, 343 + destinations.append(destination)
  344 +
  345 + response += u"Destination: %s == %s (%f, %f)\n" % (
  346 + destination_address, destination.address,
  347 + destination.latitude, destination.longitude,
335 ) 348 )
336 - _handle_warning(estimation, response)  
337 - failed_addresses.append(destination_address)  
338 - continue  
339 349
340 - # print(repr(destination.raw)) 350 + # GTFO IF NO ORIGINS OR NO DESTINATIONS ###################################
341 351
342 - destinations.append(destination) 352 + if 0 == len(origins):
  353 + response += u"Failed to geocode all the origin(s).\n"
  354 + _handle_failure(estimation, response)
  355 + return _respond(response)
  356 + if 0 == len(destinations):
  357 + response += u"Failed to geocode all the destination(s).\n"
  358 + _handle_failure(estimation, response)
  359 + return _respond(response)
343 360
344 - response += u"Destination: %s == %s (%f, %f)\n" % (  
345 - destination_address, destination.address,  
346 - destination.latitude, destination.longitude,  
347 - ) 361 + # GRAB AND CONFIGURE THE EMISSION MODELS ##################################
348 362
349 - # GTFO IF NO ORIGINS OR NO DESTINATIONS ################################### 363 + emission_models = estimation.get_models()
  364 + # print(emission_models)
350 365
351 - if 0 == len(origins):  
352 - response += u"Failed to geocode all the origin(s).\n"  
353 - _handle_failure(estimation, response)  
354 - return _respond(response)  
355 - if 0 == len(destinations):  
356 - response += u"Failed to geocode all the destination(s).\n"  
357 - _handle_failure(estimation, response)  
358 - return _respond(response)  
359 -  
360 - # GRAB AND CONFIGURE THE EMISSION MODELS ##################################  
361 -  
362 - emission_models = estimation.get_models()  
363 - # print(emission_models)  
364 -  
365 - extra_config = {  
366 - 'use_train_below_distance': estimation.use_train_below_km,  
367 - # 'use_train_below_distance': 300,  
368 - }  
369 -  
370 - # PREPARE RESULT DICTIONARY THAT WILL BE STORED ###########################  
371 -  
372 - results = {}  
373 -  
374 - # UTILITY PRIVATE FUNCTION(S) #############################################  
375 -  
376 - def get_city_key(_location):  
377 - # Will this hack hold? Suspense...  
378 - return _location.address.split(',')[0]  
379 -  
380 - # _city_key = _location.address  
381 - # # if 'address100' in _location.raw['address']:  
382 - # # _city_key = _location.raw['address']['address100']  
383 - # if 'city' in _location.raw['address']:  
384 - # _city_key = _location.raw['address']['city']  
385 - # elif 'state' in _location.raw['address']:  
386 - # _city_key = _location.raw['address']['state']  
387 - # return _city_key  
388 -  
389 - def compute_one_to_many(  
390 - _origin,  
391 - _destinations,  
392 - _extra_config=None  
393 - ):  
394 - _results = {}  
395 - footprints = {}  
396 -  
397 - destinations_by_city_key = {}  
398 -  
399 - cities_sum_foot = {}  
400 - cities_sum_dist = {}  
401 - cities_dict_first_model = None  
402 - for model in emission_models:  
403 - cities_dict = {}  
404 - for _destination in _destinations:  
405 - footprint = model.compute_travel_footprint(  
406 - origin_latitude=_origin.latitude,  
407 - origin_longitude=_origin.longitude,  
408 - destination_latitude=_destination.latitude,  
409 - destination_longitude=_destination.longitude,  
410 - extra_config=_extra_config,  
411 - ) 366 + extra_config = {
  367 + 'use_train_below_distance': estimation.use_train_below_km,
  368 + # 'use_train_below_distance': 300,
  369 + }
412 370
413 - _key = get_city_key(_destination)  
414 -  
415 - destinations_by_city_key[_key] = _destination  
416 -  
417 - if _key not in cities_dict:  
418 - cities_dict[_key] = {  
419 - 'city': _key,  
420 - 'address': _destination.address,  
421 - 'footprint': 0.0,  
422 - 'distance': 0.0,  
423 - 'train_trips': 0,  
424 - 'plane_trips': 0,  
425 - }  
426 - cities_dict[_key]['footprint'] += footprint['co2eq_kg']  
427 - cities_dict[_key]['distance'] += footprint['distance_km']  
428 - cities_dict[_key]['train_trips'] += footprint['train_trips']  
429 - cities_dict[_key]['plane_trips'] += footprint['plane_trips']  
430 - if _key not in cities_sum_foot:  
431 - cities_sum_foot[_key] = 0.0  
432 - cities_sum_foot[_key] += footprint['co2eq_kg']  
433 - if _key not in cities_sum_dist:  
434 - cities_sum_dist[_key] = 0.0  
435 - cities_sum_dist[_key] += footprint['distance_km']  
436 -  
437 - cities = sorted(cities_dict.values(), key=lambda c: c['footprint'])  
438 -  
439 - footprints[model.slug] = {  
440 - 'cities': cities,  
441 - } 371 + # PREPARE RESULT DICTIONARY THAT WILL BE STORED ###########################
  372 +
  373 + results = {}
  374 +
  375 + # UTILITY PRIVATE FUNCTION(S) #############################################
  376 +
  377 + def get_city_key(_location):
  378 + # Will this hack hold? Suspense...
  379 + return _location.address.split(',')[0]
  380 +
  381 + # _city_key = _location.address
  382 + # # if 'address100' in _location.raw['address']:
  383 + # # _city_key = _location.raw['address']['address100']
  384 + # if 'city' in _location.raw['address']:
  385 + # _city_key = _location.raw['address']['city']
  386 + # elif 'state' in _location.raw['address']:
  387 + # _city_key = _location.raw['address']['state']
  388 + # return _city_key
  389 +
  390 + def compute_one_to_many(
  391 + _origin,
  392 + _destinations,
  393 + _extra_config=None
  394 + ):
  395 + _results = {}
  396 + footprints = {}
  397 +
  398 + destinations_by_city_key = {}
  399 +
  400 + cities_sum_foot = {}
  401 + cities_sum_dist = {}
  402 + cities_dict_first_model = None
  403 + for model in emission_models:
  404 + cities_dict = {}
  405 + for _destination in _destinations:
  406 + footprint = model.compute_travel_footprint(
  407 + origin_latitude=_origin.latitude,
  408 + origin_longitude=_origin.longitude,
  409 + destination_latitude=_destination.latitude,
  410 + destination_longitude=_destination.longitude,
  411 + extra_config=_extra_config,
  412 + )
442 413
443 - if cities_dict_first_model is None:  
444 - cities_dict_first_model = deepcopy(cities_dict)  
445 -  
446 - _results['footprints'] = footprints  
447 -  
448 - total_foot = 0.0  
449 - total_dist = 0.0  
450 - total_train_trips = 0  
451 - total_plane_trips = 0  
452 -  
453 - cities_mean_dict = {}  
454 - for city in cities_sum_foot.keys():  
455 - city_mean_foot = 1.0 * cities_sum_foot[city] / len(emission_models)  
456 - city_mean_dist = 1.0 * cities_sum_dist[city] / len(emission_models)  
457 - city_train_trips = cities_dict_first_model[city]['train_trips']  
458 - city_plane_trips = cities_dict_first_model[city]['plane_trips']  
459 - cities_mean_dict[city] = {  
460 - 'address': destinations_by_city_key[city].address,  
461 - 'city': city,  
462 - 'footprint': city_mean_foot,  
463 - 'distance': city_mean_dist,  
464 - 'train_trips': city_train_trips,  
465 - 'plane_trips': city_plane_trips, 414 + _key = get_city_key(_destination)
  415 +
  416 + destinations_by_city_key[_key] = _destination
  417 +
  418 + if _key not in cities_dict:
  419 + cities_dict[_key] = {
  420 + 'city': _key,
  421 + 'address': _destination.address,
  422 + 'footprint': 0.0,
  423 + 'distance': 0.0,
  424 + 'train_trips': 0,
  425 + 'plane_trips': 0,
  426 + }
  427 + cities_dict[_key]['footprint'] += footprint['co2eq_kg']
  428 + cities_dict[_key]['distance'] += footprint['distance_km']
  429 + cities_dict[_key]['train_trips'] += footprint['train_trips']
  430 + cities_dict[_key]['plane_trips'] += footprint['plane_trips']
  431 + if _key not in cities_sum_foot:
  432 + cities_sum_foot[_key] = 0.0
  433 + cities_sum_foot[_key] += footprint['co2eq_kg']
  434 + if _key not in cities_sum_dist:
  435 + cities_sum_dist[_key] = 0.0
  436 + cities_sum_dist[_key] += footprint['distance_km']
  437 +
  438 + cities = sorted(cities_dict.values(), key=lambda c: c['footprint'])
  439 +
  440 + footprints[model.slug] = {
  441 + 'cities': cities,
  442 + }
  443 +
  444 + if cities_dict_first_model is None:
  445 + cities_dict_first_model = deepcopy(cities_dict)
  446 +
  447 + _results['footprints'] = footprints
  448 +
  449 + total_foot = 0.0
  450 + total_dist = 0.0
  451 + total_train_trips = 0
  452 + total_plane_trips = 0
  453 +
  454 + cities_mean_dict = {}
  455 + for city in cities_sum_foot.keys():
  456 + city_mean_foot = 1.0 * cities_sum_foot[city] / len(emission_models)
  457 + city_mean_dist = 1.0 * cities_sum_dist[city] / len(emission_models)
  458 + city_train_trips = cities_dict_first_model[city]['train_trips']
  459 + city_plane_trips = cities_dict_first_model[city]['plane_trips']
  460 + cities_mean_dict[city] = {
  461 + 'address': destinations_by_city_key[city].address,
  462 + 'city': city,
  463 + 'footprint': city_mean_foot,
  464 + 'distance': city_mean_dist,
  465 + 'train_trips': city_train_trips,
  466 + 'plane_trips': city_plane_trips,
  467 + }
  468 + total_foot += city_mean_foot
  469 + total_dist += city_mean_dist
  470 + total_train_trips += city_train_trips
  471 + total_plane_trips += city_plane_trips
  472 +
  473 + cities_mean = [cities_mean_dict[k] for k in cities_mean_dict.keys()]
  474 + cities_mean = sorted(cities_mean, key=lambda c: c['footprint'])
  475 +
  476 + _results['mean_footprint'] = { # DEPRECATED?
  477 + 'cities': cities_mean
466 } 478 }
467 - total_foot += city_mean_foot  
468 - total_dist += city_mean_dist  
469 - total_train_trips += city_train_trips  
470 - total_plane_trips += city_plane_trips 479 + _results['cities'] = cities_mean
471 480
472 - cities_mean = [cities_mean_dict[k] for k in cities_mean_dict.keys()]  
473 - cities_mean = sorted(cities_mean, key=lambda c: c['footprint']) 481 + _results['total'] = total_foot # DEPRECATED
  482 + _results['footprint'] = total_foot
474 483
475 - _results['mean_footprint'] = { # DEPRECATED?  
476 - 'cities': cities_mean  
477 - }  
478 - _results['cities'] = cities_mean  
479 -  
480 - _results['total'] = total_foot # DEPRECATED  
481 - _results['footprint'] = total_foot  
482 -  
483 - _results['distance'] = total_dist  
484 - _results['train_trips'] = total_train_trips  
485 - _results['plane_trips'] = total_plane_trips  
486 -  
487 - return _results  
488 -  
489 - # SCENARIO A : One Origin, At Least One Destination #######################  
490 - #  
491 - # In this scenario, we compute the sum of each of the travels' footprint,  
492 - # for each of the Emission Models, and present a mean of all Models.  
493 - #  
494 - if 1 == len(origins):  
495 - estimation.scenario = ScenarioEnum.one_to_many  
496 - results = compute_one_to_many(  
497 - _origin=origins[0],  
498 - _destinations=destinations,  
499 - _extra_config=extra_config,  
500 - ) 484 + _results['distance'] = total_dist
  485 + _results['train_trips'] = total_train_trips
  486 + _results['plane_trips'] = total_plane_trips
501 487
502 - # SCENARIO B : At Least One Origin, One Destination #######################  
503 - #  
504 - # Same as A for now.  
505 - #  
506 - elif 1 == len(destinations):  
507 - estimation.scenario = ScenarioEnum.many_to_one  
508 - results = compute_one_to_many(  
509 - _origin=destinations[0],  
510 - _destinations=origins,  
511 - _extra_config=extra_config,  
512 - ) 488 + return _results
513 489
514 - # SCENARIO C : At Least One Origin, At Least One Destination ##############  
515 - #  
516 - # Run Scenario A for each Destination, and expose optimum Destination.  
517 - #  
518 - else:  
519 - estimation.scenario = ScenarioEnum.many_to_many  
520 - unique_city_keys = []  
521 - result_cities = []  
522 - for destination in destinations:  
523 - city_key = get_city_key(destination)  
524 -  
525 - if city_key in unique_city_keys:  
526 - continue  
527 - else:  
528 - unique_city_keys.append(city_key) 490 + # SCENARIO A : One Origin, At Least One Destination #######################
  491 + #
  492 + # In this scenario, we compute the sum of each of the travels' footprint,
  493 + # for each of the Emission Models, and present a mean of all Models.
  494 + #
  495 + if 1 == len(origins):
  496 + estimation.scenario = ScenarioEnum.one_to_many
  497 + results = compute_one_to_many(
  498 + _origin=origins[0],
  499 + _destinations=destinations,
  500 + _extra_config=extra_config,
  501 + )
529 502
530 - city_results = compute_one_to_many(  
531 - _origin=destination, 503 + # SCENARIO B : At Least One Origin, One Destination #######################
  504 + #
  505 + # Same as A for now.
  506 + #
  507 + elif 1 == len(destinations):
  508 + estimation.scenario = ScenarioEnum.many_to_one
  509 + results = compute_one_to_many(
  510 + _origin=destinations[0],
532 _destinations=origins, 511 _destinations=origins,
533 _extra_config=extra_config, 512 _extra_config=extra_config,
534 ) 513 )
535 - city_results['city'] = city_key  
536 - city_results['address'] = destination.address  
537 - result_cities.append(city_results)  
538 514
539 - result_cities = sorted(result_cities, key=lambda c: int(c['footprint']))  
540 - results = {  
541 - 'cities': result_cities,  
542 - } 515 + # SCENARIO C : At Least One Origin, At Least One Destination ##############
  516 + #
  517 + # Run Scenario A for each Destination, and expose optimum Destination.
  518 + #
  519 + else:
  520 + estimation.scenario = ScenarioEnum.many_to_many
  521 + unique_city_keys = []
  522 + result_cities = []
  523 + for destination in destinations:
  524 + city_key = get_city_key(destination)
  525 +
  526 + if city_key in unique_city_keys:
  527 + continue
  528 + else:
  529 + unique_city_keys.append(city_key)
543 530
544 - # WRITE RESULTS INTO THE DATABASE ######################################### 531 + city_results = compute_one_to_many(
  532 + _origin=destination,
  533 + _destinations=origins,
  534 + _extra_config=extra_config,
  535 + )
  536 + city_results['city'] = city_key
  537 + city_results['address'] = destination.address
  538 + result_cities.append(city_results)
  539 +
  540 + result_cities = sorted(result_cities, key=lambda c: int(c['footprint']))
  541 + results = {
  542 + 'cities': result_cities,
  543 + }
  544 +
  545 + # WRITE RESULTS INTO THE DATABASE #########################################
545 546
546 - estimation.status = StatusEnum.success  
547 - estimation.output_yaml = yaml_dump(results)  
548 - db.session.commit() 547 + estimation.status = StatusEnum.success
  548 + estimation.output_yaml = yaml_dump(results)
  549 + db.session.commit()
  550 +
  551 + # FINALLY, RESPOND ########################################################
549 552
550 - # FINALLY, RESPOND ######################################################## 553 + response += yaml_dump(results) + "\n"
551 554
552 - response += yaml_dump(results) + "\n" 555 + return _respond(response)
553 556
554 - return _respond(response) 557 + except Exception as e:
  558 + errmsg = "Computation failed : %s" % e
  559 + if estimation:
  560 + _handle_failure(estimation, errmsg)
  561 + return _respond(errmsg)
555 562
556 563
557 @main.route("/estimation/<public_id>.<extension>") 564 @main.route("/estimation/<public_id>.<extension>")
@@ -563,7 +570,7 @@ def consult_estimation(public_id, extension): @@ -563,7 +570,7 @@ def consult_estimation(public_id, extension):
563 except sqlalchemy.orm.exc.NoResultFound: 570 except sqlalchemy.orm.exc.NoResultFound:
564 return abort(404) 571 return abort(404)
565 except Exception as e: 572 except Exception as e:
566 - # TODO: log 573 + # TODO: log?
567 return abort(500) 574 return abort(500)
568 575
569 # allowed_formats = ['html'] 576 # allowed_formats = ['html']