<commit_before>/*
 * Copyright (C) 2017-2018 Ryan Huang
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

mod board;
mod board_parts;
mod sys;
mod vec2;

use imgui::{ImGuiCond, ImStr, StyleVar, Ui};

use self::board::board;
pub use self::sys::run;
use self::vec2::Vec2;
use model::{Color, ColorMap, GameType, Model, Player};
use update::Event;

pub fn draw(ui: &Ui, size: (f32, f32), model: &Model) -> Option<Event> {
    let mut event = None;
    let mut window_states = model.window_states.borrow_mut();

    ui.main_menu_bar(|| {
        ui.menu(im_str!("Game")).build(|| {
            ui.menu_item(im_str!("New game")).enabled(false).build();

            ui.menu(im_str!("Laurentius")).build(|| {
                player_options(ui, &mut event, GameType::Laurentius);
            });
            ui.menu(im_str!("Ocius")).build(|| {
                player_options(ui, &mut event, GameType::Ocius);
            });

            ui.separator();

            ui.menu_item(im_str!("Rules")).enabled(false).build();
            if ui.is_item_hovered() {
                ui.tooltip_text("Any changes to the rules apply at the start of the next game.");
            }

            ui.menu_item(im_str!("One tile to exchange"))
                .selected(&mut model.exchange_one_hex.borrow_mut())
                .build();
            if ui.is_item_hovered() {
                ui.tooltip_text(
                    "If selected, only one tile (rather than two) is needed to exchange for a piece."
                );
            }

            ui.separator();

            if ui.menu_item(im_str!("Quit")).build() {
                insert_if_empty(&mut event, Event::Quit);
            }
        });

        ui.menu(im_str!("Computer")).build(|| {
            ui.slider_int(
                im_str!("Search depth"),
                &mut model.ai_search_depth.borrow_mut(),
                1,
                7,
            ).build();
            if ui.is_item_hovered() {
                ui.tooltip_text(
                    "How many moves ahead the computer will search.\nFewer moves is \
                     faster and easier, while more moves is slower and more difficult.",
                );
            }

            ui.menu_item(im_str!("Show debug info"))
                .selected(&mut window_states.ai_debug)
                .build();
        });

        ui.menu(im_str!("Help")).build(|| {
            ui.menu_item(im_str!("How to Play"))
                .selected(&mut window_states.how_to_play)
                .build();
            ui.menu_item(im_str!("About"))
                .selected(&mut window_states.about)
                .build();
        });
    });

    ui.with_style_var(StyleVar::WindowRounding(0.0), || {
        draw_window(ui, size, model, &mut event);
    });

    if window_states.ai_debug {
        ui.window(im_str!("AI Debug Info"))
            .opened(&mut window_states.ai_debug)
            .size((300.0, 600.0), ImGuiCond::FirstUseEver)
            .build(|| {
                if let Ok(debug_info) = model.ai.debug_info.read() {
                    ui.text(debug_info.clone());
                }
            });
    }

    if window_states.how_to_play {
        // TODO: Create an interactive, in-game tutorial to teach the rules of the game
        ui.window(im_str!("How to Play"))
            .opened(&mut window_states.how_to_play)
            .build(|| {
                ui.text(
                    "Unfortunately, there isn't an in-game tutorial. Sorry!\nSee coerceo.com for \
                     the rules of the game.",
                );
            });
    }

    if window_states.about {
        ui.window(im_str!("About"))
            .opened(&mut window_states.about)
            .build(|| {
                ui.text(
                    "Coerceo v1.0.0 (https://github.com/NPN/coerceo)

An unofficial clone of a strategic board game.

Copyright (C) 2017-2018 Ryan Huang
This program comes with absolutely no warranty.
See the GNU AGPL, version 3 or later, for details.

This program includes work from the following sources:

imgui-rs (https://github.com/Gekkio/imgui-rs)
Copyright (c) 2015-2017 The imgui-rs Developers
Licensed under the MIT License.

Fira Sans (https://github.com/mozilla/Fira)
Copyright (c) 2012-2015 The Mozilla Foundation and Telefonica S.A.
Licensed under the SIL Open Font License v1.1",
                );
            });
    }

    event
}

fn player_options(ui: &Ui, event: &mut Option<Event>, game_type: GameType) {
    use self::Player::*;
    if ui.menu_item(im_str!("Human vs. Human")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Human, Human)),
        );
    }
    if ui.menu_item(im_str!("Human vs. Computer")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Human, Computer)),
        );
    }
    if ui.menu_item(im_str!("Computer vs. Human")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Computer, Human)),
        );
    }
    if ui.menu_item(im_str!("Computer vs. Computer")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Computer, Computer)),
        );
    }
}

fn draw_window(ui: &Ui, size: (f32, f32), model: &Model, event: &mut Option<Event>) {
    ui.window(im_str!("Coerceo"))
        .size(size, ImGuiCond::Always)
        .position((0.0, 27.0), ImGuiCond::Always)
        .title_bar(false)
        .resizable(false)
        .movable(false)
        .no_bring_to_front_on_focus(true)
        .build(|| {
            ui.text("Welcome to Coerceo!");

            let exchange_hex_string = if model.board.hexes_to_exchange == 1 {
                "One tile to exchange"
            } else {
                "Two tiles to exchange"
            };
            ui.text(format!(
                "{:?} vs. {:?} ({})",
                model.players.white, model.players.black, exchange_hex_string
            ));

            if let Some(click) = board(ui, model, Vec2::new(size.0 - 16.0, size.1 - 232.0)) {
                insert_if_empty(event, click);
            }

            let format_piece_count = |count| match count {
                1 => String::from("1 piece"),
                _ => format!("{} pieces", count),
            };

            let format_hex_count = |count| match count {
                1 => String::from("1 captured tile"),
                _ => format!("{} captured tiles", count),
            };

            let display_vitals = || {
                ui.text(format!(
                    "{:?} has {} and {}.",
                    Color::White,
                    format_piece_count(model.board.pieces(Color::White)),
                    format_hex_count(model.board.hexes(Color::White)),
                ));
                ui.text(format!(
                    "{:?} has {} and {}.",
                    Color::Black,
                    format_piece_count(model.board.pieces(Color::Black)),
                    format_hex_count(model.board.hexes(Color::Black)),
                ));
            };

            let button_size = Vec2::new(155.0, 29.0);
            use model::Outcome::*;
            match model.outcome {
                Win(color) => {
                    ui.text(format!("{:?} wins!", color));
                    display_vitals();
                    if model.can_undo() && ui.button(im_str!("Undo"), button_size) {
                        insert_if_empty(event, Event::Undo);
                    }
                }
                InProgress => {
                    if model.players.white == model.players.black {
                        ui.text(format!("It's {:?}'s turn.", model.board.turn,));
                    } else {
                        ui.text(match model.current_player() {
                            Player::Computer => "Waiting for the computer...",
                            Player::Human => "It's your turn.",
                        });
                    }

                    display_vitals();

                    horz_button_layout(
                        ui,
                        vec![
                            (model.can_undo(), im_str!("Undo"), Event::Undo),
                            (model.can_redo(), im_str!("Redo"), Event::Redo),
                        ],
                        button_size,
                        event,
                    );
                    let is_human_player = model.current_player() == Player::Human;
                    horz_button_layout(
                        ui,
                        vec![
                            (is_human_player, im_str!("Resign"), Event::Resign),
                            (
                                model.board.can_exchange() && is_human_player,
                                if model.exchanging {
                                    im_str!("Stop Exchanging")
                                } else {
                                    im_str!("Exchange")
                                },
                                Event::Exchange,
                            ),
                        ],
                        button_size,
                        event,
                    );
                }
                // Draw cases
                _ => {
                    let message = match model.outcome {
                        DrawStalemate => "It's a draw by stalemate!",
                        DrawThreefoldRepetition => "It's a draw by threefold repetition!",
                        DrawInsufficientMaterial => "It's a draw by insufficient material!",
                        _ => unreachable!(),
                    };
                    ui.text(message);
                    display_vitals();
                    if model.can_undo() && ui.button(im_str!("Undo"), button_size) {
                        insert_if_empty(event, Event::Undo);
                    }
                }
            }
        });
}

fn horz_button_layout(
    ui: &Ui,
    buttons: Vec<(bool, &ImStr, Event)>,
    size: Vec2,
    event: &mut Option<Event>,
) {
    if !buttons.iter().any(|&(show, _, _)| show) {
        return;
    }

    for (show, label, action) in buttons {
        if show {
            if ui.button(label, size) {
                insert_if_empty(event, action);
            }
        } else {
            ui.dummy(size);
        }
        ui.same_line(0.0);
    }
    ui.new_line();
}

fn insert_if_empty<T>(a: &mut Option<T>, b: T) {
    if a.is_none() {
        *a = Some(b);
    }
}
<commit_msg>Add disclaimer to About<commit_after>/*
 * Copyright (C) 2017-2018 Ryan Huang
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

mod board;
mod board_parts;
mod sys;
mod vec2;

use imgui::{ImGuiCond, ImStr, StyleVar, Ui};

use self::board::board;
pub use self::sys::run;
use self::vec2::Vec2;
use model::{Color, ColorMap, GameType, Model, Player};
use update::Event;

pub fn draw(ui: &Ui, size: (f32, f32), model: &Model) -> Option<Event> {
    let mut event = None;
    let mut window_states = model.window_states.borrow_mut();

    ui.main_menu_bar(|| {
        ui.menu(im_str!("Game")).build(|| {
            ui.menu_item(im_str!("New game")).enabled(false).build();

            ui.menu(im_str!("Laurentius")).build(|| {
                player_options(ui, &mut event, GameType::Laurentius);
            });
            ui.menu(im_str!("Ocius")).build(|| {
                player_options(ui, &mut event, GameType::Ocius);
            });

            ui.separator();

            ui.menu_item(im_str!("Rules")).enabled(false).build();
            if ui.is_item_hovered() {
                ui.tooltip_text("Any changes to the rules apply at the start of the next game.");
            }

            ui.menu_item(im_str!("One tile to exchange"))
                .selected(&mut model.exchange_one_hex.borrow_mut())
                .build();
            if ui.is_item_hovered() {
                ui.tooltip_text(
                    "If selected, only one tile (rather than two) is needed to exchange for a piece."
                );
            }

            ui.separator();

            if ui.menu_item(im_str!("Quit")).build() {
                insert_if_empty(&mut event, Event::Quit);
            }
        });

        ui.menu(im_str!("Computer")).build(|| {
            ui.slider_int(
                im_str!("Search depth"),
                &mut model.ai_search_depth.borrow_mut(),
                1,
                7,
            ).build();
            if ui.is_item_hovered() {
                ui.tooltip_text(
                    "How many moves ahead the computer will search.\nFewer moves is \
                     faster and easier, while more moves is slower and more difficult.",
                );
            }

            ui.menu_item(im_str!("Show debug info"))
                .selected(&mut window_states.ai_debug)
                .build();
        });

        ui.menu(im_str!("Help")).build(|| {
            ui.menu_item(im_str!("How to Play"))
                .selected(&mut window_states.how_to_play)
                .build();
            ui.menu_item(im_str!("About"))
                .selected(&mut window_states.about)
                .build();
        });
    });

    ui.with_style_var(StyleVar::WindowRounding(0.0), || {
        draw_window(ui, size, model, &mut event);
    });

    if window_states.ai_debug {
        ui.window(im_str!("AI Debug Info"))
            .opened(&mut window_states.ai_debug)
            .size((300.0, 600.0), ImGuiCond::FirstUseEver)
            .build(|| {
                if let Ok(debug_info) = model.ai.debug_info.read() {
                    ui.text(debug_info.clone());
                }
            });
    }

    if window_states.how_to_play {
        // TODO: Create an interactive, in-game tutorial to teach the rules of the game
        ui.window(im_str!("How to Play"))
            .opened(&mut window_states.how_to_play)
            .build(|| {
                ui.text(
                    "Unfortunately, there isn't an in-game tutorial. Sorry!\nSee coerceo.com for \
                     the rules of the game.",
                );
            });
    }

    if window_states.about {
        ui.window(im_str!("About"))
            .opened(&mut window_states.about)
            .build(|| {
                ui.text(
                    "Coerceo v1.0.0 (https://github.com/NPN/coerceo)

An unofficial clone of a strategic board game.

Copyright (C) 2017-2018 Ryan Huang
This program comes with absolutely no warranty.
See the GNU AGPL, version 3 or later, for details.

All rights, trademarks, copyrights, concepts, etc. of the game
Coerceo belong to the Coerceo Company.

This program includes work from the following sources:

imgui-rs (https://github.com/Gekkio/imgui-rs)
Copyright (c) 2015-2017 The imgui-rs Developers
Licensed under the MIT License.

Fira Sans (https://github.com/mozilla/Fira)
Copyright (c) 2012-2015 The Mozilla Foundation and Telefonica S.A.
Licensed under the SIL Open Font License v1.1",
                );
            });
    }

    event
}

fn player_options(ui: &Ui, event: &mut Option<Event>, game_type: GameType) {
    use self::Player::*;
    if ui.menu_item(im_str!("Human vs. Human")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Human, Human)),
        );
    }
    if ui.menu_item(im_str!("Human vs. Computer")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Human, Computer)),
        );
    }
    if ui.menu_item(im_str!("Computer vs. Human")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Computer, Human)),
        );
    }
    if ui.menu_item(im_str!("Computer vs. Computer")).build() {
        insert_if_empty(
            event,
            Event::NewGame(game_type, ColorMap::new(Computer, Computer)),
        );
    }
}

fn draw_window(ui: &Ui, size: (f32, f32), model: &Model, event: &mut Option<Event>) {
    ui.window(im_str!("Coerceo"))
        .size(size, ImGuiCond::Always)
        .position((0.0, 27.0), ImGuiCond::Always)
        .title_bar(false)
        .resizable(false)
        .movable(false)
        .no_bring_to_front_on_focus(true)
        .build(|| {
            ui.text("Welcome to Coerceo!");

            let exchange_hex_string = if model.board.hexes_to_exchange == 1 {
                "One tile to exchange"
            } else {
                "Two tiles to exchange"
            };
            ui.text(format!(
                "{:?} vs. {:?} ({})",
                model.players.white, model.players.black, exchange_hex_string
            ));

            if let Some(click) = board(ui, model, Vec2::new(size.0 - 16.0, size.1 - 232.0)) {
                insert_if_empty(event, click);
            }

            let format_piece_count = |count| match count {
                1 => String::from("1 piece"),
                _ => format!("{} pieces", count),
            };

            let format_hex_count = |count| match count {
                1 => String::from("1 captured tile"),
                _ => format!("{} captured tiles", count),
            };

            let display_vitals = || {
                ui.text(format!(
                    "{:?} has {} and {}.",
                    Color::White,
                    format_piece_count(model.board.pieces(Color::White)),
                    format_hex_count(model.board.hexes(Color::White)),
                ));
                ui.text(format!(
                    "{:?} has {} and {}.",
                    Color::Black,
                    format_piece_count(model.board.pieces(Color::Black)),
                    format_hex_count(model.board.hexes(Color::Black)),
                ));
            };

            let button_size = Vec2::new(155.0, 29.0);
            use model::Outcome::*;
            match model.outcome {
                Win(color) => {
                    ui.text(format!("{:?} wins!", color));
                    display_vitals();
                    if model.can_undo() && ui.button(im_str!("Undo"), button_size) {
                        insert_if_empty(event, Event::Undo);
                    }
                }
                InProgress => {
                    if model.players.white == model.players.black {
                        ui.text(format!("It's {:?}'s turn.", model.board.turn,));
                    } else {
                        ui.text(match model.current_player() {
                            Player::Computer => "Waiting for the computer...",
                            Player::Human => "It's your turn.",
                        });
                    }

                    display_vitals();

                    horz_button_layout(
                        ui,
                        vec![
                            (model.can_undo(), im_str!("Undo"), Event::Undo),
                            (model.can_redo(), im_str!("Redo"), Event::Redo),
                        ],
                        button_size,
                        event,
                    );
                    let is_human_player = model.current_player() == Player::Human;
                    horz_button_layout(
                        ui,
                        vec![
                            (is_human_player, im_str!("Resign"), Event::Resign),
                            (
                                model.board.can_exchange() && is_human_player,
                                if model.exchanging {
                                    im_str!("Stop Exchanging")
                                } else {
                                    im_str!("Exchange")
                                },
                                Event::Exchange,
                            ),
                        ],
                        button_size,
                        event,
                    );
                }
                // Draw cases
                _ => {
                    let message = match model.outcome {
                        DrawStalemate => "It's a draw by stalemate!",
                        DrawThreefoldRepetition => "It's a draw by threefold repetition!",
                        DrawInsufficientMaterial => "It's a draw by insufficient material!",
                        _ => unreachable!(),
                    };
                    ui.text(message);
                    display_vitals();
                    if model.can_undo() && ui.button(im_str!("Undo"), button_size) {
                        insert_if_empty(event, Event::Undo);
                    }
                }
            }
        });
}

fn horz_button_layout(
    ui: &Ui,
    buttons: Vec<(bool, &ImStr, Event)>,
    size: Vec2,
    event: &mut Option<Event>,
) {
    if !buttons.iter().any(|&(show, _, _)| show) {
        return;
    }

    for (show, label, action) in buttons {
        if show {
            if ui.button(label, size) {
                insert_if_empty(event, action);
            }
        } else {
            ui.dummy(size);
        }
        ui.same_line(0.0);
    }
    ui.new_line();
}

fn insert_if_empty<T>(a: &mut Option<T>, b: T) {
    if a.is_none() {
        *a = Some(b);
    }
}
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! Adapter for WebPush.
//!
//! Implemented as described in the draft IETF RFC:
//! https://tools.ietf.org/html/draft-ietf-webpush-protocol-04
//!
//! Encryption and sending of push notifications is controlled by the
//! "webpush" build feature. Older versions of OpenSSL (< 1.0.0) are
//! missing the necessary APIs to support the implementation.
//!

extern crate serde_json;

#[cfg(feature = "webpush")]
mod crypto;
mod db;

use foxbox_adapters::adapter::*;
use foxbox_taxonomy::api::{ Error, InternalError };
use foxbox_taxonomy::values::{ Range, Type, Value, Json };
use foxbox_taxonomy::services::*;
use foxbox_taxonomy::util::Id;
#[cfg(feature = "webpush")]
use hyper::header::{ ContentEncoding, Encoding };
#[cfg(feature = "webpush")]
use hyper::Client;
#[cfg(feature = "webpush")]
use hyper::client::Body;
use rusqlite::{ self };
use std::collections::{ HashMap, HashSet };
use std::sync::Arc;
use std::thread;
use traits::Controller;
use transformable_channels::mpsc::*;

header! { (Encryption, "Encryption") => [String] }
header! { (EncryptionKey, "Encryption-Key") => [String] }

static ADAPTER_NAME: &'static str = "WebPush adapter (built-in)";
static ADAPTER_VENDOR: &'static str = "team@link.mozilla.org";
static ADAPTER_VERSION: [u32;4] = [0, 0, 0, 0];

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Subscription {
    pub push_uri: String,
    pub public_key: String
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SubscriptionGetter {
    subscriptions: Vec<Subscription>
}

impl SubscriptionGetter {
    fn new(subs: Vec<Subscription>) -> Self {
        SubscriptionGetter {
            subscriptions: subs
        }
    }
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ResourceGetter {
    resources: Vec<String>
}

impl ResourceGetter {
    fn new(res: Vec<String>) -> Self {
        ResourceGetter {
            resources: res
        }
    }
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct NotifySetter {
    resource: String,
    message: String
}

impl Subscription {
    #[cfg(feature = "webpush")]
    fn notify(&self, message: &str) {
        let enc = match self::crypto::encrypt(&self.public_key, message.to_owned()) {
            Some(x) => x,
            None => {
                warn!("notity subscription {} failed for {}", self.push_uri, message);
                return;
            }
        };

        let client = Client::new();
        let res = match client.post(&self.push_uri)
            .header(ContentEncoding(vec![Encoding::EncodingExt(String::from("aesgcm128"))]))
            .header(EncryptionKey(format!("keyid=p256dh;dh={}", enc.public_key)))
            .header(Encryption(format!("keyid=p256dh;salt={}", enc.salt)))
            .body(Body::BufBody(&enc.output, enc.output.len()))
            .send() {
                Ok(x) => x,
                Err(e) => { warn!("notify subscription {} failed: {:?}", self.push_uri, e); return; }
            };

        info!("notified subscription {} (status {:?})", self.push_uri, res.status);
    }

    #[cfg(not(feature = "webpush"))]
    fn notify(&self, _: &str) {
        warn!("discard notification for subscription {}, webpush disabled at build time", self.push_uri);
    }
}

pub struct WebPush<C> {
    controller: C,
    getter_resource_id: Id<Getter>,
    getter_subscription_id: Id<Getter>,
    setter_resource_id: Id<Setter>,
    setter_subscribe_id: Id<Setter>,
    setter_unsubscribe_id: Id<Setter>,
    setter_notify_id: Id<Setter>,
}

impl<C: Controller> WebPush<C> {
    pub fn id() -> Id<AdapterId> {
        Id::new("webpush@link.mozilla.org")
    }

    pub fn service_webpush_id() -> Id<ServiceId> {
        Id::new("service:webpush@link.mozilla.org")
    }

    pub fn getter_resource_id() -> Id<Getter> {
        Id::new("getter:resource.webpush@link.mozilla.org")
    }

    pub fn getter_subscription_id() -> Id<Getter> {
        Id::new("getter:subscription.webpush@link.mozilla.org")
    }

    pub fn setter_resource_id() -> Id<Setter> {
        Id::new("setter:resource.webpush@link.mozilla.org")
    }

    pub fn setter_subscribe_id() -> Id<Setter> {
        Id::new("setter:subscribe.webpush@link.mozilla.org")
    }

    pub fn setter_unsubscribe_id() -> Id<Setter> {
        Id::new("setter:unsubscribe.webpush@link.mozilla.org")
    }

    pub fn setter_notify_id() -> Id<Setter> {
        Id::new("setter:notify.webpush@link.mozilla.org")
    }
}

impl<C: Controller> Adapter for WebPush<C> {
    fn id(&self) -> Id<AdapterId> {
        Self::id()
    }

    fn name(&self) -> &str {
        ADAPTER_NAME
    }

    fn vendor(&self) -> &str {
        ADAPTER_VENDOR
    }

    fn version(&self) -> &[u32;4] {
        &ADAPTER_VERSION
    }

    fn fetch_values(&self, mut set: Vec<Id<Getter>>) -> ResultMap<Id<Getter>, Option<Value>, Error> {
        set.drain(..).map(|id| {
            let user_id = 1; // FIXME: currently logged in user

            macro_rules! getter_api {
                ($getter:ident, $getter_id:ident, $getter_type:ident) => (
                    if id == self.$getter_id {
                        match self.$getter(user_id) {
                            Ok(data) => {
                                let rsp = $getter_type::new(data);
                                return (id, Ok(Some(Value::Json(Arc::new(Json(serde_json::to_value(&rsp)))))));
                            },
                            Err(_) => return (id, Err(Error::InternalError(InternalError::InvalidInitialService)))
                        };
                    }
                )
            }

            getter_api!(get_subscriptions, getter_subscription_id, SubscriptionGetter);
            getter_api!(get_resources, getter_resource_id, ResourceGetter);
            (id.clone(), Err(Error::InternalError(InternalError::NoSuchGetter(id))))
        }).collect()
    }

    fn send_values(&self, mut values: HashMap<Id<Setter>, Value>) -> ResultMap<Id<Setter>, (), Error> {
        values.drain().map(|(id, value)| {
            let user_id = 1; // FIXME: currently logged in user

            let arc_json_value = match value {
                Value::Json(v) => v,
                _ => return (id, Err(Error::InternalError(InternalError::InvalidInitialService)))
            };
            let Json(ref json_value) = *arc_json_value;

            macro_rules! setter_api {
                ($setter:ident, $setter_id:ident, $setter_type:ident) => (
                    if id == self.$setter_id {
                        let data: Result<$setter_type, _> = serde_json::from_value(json_value.clone());
                        match data {
                            Ok(x) => {
                                self.$setter(user_id, &x).unwrap();
                                return (id, Ok(()));
                            }
                            Err(_) => return (id, Err(Error::InternalError(InternalError::InvalidInitialService)))
                        }
                    }
                )
            }

            setter_api!(set_resources, setter_resource_id, ResourceGetter);
            setter_api!(set_subscribe, setter_subscribe_id, SubscriptionGetter);
            setter_api!(set_unsubscribe, setter_unsubscribe_id, SubscriptionGetter);
            setter_api!(set_notify, setter_notify_id, NotifySetter);
            (id.clone(), Err(Error::InternalError(InternalError::NoSuchSetter(id))))
        }).collect()
    }

    fn register_watch(&self, mut watch: Vec<(Id<Getter>, Option<Range>)>,
        _: Box<ExtSender<WatchEvent>>) ->
           ResultMap<Id<Getter>, Box<AdapterWatchGuard>, Error>
    {
        watch.drain(..).map(|(id, _)| {
            (id.clone(), Err(Error::GetterDoesNotSupportWatching(id)))
        }).collect()
    }
}

impl<C: Controller> WebPush<C> {
    pub fn init<A: AdapterManagerHandle>(controller: C, adapt: &A) -> Result<(), Error> {
        let wp = Box::new(Self::new(controller));
        let id = WebPush::<C>::id();
        let service_id = WebPush::<C>::service_webpush_id();
        let getter_resource_id = wp.getter_resource_id.clone();
        let getter_subscription_id = wp.getter_subscription_id.clone();
        let setter_resource_id = wp.setter_resource_id.clone();
        let setter_subscribe_id = wp.setter_subscribe_id.clone();
        let setter_unsubscribe_id = wp.setter_unsubscribe_id.clone();
        let setter_notify_id = wp.setter_notify_id.clone();

        try!(adapt.add_adapter(wp));
        try!(adapt.add_service(Service::empty(service_id.clone(), id.clone())));

        macro_rules! add_getter {
            ($id:ident, $kind_id:expr) => (
                try!(adapt.add_getter(Channel {
                    tags: HashSet::new(),
                    adapter: id.clone(),
                    id: $id,
                    last_seen: None,
                    service: service_id.clone(),
                    mechanism: Getter {
                        kind: ChannelKind::Extension {
                            vendor: ADAPTER_VENDOR.to_owned(),
                            adapter: ADAPTER_NAME.to_owned(),
                            kind: $kind_id.to_owned(),
                            typ: Type::Json,
                        },
                        poll: None,
                        trigger: None,
                        watch: false,
                        updated: None
                    }
                }));
            )
        }

        macro_rules! add_setter {
            ($id:ident, $kind_id:expr) => (
                try!(adapt.add_setter(Channel {
                    tags: HashSet::new(),
                    adapter: id.clone(),
                    id: $id,
                    last_seen: None,
                    service: service_id.clone(),
                    mechanism: Setter {
                        kind: ChannelKind::Extension {
                            vendor: ADAPTER_VENDOR.to_owned(),
                            adapter: ADAPTER_NAME.to_owned(),
                            kind: $kind_id.to_owned(),
                            typ: Type::Json,
                        },
                        push: None,
                        updated: None
                    }
                }));
            )
        }

        add_getter!(getter_resource_id, "WebPushResource");
        add_getter!(getter_subscription_id, "WebPushSubscription");
        add_setter!(setter_resource_id, "WebPushResource");
        add_setter!(setter_subscribe_id, "WebPushSubscription");
        add_setter!(setter_unsubscribe_id, "WebPushSubscription");
        add_setter!(setter_notify_id, "WebPushNotify");
        Ok(())
    }

    fn new(controller: C) -> Self
    {
        WebPush {
            controller: controller,
            getter_resource_id: Self::getter_resource_id(),
            getter_subscription_id: Self::getter_subscription_id(),
            setter_resource_id: Self::setter_resource_id(),
            setter_subscribe_id: Self::setter_subscribe_id(),
            setter_unsubscribe_id: Self::setter_unsubscribe_id(),
            setter_notify_id: Self::setter_notify_id(),
        }
    }

    fn get_db(&self) -> db::WebPushDb {
        db::WebPushDb::new(&self.controller.get_profile().path_for("webpush.sqlite"))
    }

    fn set_subscribe(&self, user_id: i32, setter: &SubscriptionGetter) -> rusqlite::Result<()> {
        let db = self.get_db();
        for sub in &setter.subscriptions {
            try!(db.subscribe(user_id, sub));
        }
        Ok(())
    }

    fn set_unsubscribe(&self, user_id: i32, setter: &SubscriptionGetter) -> rusqlite::Result<()> {
        let db = self.get_db();
        for sub in &setter.subscriptions {
            try!(db.unsubscribe(user_id, &sub.push_uri));
        }
        Ok(())
    }

    fn set_resources(&self, user_id: i32, setter: &ResourceGetter) -> rusqlite::Result<()> {
        try!(self.get_db().set_resources(user_id, &setter.resources));
        Ok(())
    }

    fn get_resources(&self, user_id: i32) -> rusqlite::Result<Vec<String>> {
        self.get_db().get_resources(user_id)
    }

    fn get_subscriptions(&self, user_id: i32) -> rusqlite::Result<Vec<Subscription>> {
        self.get_db().get_subscriptions(user_id)
    }

    fn get_resource_subscriptions(&self, resource: &str) -> rusqlite::Result<Vec<Subscription>> {
        self.get_db().get_resource_subscriptions(resource)
    }

    fn set_notify(&self, _: i32, setter: &NotifySetter) -> rusqlite::Result<()> {
        info!("notify on resource {}: {}", setter.resource, setter.message);

        let json = json!({resource: setter.resource, message: setter.message});
        let subscriptions = try!(self.get_resource_subscriptions(&setter.resource));
        if subscriptions.is_empty() {
            debug!("no users listening on push resource");
        } else {
            thread::spawn(move || {
                for sub in subscriptions {
                    sub.notify(&json);
                }
            });
        }
        Ok(())
    }
}
<commit_msg>Make things build again<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! Adapter for WebPush.
//!
//! Implemented as described in the draft IETF RFC:
//! https://tools.ietf.org/html/draft-ietf-webpush-protocol-04
//!
//! Encryption and sending of push notifications is controlled by the
//! "webpush" build feature. Older versions of OpenSSL (< 1.0.0) are
//! missing the necessary APIs to support the implementation.
//!

extern crate serde_json;

#[cfg(feature = "webpush")]
mod crypto;
mod db;

use foxbox_adapters::adapter::*;
use foxbox_taxonomy::api::{ Error, InternalError };
use foxbox_taxonomy::values::{ Range, Type, Value, Json };
use foxbox_taxonomy::services::*;
use foxbox_taxonomy::util::Id;
#[cfg(feature = "webpush")]
use hyper::header::{ ContentEncoding, Encoding };
#[cfg(feature = "webpush")]
use hyper::Client;
#[cfg(feature = "webpush")]
use hyper::client::Body;
use rusqlite::{ self };
use std::collections::{ HashMap, HashSet };
use std::sync::Arc;
use std::thread;
use traits::Controller;
use transformable_channels::mpsc::*;

header! { (Encryption, "Encryption") => [String] }
header! { (EncryptionKey, "Encryption-Key") => [String] }

static ADAPTER_NAME: &'static str = "WebPush adapter (built-in)";
static ADAPTER_VENDOR: &'static str = "team@link.mozilla.org";
static ADAPTER_VERSION: [u32;4] = [0, 0, 0, 0];

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Subscription {
    pub push_uri: String,
    pub public_key: String
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SubscriptionGetter {
    subscriptions: Vec<Subscription>
}

impl SubscriptionGetter {
    fn new(subs: Vec<Subscription>) -> Self {
        SubscriptionGetter {
            subscriptions: subs
        }
    }
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ResourceGetter {
    resources: Vec<String>
}

impl ResourceGetter {
    fn new(res: Vec<String>) -> Self {
        ResourceGetter {
            resources: res
        }
    }
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct NotifySetter {
    resource: String,
    message: String
}

impl Subscription {
    #[cfg(feature = "webpush")]
    fn notify(&self, message: &str) {
        let enc = match self::crypto::encrypt(&self.public_key, message.to_owned()) {
            Some(x) => x,
            None => {
                warn!("notity subscription {} failed for {}", self.push_uri, message);
                return;
            }
        };

        let client = Client::new();
        let res = match client.post(&self.push_uri)
            .header(ContentEncoding(vec![Encoding::EncodingExt(String::from("aesgcm128"))]))
            .header(EncryptionKey(format!("keyid=p256dh;dh={}", enc.public_key)))
            .header(Encryption(format!("keyid=p256dh;salt={}", enc.salt)))
            .body(Body::BufBody(&enc.output, enc.output.len()))
            .send() {
                Ok(x) => x,
                Err(e) => { warn!("notify subscription {} failed: {:?}", self.push_uri, e); return; }
            };

        info!("notified subscription {} (status {:?})", self.push_uri, res.status);
    }

    #[cfg(not(feature = "webpush"))]
    fn notify(&self, _: &str) {
        warn!("discard notification for subscription {}, webpush disabled at build time", self.push_uri);
    }
}

pub struct WebPush<C> {
    controller: C,
    getter_resource_id: Id<Getter>,
    getter_subscription_id: Id<Getter>,
    setter_resource_id: Id<Setter>,
    setter_subscribe_id: Id<Setter>,
    setter_unsubscribe_id: Id<Setter>,
    setter_notify_id: Id<Setter>,
}

impl<C: Controller> WebPush<C> {
    pub fn id() -> Id<AdapterId> {
        Id::new("webpush@link.mozilla.org")
    }

    pub fn service_webpush_id() -> Id<ServiceId> {
        Id::new("service:webpush@link.mozilla.org")
    }

    pub fn getter_resource_id() -> Id<Getter> {
        Id::new("getter:resource.webpush@link.mozilla.org")
    }

    pub fn getter_subscription_id() -> Id<Getter> {
        Id::new("getter:subscription.webpush@link.mozilla.org")
    }

    pub fn setter_resource_id() -> Id<Setter> {
        Id::new("setter:resource.webpush@link.mozilla.org")
    }

    pub fn setter_subscribe_id() -> Id<Setter> {
        Id::new("setter:subscribe.webpush@link.mozilla.org")
    }

    pub fn setter_unsubscribe_id() -> Id<Setter> {
        Id::new("setter:unsubscribe.webpush@link.mozilla.org")
    }

    pub fn setter_notify_id() -> Id<Setter> {
        Id::new("setter:notify.webpush@link.mozilla.org")
    }
}

impl<C: Controller> Adapter for WebPush<C> {
    fn id(&self) -> Id<AdapterId> {
        Self::id()
    }

    fn name(&self) -> &str {
        ADAPTER_NAME
    }

    fn vendor(&self) -> &str {
        ADAPTER_VENDOR
    }

    fn version(&self) -> &[u32;4] {
        &ADAPTER_VERSION
    }

    fn fetch_values(&self, mut set: Vec<Id<Getter>>) -> ResultMap<Id<Getter>, Option<Value>, Error> {
        set.drain(..).map(|id| {
            let user_id = 1; // FIXME: currently logged in user

            macro_rules! getter_api {
                ($getter:ident, $getter_id:ident, $getter_type:ident) => (
                    if id == self.$getter_id {
                        match self.$getter(user_id) {
                            Ok(data) => {
                                let rsp = $getter_type::new(data);
                                return (id, Ok(Some(Value::Json(Arc::new(Json(serde_json::to_value(&rsp)))))));
                            },
                            Err(_) => return (id, Err(Error::InternalError(InternalError::InvalidInitialService)))
                        };
                    }
                )
            }

            getter_api!(get_subscriptions, getter_subscription_id, SubscriptionGetter);
            getter_api!(get_resources, getter_resource_id, ResourceGetter);
            (id.clone(), Err(Error::InternalError(InternalError::NoSuchGetter(id))))
        }).collect()
    }

    fn send_values(&self, mut values: HashMap<Id<Setter>, Value>) -> ResultMap<Id<Setter>, (), Error> {
        values.drain().map(|(id, value)| {
            let user_id = 1; // FIXME: currently logged in user

            let arc_json_value = match value {
                Value::Json(v) => v,
                _ => return (id, Err(Error::InternalError(InternalError::InvalidInitialService)))
            };
            let Json(ref json_value) = *arc_json_value;

            macro_rules! setter_api {
                ($setter:ident, $setter_id:ident, $setter_type:ident) => (
                    if id == self.$setter_id {
                        let data: Result<$setter_type, _> = serde_json::from_value(json_value.clone());
                        match data {
                            Ok(x) => {
                                self.$setter(user_id, &x).unwrap();
                                return (id, Ok(()));
                            }
                            Err(_) => return (id, Err(Error::InternalError(InternalError::InvalidInitialService)))
                        }
                    }
                )
            }

            setter_api!(set_resources, setter_resource_id, ResourceGetter);
            setter_api!(set_subscribe, setter_subscribe_id, SubscriptionGetter);
            setter_api!(set_unsubscribe, setter_unsubscribe_id, SubscriptionGetter);
            setter_api!(set_notify, setter_notify_id, NotifySetter);
            (id.clone(), Err(Error::InternalError(InternalError::NoSuchSetter(id))))
        }).collect()
    }

    fn register_watch(&self, mut watch: Vec<(Id<Getter>, Option<Range>)>,
        _: Box<ExtSender<WatchEvent>>) ->
           ResultMap<Id<Getter>, Box<AdapterWatchGuard>, Error>
    {
        watch.drain(..).map(|(id, _)| {
            (id.clone(), Err(Error::GetterDoesNotSupportWatching(id)))
        }).collect()
    }
}

impl<C: Controller> WebPush<C> {
    pub fn init<A: AdapterManagerHandle>(controller: C, adapt: &A) -> Result<(), Error> {
        let wp = Box::new(Self::new(controller));
        let id = WebPush::<C>::id();
        let service_id = WebPush::<C>::service_webpush_id();
        let getter_resource_id = wp.getter_resource_id.clone();
        let getter_subscription_id = wp.getter_subscription_id.clone();
        let setter_resource_id = wp.setter_resource_id.clone();
        let setter_subscribe_id = wp.setter_subscribe_id.clone();
        let setter_unsubscribe_id = wp.setter_unsubscribe_id.clone();
        let setter_notify_id = wp.setter_notify_id.clone();

        try!(adapt.add_adapter(wp));
        try!(adapt.add_service(Service::empty(service_id.clone(), id.clone())));

        macro_rules! add_getter {
            ($id:ident, $kind_id:expr) => (
                try!(adapt.add_getter(Channel {
                    tags: HashSet::new(),
                    adapter: id.clone(),
                    id: $id,
                    last_seen: None,
                    service: service_id.clone(),
                    mechanism: Getter {
                        kind: ChannelKind::Extension {
                            vendor: ADAPTER_VENDOR.to_owned(),
                            adapter: ADAPTER_NAME.to_owned(),
                            kind: $kind_id.to_owned(),
                            typ: Type::Json,
                        },
                        updated: None
                    }
                }));
            )
        }

        macro_rules! add_setter {
            ($id:ident, $kind_id:expr) => (
                try!(adapt.add_setter(Channel {
                    tags: HashSet::new(),
                    adapter: id.clone(),
                    id: $id,
                    last_seen: None,
                    service: service_id.clone(),
                    mechanism: Setter {
                        kind: ChannelKind::Extension {
                            vendor: ADAPTER_VENDOR.to_owned(),
                            adapter: ADAPTER_NAME.to_owned(),
                            kind: $kind_id.to_owned(),
                            typ: Type::Json,
                        },
                        updated: None
                    }
                }));
            )
        }

        add_getter!(getter_resource_id, "WebPushResource");
        add_getter!(getter_subscription_id, "WebPushSubscription");
        add_setter!(setter_resource_id, "WebPushResource");
        add_setter!(setter_subscribe_id, "WebPushSubscription");
        add_setter!(setter_unsubscribe_id, "WebPushSubscription");
        add_setter!(setter_notify_id, "WebPushNotify");
        Ok(())
    }

    fn new(controller: C) -> Self
    {
        WebPush {
            controller: controller,
            getter_resource_id: Self::getter_resource_id(),
            getter_subscription_id: Self::getter_subscription_id(),
            setter_resource_id: Self::setter_resource_id(),
            setter_subscribe_id: Self::setter_subscribe_id(),
            setter_unsubscribe_id: Self::setter_unsubscribe_id(),
            setter_notify_id: Self::setter_notify_id(),
        }
    }

    fn get_db(&self) -> db::WebPushDb {
        db::WebPushDb::new(&self.controller.get_profile().path_for("webpush.sqlite"))
    }

    fn set_subscribe(&self, user_id: i32, setter: &SubscriptionGetter) -> rusqlite::Result<()> {
        let db = self.get_db();
        for sub in &setter.subscriptions {
            try!(db.subscribe(user_id, sub));
        }
        Ok(())
    }

    fn set_unsubscribe(&self, user_id: i32, setter: &SubscriptionGetter) -> rusqlite::Result<()> {
        let db = self.get_db();
        for sub in &setter.subscriptions {
            try!(db.unsubscribe(user_id, &sub.push_uri));
        }
        Ok(())
    }

    fn set_resources(&self, user_id: i32, setter: &ResourceGetter) -> rusqlite::Result<()> {
        try!(self.get_db().set_resources(user_id, &setter.resources));
        Ok(())
    }

    fn get_resources(&self, user_id: i32) -> rusqlite::Result<Vec<String>> {
        self.get_db().get_resources(user_id)
    }

    fn get_subscriptions(&self, user_id: i32) -> rusqlite::Result<Vec<Subscription>> {
        self.get_db().get_subscriptions(user_id)
    }

    fn get_resource_subscriptions(&self, resource: &str) -> rusqlite::Result<Vec<Subscription>> {
        self.get_db().get_resource_subscriptions(resource)
    }

    fn set_notify(&self, _: i32, setter: &NotifySetter) -> rusqlite::Result<()> {
        info!("notify on resource {}: {}", setter.resource, setter.message);

        let json = json!({resource: setter.resource, message: setter.message});
        let subscriptions = try!(self.get_resource_subscriptions(&setter.resource));
        if subscriptions.is_empty() {
            debug!("no users listening on push resource");
        } else {
            thread::spawn(move || {
                for sub in subscriptions {
                    sub.notify(&json);
                }
            });
        }
        Ok(())
    }
}
<|endoftext|>


<commit_before>use reqwest;
pub use reqwest::header::ContentType;
use reqwest::Response;
pub use reqwest::{Method, StatusCode};
use result::{MwsError, MwsResult};
use sign::SignatureV2;
use std::io::Read;
use xmlhelper::decode::{FromXmlStream, Stream};
use SerializeMwsParams;

#[derive(Debug)]
pub struct ErrorResponse {
  pub status: StatusCode,
  pub info: Option<ErrorResponseInfo>,
  pub raw: String,
}

#[derive(Debug, Default)]
pub struct ErrorResponseInfo {
  pub errors: Vec<ErrorResponseError>,
  pub request_id: String,
}

#[derive(Debug, Default, PartialEq)]
pub struct ErrorResponseError {
  pub error_type: String,
  pub code: String,
  pub message: String,
  pub detail: String,
}

impl ErrorResponseInfo {
  fn from_xml_stream<R: ::std::io::Read>(s: &mut Stream<R>) -> MwsResult<ErrorResponseInfo> {
    use xmlhelper::decode::{characters, element, fold_elements, start_document};
    start_document(s)?;
    element(s, "ErrorResponse", |s| {
      fold_elements(s, ErrorResponseInfo::default(), |s, resp| {
        match s.local_name() {
          "Error" => {
            let err = fold_elements(s, ErrorResponseError::default(), |s, err| {
              match s.local_name() {
                "Type" => {
                  err.error_type = characters(s)?;
                }
                "Code" => {
                  err.code = characters(s)?;
                }
                "Message" => {
                  err.message = characters(s)?;
                }
                "Detail" => {
                  err.detail = characters(s)?;
                }
                _ => {}
              }
              Ok(())
            })?;
            resp.errors.push(err);
          }
          "RequestID" => {
            resp.request_id = characters(s)?;
          }
          _ => {}
        }
        Ok(())
      })
    })
    .into()
  }
}

impl FromXmlStream<Stream<reqwest::Response>> for ErrorResponseInfo {
  fn from_xml(s: &mut Stream<reqwest::Response>) -> MwsResult<ErrorResponseInfo> {
    ErrorResponseInfo::from_xml_stream(s)
  }
}

impl FromXmlStream<Stream<::std::io::Cursor<String>>> for ErrorResponseInfo {
  fn from_xml(s: &mut Stream<::std::io::Cursor<String>>) -> MwsResult<ErrorResponseInfo> {
    ErrorResponseInfo::from_xml_stream(s)
  }
}

/// [Reference](http://docs.developer.amazonservices.com/en_CA/dev_guide/DG_Endpoints.html)
pub struct ClientOptions {
  /// Your software can access Amazon Marketplace Web Service (Amazon MWS) using region-specific endpoints.
  pub endpoint: String,

  /// Your seller or merchant identifier.
  pub seller_id: String,

  /// Represents the authorization of a specific developer of a web application by a specific Amazon seller.
  pub mws_auth_token: Option<String>,

  /// Your Amazon MWS account is identified by your access key Id, which Amazon MWS uses to look up your Secret Access Key.
  pub aws_access_key_id: String,
  pub secret_key: String,
}

pub struct Client {
  options: ClientOptions,
  http_client: reqwest::Client,
}

impl Client {
  pub fn new(options: ClientOptions) -> MwsResult<Client> {
    Ok(Client {
      options: options,
      http_client: reqwest::Client::new(),
    })
  }

  pub fn with_http_client(options: ClientOptions, http_client: reqwest::Client) -> Client {
    Client {
      options: options,
      http_client: http_client,
    }
  }

  pub fn request<P>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
  ) -> MwsResult<reqwest::Response>
  where
    P: SerializeMwsParams,
  {
    let mut sign = SignatureV2::new(
      &self.options.endpoint,
      &self.options.aws_access_key_id,
      &self.options.secret_key,
      self.options.mws_auth_token.as_ref().map(AsRef::as_ref),
    );
    for (k, v) in parameters.into_mws_params() {
      sign.add(&k, v);
    }
    sign.add("SellerId", &self.options.seller_id);
    //sign.add("Merchant", self.options.seller_id.as_ref());
    let url = sign
      .generate_url(method.clone(), path, version, action)?
      .to_string();
    //println!("request: {}", url);
    self
      .http_client
      .request(method, &url)
      .send()
      .map_err(MwsError::from)
      .and_then(handle_error_status)
  }

  pub fn request_with_body<P, R>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
    body: R,
    content_md5: String,
    content_type: ContentType,
  ) -> MwsResult<reqwest::Response>
  where
    P: SerializeMwsParams,
    R: Read + Send + 'static,
  {
    let mut sign = SignatureV2::new(
      &self.options.endpoint,
      &self.options.aws_access_key_id,
      &self.options.secret_key,
      self.options.mws_auth_token.as_ref().map(AsRef::as_ref),
    );
    for (k, v) in parameters.into_mws_params() {
      sign.add(&k, v);
    }
    sign.add("SellerId", &self.options.seller_id);
    sign.add("ContentMD5Value", content_md5);
    //sign.add("Merchant", self.options.seller_id.as_ref());
    let url = sign
      .generate_url(method.clone(), path, version, action)?
      .to_string();
    //println!("request: {}", url);

    self
      .http_client
      .request(method, &url)
      .header(content_type)
      .body(reqwest::Body::new(body))
      .send()
      .map_err(MwsError::from)
      .and_then(handle_error_status)
  }

  pub fn request_xml<P, T>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
  ) -> MwsResult<T>
  where
    P: SerializeMwsParams,
    T: FromXmlStream<Stream<reqwest::Response>>,
  {
    let resp = self.request(method, path, version, action, parameters)?;
    let mut stream = Stream::new(resp);
    let v = T::from_xml(&mut stream)?;
    Ok(v)
  }

  pub fn request_xml_with_body<P, R, T>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
    body: R,
    content_md5: String,
    content_type: ContentType,
  ) -> MwsResult<T>
  where
    P: SerializeMwsParams,
    T: FromXmlStream<Stream<reqwest::Response>>,
    R: Read + Send + 'static,
  {
    let resp = self.request_with_body(
      method,
      path,
      version,
      action,
      parameters,
      body,
      content_md5,
      content_type,
    )?;
    let mut stream = Stream::new(resp);
    let v = T::from_xml(&mut stream)?;
    Ok(v)
  }

  pub fn request_raw<P>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
  ) -> MwsResult<(StatusCode, Vec<(String, String)>, Vec<u8>)>
  where
    P: SerializeMwsParams,
  {
    let mut sign = SignatureV2::new(
      &self.options.endpoint,
      &self.options.aws_access_key_id,
      &self.options.secret_key,
      self.options.mws_auth_token.as_ref().map(AsRef::as_ref),
    );
    for (k, v) in parameters.into_mws_params() {
      sign.add(&k, v);
    }
    let url = sign
      .generate_url(method.clone(), path, version, action)?
      .to_string();
    let mut resp = self
      .http_client
      .request(method, &url)
      .send()
      .map_err(MwsError::from)
      .and_then(handle_error_status)?;

    let headers = resp
      .headers()
      .iter()
      .map(|view| (view.name().to_string(), view.value_string()))
      .collect();

    let mut body = vec![];
    resp.read_to_end(&mut body)?;
    Ok((resp.status().clone(), headers, body))
  }
}

fn handle_error_status(resp: Response) -> MwsResult<Response> {
  if resp.status().is_success() {
    Ok(resp)
  } else {
    use std::io::Cursor;

    let mut resp = resp;
    let mut body = String::new();
    resp.read_to_string(&mut body)?;
    let mut s = Stream::new(Cursor::new(body.clone()));
    match ErrorResponseInfo::from_xml(&mut s) {
      Ok(info) => Err(MwsError::ErrorResponse(ErrorResponse {
        status: resp.status().clone(),
        raw: body,
        info: Some(info),
      })),
      Err(_) => Err(MwsError::ErrorResponse(ErrorResponse {
        status: resp.status().clone(),
        raw: body,
        info: None,
      })),
    }
  }
}

#[cfg(test)]
pub fn get_test_client() -> Client {
  use std::env;
  Client::new(ClientOptions {
    endpoint: env::var("Endpoint").expect("get Endpoint"),
    seller_id: env::var("SellerId").expect("get SellerId"),
    mws_auth_token: None,
    aws_access_key_id: env::var("AWSAccessKeyId").expect("get AWSAccessKeyId"),
    secret_key: env::var("SecretKey").expect("get SecretKey"),
  })
  .expect("create client")
}

#[cfg(test)]
mod tests {
  use super::*;
  use dotenv::dotenv;

  #[test]
  #[ignore]
  fn it_works() {
    dotenv().ok();
    let client = get_test_client();
    let (status, _, body) = client
      .request_raw(
        Method::Post,
        "/Orders/2013-09-01",
        "2013-09-01",
        "GetServiceStatus",
        (),
      )
      .expect("send request");
    let body = String::from_utf8(body).unwrap();
    assert!(status.is_success());
    assert!(body.starts_with("<?xml"));

    use std::io::Cursor;
    let (status, _, body) = client
      .request_raw(
        Method::Post,
        "/Fake/2013-09-01",
        "2013-09-01",
        "GetServiceStatus",
        (),
      )
      .expect("send request");
    let body = String::from_utf8(body).unwrap();
    assert!(!status.is_success());
    let source = Cursor::new(body);
    let mut s = Stream::new(source);
    let err_info = ErrorResponseInfo::from_xml(&mut s).expect("decode error response");
    assert_eq!(err_info.errors.len(), 1);
    assert_eq!(
      err_info.errors[0],
      ErrorResponseError {
        error_type: "Sender".to_string(),
        code: "InvalidAddress".to_string(),
        message: "Section Fake/2013-09-01 is invalid".to_string(),
        detail: "".to_string(),
      }
    );
  }
}
<commit_msg>fix: include SellerId in request_raw.<commit_after>use reqwest;
pub use reqwest::header::ContentType;
use reqwest::Response;
pub use reqwest::{Method, StatusCode};
use result::{MwsError, MwsResult};
use sign::SignatureV2;
use std::io::Read;
use xmlhelper::decode::{FromXmlStream, Stream};
use SerializeMwsParams;

#[derive(Debug)]
pub struct ErrorResponse {
  pub status: StatusCode,
  pub info: Option<ErrorResponseInfo>,
  pub raw: String,
}

#[derive(Debug, Default)]
pub struct ErrorResponseInfo {
  pub errors: Vec<ErrorResponseError>,
  pub request_id: String,
}

#[derive(Debug, Default, PartialEq)]
pub struct ErrorResponseError {
  pub error_type: String,
  pub code: String,
  pub message: String,
  pub detail: String,
}

impl ErrorResponseInfo {
  fn from_xml_stream<R: ::std::io::Read>(s: &mut Stream<R>) -> MwsResult<ErrorResponseInfo> {
    use xmlhelper::decode::{characters, element, fold_elements, start_document};
    start_document(s)?;
    element(s, "ErrorResponse", |s| {
      fold_elements(s, ErrorResponseInfo::default(), |s, resp| {
        match s.local_name() {
          "Error" => {
            let err = fold_elements(s, ErrorResponseError::default(), |s, err| {
              match s.local_name() {
                "Type" => {
                  err.error_type = characters(s)?;
                }
                "Code" => {
                  err.code = characters(s)?;
                }
                "Message" => {
                  err.message = characters(s)?;
                }
                "Detail" => {
                  err.detail = characters(s)?;
                }
                _ => {}
              }
              Ok(())
            })?;
            resp.errors.push(err);
          }
          "RequestID" => {
            resp.request_id = characters(s)?;
          }
          _ => {}
        }
        Ok(())
      })
    })
    .into()
  }
}

impl FromXmlStream<Stream<reqwest::Response>> for ErrorResponseInfo {
  fn from_xml(s: &mut Stream<reqwest::Response>) -> MwsResult<ErrorResponseInfo> {
    ErrorResponseInfo::from_xml_stream(s)
  }
}

impl FromXmlStream<Stream<::std::io::Cursor<String>>> for ErrorResponseInfo {
  fn from_xml(s: &mut Stream<::std::io::Cursor<String>>) -> MwsResult<ErrorResponseInfo> {
    ErrorResponseInfo::from_xml_stream(s)
  }
}

/// [Reference](http://docs.developer.amazonservices.com/en_CA/dev_guide/DG_Endpoints.html)
pub struct ClientOptions {
  /// Your software can access Amazon Marketplace Web Service (Amazon MWS) using region-specific endpoints.
  pub endpoint: String,

  /// Your seller or merchant identifier.
  pub seller_id: String,

  /// Represents the authorization of a specific developer of a web application by a specific Amazon seller.
  pub mws_auth_token: Option<String>,

  /// Your Amazon MWS account is identified by your access key Id, which Amazon MWS uses to look up your Secret Access Key.
  pub aws_access_key_id: String,
  pub secret_key: String,
}

pub struct Client {
  options: ClientOptions,
  http_client: reqwest::Client,
}

impl Client {
  pub fn new(options: ClientOptions) -> MwsResult<Client> {
    Ok(Client {
      options: options,
      http_client: reqwest::Client::new(),
    })
  }

  pub fn with_http_client(options: ClientOptions, http_client: reqwest::Client) -> Client {
    Client {
      options: options,
      http_client: http_client,
    }
  }

  pub fn request<P>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
  ) -> MwsResult<reqwest::Response>
  where
    P: SerializeMwsParams,
  {
    let mut sign = SignatureV2::new(
      &self.options.endpoint,
      &self.options.aws_access_key_id,
      &self.options.secret_key,
      self.options.mws_auth_token.as_ref().map(AsRef::as_ref),
    );
    for (k, v) in parameters.into_mws_params() {
      sign.add(&k, v);
    }
    sign.add("SellerId", &self.options.seller_id);
    //sign.add("Merchant", self.options.seller_id.as_ref());
    let url = sign
      .generate_url(method.clone(), path, version, action)?
      .to_string();
    //println!("request: {}", url);
    self
      .http_client
      .request(method, &url)
      .send()
      .map_err(MwsError::from)
      .and_then(handle_error_status)
  }

  pub fn request_with_body<P, R>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
    body: R,
    content_md5: String,
    content_type: ContentType,
  ) -> MwsResult<reqwest::Response>
  where
    P: SerializeMwsParams,
    R: Read + Send + 'static,
  {
    let mut sign = SignatureV2::new(
      &self.options.endpoint,
      &self.options.aws_access_key_id,
      &self.options.secret_key,
      self.options.mws_auth_token.as_ref().map(AsRef::as_ref),
    );
    for (k, v) in parameters.into_mws_params() {
      sign.add(&k, v);
    }
    sign.add("SellerId", &self.options.seller_id);
    sign.add("ContentMD5Value", content_md5);
    //sign.add("Merchant", self.options.seller_id.as_ref());
    let url = sign
      .generate_url(method.clone(), path, version, action)?
      .to_string();
    //println!("request: {}", url);

    self
      .http_client
      .request(method, &url)
      .header(content_type)
      .body(reqwest::Body::new(body))
      .send()
      .map_err(MwsError::from)
      .and_then(handle_error_status)
  }

  pub fn request_xml<P, T>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
  ) -> MwsResult<T>
  where
    P: SerializeMwsParams,
    T: FromXmlStream<Stream<reqwest::Response>>,
  {
    let resp = self.request(method, path, version, action, parameters)?;
    let mut stream = Stream::new(resp);
    let v = T::from_xml(&mut stream)?;
    Ok(v)
  }

  pub fn request_xml_with_body<P, R, T>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
    body: R,
    content_md5: String,
    content_type: ContentType,
  ) -> MwsResult<T>
  where
    P: SerializeMwsParams,
    T: FromXmlStream<Stream<reqwest::Response>>,
    R: Read + Send + 'static,
  {
    let resp = self.request_with_body(
      method,
      path,
      version,
      action,
      parameters,
      body,
      content_md5,
      content_type,
    )?;
    let mut stream = Stream::new(resp);
    let v = T::from_xml(&mut stream)?;
    Ok(v)
  }

  pub fn request_raw<P>(
    &self,
    method: Method,
    path: &str,
    version: &str,
    action: &str,
    parameters: P,
  ) -> MwsResult<(StatusCode, Vec<(String, String)>, Vec<u8>)>
  where
    P: SerializeMwsParams,
  {
    let mut sign = SignatureV2::new(
      &self.options.endpoint,
      &self.options.aws_access_key_id,
      &self.options.secret_key,
      self.options.mws_auth_token.as_ref().map(AsRef::as_ref),
    );
    for (k, v) in parameters.into_mws_params() {
      sign.add(&k, v);
    }
    sign.add("SellerId", &self.options.seller_id);
    let url = sign
      .generate_url(method.clone(), path, version, action)?
      .to_string();
    let mut resp = self
      .http_client
      .request(method, &url)
      .send()
      .map_err(MwsError::from)
      .and_then(handle_error_status)?;

    let headers = resp
      .headers()
      .iter()
      .map(|view| (view.name().to_string(), view.value_string()))
      .collect();

    let mut body = vec![];
    resp.read_to_end(&mut body)?;
    Ok((resp.status().clone(), headers, body))
  }
}

fn handle_error_status(resp: Response) -> MwsResult<Response> {
  if resp.status().is_success() {
    Ok(resp)
  } else {
    use std::io::Cursor;

    let mut resp = resp;
    let mut body = String::new();
    resp.read_to_string(&mut body)?;
    let mut s = Stream::new(Cursor::new(body.clone()));
    match ErrorResponseInfo::from_xml(&mut s) {
      Ok(info) => Err(MwsError::ErrorResponse(ErrorResponse {
        status: resp.status().clone(),
        raw: body,
        info: Some(info),
      })),
      Err(_) => Err(MwsError::ErrorResponse(ErrorResponse {
        status: resp.status().clone(),
        raw: body,
        info: None,
      })),
    }
  }
}

#[cfg(test)]
pub fn get_test_client() -> Client {
  use std::env;
  Client::new(ClientOptions {
    endpoint: env::var("Endpoint").expect("get Endpoint"),
    seller_id: env::var("SellerId").expect("get SellerId"),
    mws_auth_token: None,
    aws_access_key_id: env::var("AWSAccessKeyId").expect("get AWSAccessKeyId"),
    secret_key: env::var("SecretKey").expect("get SecretKey"),
  })
  .expect("create client")
}

#[cfg(test)]
mod tests {
  use super::*;
  use dotenv::dotenv;

  #[test]
  #[ignore]
  fn it_works() {
    dotenv().ok();
    let client = get_test_client();
    let (status, _, body) = client
      .request_raw(
        Method::Post,
        "/Orders/2013-09-01",
        "2013-09-01",
        "GetServiceStatus",
        (),
      )
      .expect("send request");
    let body = String::from_utf8(body).unwrap();
    assert!(status.is_success());
    assert!(body.starts_with("<?xml"));

    use std::io::Cursor;
    let (status, _, body) = client
      .request_raw(
        Method::Post,
        "/Fake/2013-09-01",
        "2013-09-01",
        "GetServiceStatus",
        (),
      )
      .expect("send request");
    let body = String::from_utf8(body).unwrap();
    assert!(!status.is_success());
    let source = Cursor::new(body);
    let mut s = Stream::new(source);
    let err_info = ErrorResponseInfo::from_xml(&mut s).expect("decode error response");
    assert_eq!(err_info.errors.len(), 1);
    assert_eq!(
      err_info.errors[0],
      ErrorResponseError {
        error_type: "Sender".to_string(),
        code: "InvalidAddress".to_string(),
        message: "Section Fake/2013-09-01 is invalid".to_string(),
        detail: "".to_string(),
      }
    );
  }
}
<|endoftext|>


<commit_before>// Copyright (c) The Libra Core Contributors
// SPDX-License-Identifier: Apache-2.0

/// This module provides an experiment which simulates a multi-region environment.
/// It undoes the simulation in the cluster after the given duration
use crate::effects::Effect;
use crate::effects::NetworkDelay;
use crate::prometheus::Prometheus;
use crate::tx_emitter::{EmitJobRequest, EmitThreadParams, TxEmitter};
use crate::util::unix_timestamp_now;
use crate::{cluster::Cluster, experiments::Experiment, thread_pool_executor::ThreadPoolExecutor};
use failure;
use slog_scope::{info, warn};
use std::{collections::HashSet, fmt, thread, time::Duration};

#[derive(Default, Debug)]
struct Metrics {
    split_size: usize,
    cross_region_latency: u64,
    txn_per_sec: Option<f64>,
    blocks_per_sec: Option<f64>,
    avg_vote_time: Option<f64>,
    avg_qc_time: Option<f64>,
}

pub struct MultiRegionSimulation {
    splits: Vec<usize>,
    cross_region_latencies: Vec<u64>,
    exp_duration_per_config: Duration,
    thread_pool_executor: ThreadPoolExecutor,
    cluster: Cluster,
    prometheus: Prometheus,
}

impl MultiRegionSimulation {
    pub fn new(
        splits: Vec<usize>,
        cross_region_latencies: Vec<u64>,
        exp_duration_per_config: Duration,
        cluster: Cluster,
        thread_pool_executor: ThreadPoolExecutor,
        prometheus: Prometheus,
    ) -> Self {
        Self {
            splits,
            cross_region_latencies,
            exp_duration_per_config,
            thread_pool_executor,
            cluster,
            prometheus,
        }
    }

    fn single_run(&self, count: usize, cross_region_delay: Duration) -> failure::Result<Metrics> {
        let (cluster1, cluster2) = self.cluster.split_n_random(count);
        let (region1, region2) = (cluster1.into_instances(), cluster2.into_instances());
        let (smaller_region, larger_region);
        if region1.len() < region2.len() {
            smaller_region = &region1;
            larger_region = &region2;
        } else {
            smaller_region = &region2;
            larger_region = &region1;
        }
        let network_delays_effects: Vec<_> = smaller_region
            .iter()
            .map(|instance| {
                NetworkDelay::new(
                    instance.clone(),
                    Some(larger_region.clone()),
                    cross_region_delay,
                )
            })
            .collect();
        // Add network delay commands only to the instances of the smaller
        // region because we have to run fewer ssh commands.
        let start_effects: Vec<_> = network_delays_effects
            .iter()
            .map(|effect| {
                move || {
                    effect.activate().unwrap();
                }
            })
            .collect();
        self.thread_pool_executor.execute_jobs(start_effects);

        thread::sleep(self.exp_duration_per_config);
        let metrics = self.get_metrics(count, cross_region_delay.as_millis() as u64);
        let stop_effects: Vec<_> = network_delays_effects
            .iter()
            .map(|effect| {
                move || {
                    effect.deactivate().unwrap();
                }
            })
            .collect();
        self.thread_pool_executor.execute_jobs(stop_effects);
        Ok(metrics)
    }

    fn get_metrics(&self, split_size: usize, cross_region_latency: u64) -> Metrics {
        let mut metrics: Metrics = Default::default();
        metrics.split_size = split_size;
        metrics.cross_region_latency = cross_region_latency;
        let step = 10;
        let end = unix_timestamp_now();
        // Measure metrics 30s after the experiment started to ignore initial warmup metrics
        let start = end - self.exp_duration_per_config + Duration::from_secs(30);
        metrics.txn_per_sec = self
            .prometheus
            .query_range_avg(
                "irate(consensus_gauge{op='last_committed_version'}[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok();
        metrics.blocks_per_sec = self
            .prometheus
            .query_range_avg(
                "irate(consensus_gauge{op='last_committed_round'}[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok();
        metrics.avg_vote_time = self
            .prometheus
            .query_range_avg(
                "irate(consensus_duration_sum{op='creation_to_receival_s'}[1m])/irate(consensus_duration_count{op='creation_to_receival_s'}[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok().map(|x| x * 1000f64);
        metrics.avg_qc_time = self
            .prometheus
            .query_range_avg(
                "irate(consensus_duration_sum{op='creation_to_qc_s'}[1m])/irate(consensus_duration_count{op='creation_to_qc_s'}[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok().map(|x| x * 1000f64);
        metrics
    }
}

fn print_results(metrics: Vec<Metrics>) {
    info!("Results:");
    println!(
        "split_size, cross_region_latency, txn_per_sec, blocks_per_sec, avg_vote_time, avg_qc_time"
    );
    let to_str = |x: &Option<f64>| -> String {
        if let Some(val) = x {
            format!("{:.2}", val)
        } else {
            "".to_string()
        }
    };
    for metric in metrics {
        println!(
            "{}, {}, {}, {}, {}, {}",
            metric.split_size,
            metric.cross_region_latency,
            to_str(&metric.txn_per_sec),
            to_str(&metric.blocks_per_sec),
            to_str(&metric.avg_vote_time),
            to_str(&metric.avg_qc_time),
        );
    }
}

impl Experiment for MultiRegionSimulation {
    fn affected_validators(&self) -> HashSet<String> {
        let mut r = HashSet::new();
        for instance in self.cluster.clone().into_instances() {
            r.insert(instance.short_hash().clone());
        }
        r
    }

    fn run(&self) -> failure::Result<()> {
        let mut emitter = TxEmitter::new(&self.cluster);
        let mut results = vec![];
        for split in &self.splits {
            for cross_region_latency in &self.cross_region_latencies {
                let job = emitter
                    .start_job(EmitJobRequest {
                        instances: self.cluster.instances().clone(),
                        accounts_per_client: 10,
                        thread_params: EmitThreadParams::default(),
                    })
                    .expect("Failed to start emit job");
                // Wait for minting to complete and transactions to start
                thread::sleep(Duration::from_secs(30));
                info!(
                    "Running multi region simulation: split: {}, cross region latency: {}ms",
                    split, cross_region_latency
                );
                if let Ok(metrics) = self
                    .single_run(*split, Duration::from_millis(*cross_region_latency))
                    .map_err(|e| warn!("{}", e))
                {
                    info!("metrics for this run: {:?}", metrics);
                    results.push(metrics);
                }
                emitter.stop_job(job);
                // Sleep for a minute before different experiments
                thread::sleep(Duration::from_secs(60));
            }
        }
        print_results(results);
        Ok(())
    }

    fn deadline(&self) -> Duration {
        Duration::from_secs(24 * 60 * 60)
    }
}

impl fmt::Display for MultiRegionSimulation {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "Multi Region Simulation splits: {:?} cross region latencies: {:?}",
            self.splits, self.cross_region_latencies
        )
    }
}
<commit_msg>[cluster-test] Fix metrics for multi-region-simulation<commit_after>// Copyright (c) The Libra Core Contributors
// SPDX-License-Identifier: Apache-2.0

/// This module provides an experiment which simulates a multi-region environment.
/// It undoes the simulation in the cluster after the given duration
use crate::effects::Effect;
use crate::effects::NetworkDelay;
use crate::prometheus::Prometheus;
use crate::tx_emitter::{EmitJobRequest, EmitThreadParams, TxEmitter};
use crate::util::unix_timestamp_now;
use crate::{cluster::Cluster, experiments::Experiment, thread_pool_executor::ThreadPoolExecutor};
use failure;
use slog_scope::{info, warn};
use std::{collections::HashSet, fmt, thread, time::Duration};

#[derive(Default, Debug)]
struct Metrics {
    split_size: usize,
    cross_region_latency: u64,
    txn_per_sec: Option<f64>,
    blocks_per_sec: Option<f64>,
    avg_vote_time: Option<f64>,
    avg_qc_time: Option<f64>,
}

pub struct MultiRegionSimulation {
    splits: Vec<usize>,
    cross_region_latencies: Vec<u64>,
    exp_duration_per_config: Duration,
    thread_pool_executor: ThreadPoolExecutor,
    cluster: Cluster,
    prometheus: Prometheus,
}

impl MultiRegionSimulation {
    pub fn new(
        splits: Vec<usize>,
        cross_region_latencies: Vec<u64>,
        exp_duration_per_config: Duration,
        cluster: Cluster,
        thread_pool_executor: ThreadPoolExecutor,
        prometheus: Prometheus,
    ) -> Self {
        Self {
            splits,
            cross_region_latencies,
            exp_duration_per_config,
            thread_pool_executor,
            cluster,
            prometheus,
        }
    }

    fn single_run(&self, count: usize, cross_region_delay: Duration) -> failure::Result<Metrics> {
        let (cluster1, cluster2) = self.cluster.split_n_random(count);
        let (region1, region2) = (cluster1.into_instances(), cluster2.into_instances());
        let (smaller_region, larger_region);
        if region1.len() < region2.len() {
            smaller_region = &region1;
            larger_region = &region2;
        } else {
            smaller_region = &region2;
            larger_region = &region1;
        }
        let network_delays_effects: Vec<_> = smaller_region
            .iter()
            .map(|instance| {
                NetworkDelay::new(
                    instance.clone(),
                    Some(larger_region.clone()),
                    cross_region_delay,
                )
            })
            .collect();
        // Add network delay commands only to the instances of the smaller
        // region because we have to run fewer ssh commands.
        let start_effects: Vec<_> = network_delays_effects
            .iter()
            .map(|effect| {
                move || {
                    effect.activate().unwrap();
                }
            })
            .collect();
        self.thread_pool_executor.execute_jobs(start_effects);

        thread::sleep(self.exp_duration_per_config);
        let metrics = self.get_metrics(count, cross_region_delay.as_millis() as u64);
        let stop_effects: Vec<_> = network_delays_effects
            .iter()
            .map(|effect| {
                move || {
                    effect.deactivate().unwrap();
                }
            })
            .collect();
        self.thread_pool_executor.execute_jobs(stop_effects);
        Ok(metrics)
    }

    fn get_metrics(&self, split_size: usize, cross_region_latency: u64) -> Metrics {
        let mut metrics: Metrics = Default::default();
        metrics.split_size = split_size;
        metrics.cross_region_latency = cross_region_latency;
        let step = 10;
        let end = unix_timestamp_now();
        // Measure metrics 30s after the experiment started to ignore initial warmup metrics
        let start = end - self.exp_duration_per_config + Duration::from_secs(30);
        metrics.txn_per_sec = self
            .prometheus
            .query_range_avg(
                "irate(libra_consensus_last_committed_version[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok();
        metrics.blocks_per_sec = self
            .prometheus
            .query_range_avg(
                "irate(libra_consensus_last_committed_round[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok();
        metrics.avg_vote_time = self
            .prometheus
            .query_range_avg(
                "irate(libra_consensus_creation_to_receival_s_sum[1m])/irate(libra_consensus_creation_to_receival_s_count[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok().map(|x| x * 1000f64);
        metrics.avg_qc_time = self
            .prometheus
            .query_range_avg(
                "irate(libra_consensus_creation_to_qc_s_sum[1m])/irate(libra_consensus_creation_to_qc_s_count[1m])".to_string(),
                &start,
                &end,
                step,
            )
            .map_err(|e| warn!("{}", e))
            .ok().map(|x| x * 1000f64);
        metrics
    }
}

fn print_results(metrics: Vec<Metrics>) {
    info!("Results:");
    println!(
        "split_size, cross_region_latency, txn_per_sec, blocks_per_sec, avg_vote_time, avg_qc_time"
    );
    let to_str = |x: &Option<f64>| -> String {
        if let Some(val) = x {
            format!("{:.2}", val)
        } else {
            "".to_string()
        }
    };
    for metric in metrics {
        println!(
            "{}, {}, {}, {}, {}, {}",
            metric.split_size,
            metric.cross_region_latency,
            to_str(&metric.txn_per_sec),
            to_str(&metric.blocks_per_sec),
            to_str(&metric.avg_vote_time),
            to_str(&metric.avg_qc_time),
        );
    }
}

impl Experiment for MultiRegionSimulation {
    fn affected_validators(&self) -> HashSet<String> {
        let mut r = HashSet::new();
        for instance in self.cluster.clone().into_instances() {
            r.insert(instance.short_hash().clone());
        }
        r
    }

    fn run(&self) -> failure::Result<()> {
        let mut emitter = TxEmitter::new(&self.cluster);
        let mut results = vec![];
        for split in &self.splits {
            for cross_region_latency in &self.cross_region_latencies {
                let job = emitter
                    .start_job(EmitJobRequest {
                        instances: self.cluster.instances().clone(),
                        accounts_per_client: 10,
                        thread_params: EmitThreadParams::default(),
                    })
                    .expect("Failed to start emit job");
                // Wait for minting to complete and transactions to start
                thread::sleep(Duration::from_secs(30));
                info!(
                    "Running multi region simulation: split: {}, cross region latency: {}ms",
                    split, cross_region_latency
                );
                if let Ok(metrics) = self
                    .single_run(*split, Duration::from_millis(*cross_region_latency))
                    .map_err(|e| warn!("{}", e))
                {
                    info!("metrics for this run: {:?}", metrics);
                    results.push(metrics);
                }
                emitter.stop_job(job);
                // Sleep for a minute before different experiments
                thread::sleep(Duration::from_secs(60));
            }
        }
        print_results(results);
        Ok(())
    }

    fn deadline(&self) -> Duration {
        Duration::from_secs(24 * 60 * 60)
    }
}

impl fmt::Display for MultiRegionSimulation {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "Multi Region Simulation splits: {:?} cross region latencies: {:?}",
            self.splits, self.cross_region_latencies
        )
    }
}
<|endoftext|>