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 240 _estimation.warnings = _warning_message
241 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 511 _destinations=origins,
533 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 564 @main.route("/estimation/<public_id>.<extension>")
... ... @@ -563,7 +570,7 @@ def consult_estimation(public_id, extension):
563 570 except sqlalchemy.orm.exc.NoResultFound:
564 571 return abort(404)
565 572 except Exception as e:
566   - # TODO: log
  573 + # TODO: log?
567 574 return abort(500)
568 575  
569 576 # allowed_formats = ['html']
... ...