...

суббота, 14 мая 2016 г.

Qook: Портировать старую игрушку на Android и поделиться ей с миром

КДПВ

На самом деле, я очень люблю логические игрушки. Не, «три в ряд», «найди похожий» и прочие «покорми собачку» меня мало интересуют. А вот по-настоящему сложная штуковина может спокойно утянуть на пару недель. Примерно так и случилось со мной в 2004-м году, когда ко мне в руки попал новенький мобильник от Sony. Способность этого T68I отлично звонить, показывать цветные картинки и, по слухам, даже отсылать свои контакты по BT прошли мимо меня незамеченными. А вот Q – нет. И сколько часов я просидел за маленьким дисплеем, судорожно гоняя шарики туда-сюда я уже и не помню. Зато, прекрасно помню, что, идея написать порт этой игры для какой-нибудь из современных платформ меня не отпускала со времен своего самого первого Hello World. Правда, все пои попытки склепать хоть какой-то игровой движок в те старые-добрые времена разбивались о… в общем обо что-то они разбивались. Зато теперь я давно и прочно пишу на Java, а с некоторых (совсем недавних) пор еще и для Android, так что идея порта игрушки наконец-то нашла возможность быть реализованной. Хотите посмотреть, что оно есть и как оно получилось? Тогда – под кат.

А в чем смысл игры?


Q – очень сложная логическая игра, смысл которой в том, чтобы закатить все цветные шарики на игровом поле в лунки того же цвета. Почти как в биллиарде, ага. И совсем как в биллиарде, шарики двигаются только по прямой и только до первого препятствия, которым может быть либо неподвижный кирпич, либо другой шарик. При этом все уровни построены так, чтобы исключить самые простые решения – в этом-то и прелесть.

Все равно не поняли? Ладно, вот вам картинка первого уровня. Придумайте, кстати, на досуге как этот самый уровень пройти.

Так лучше? Тогда давайте еще поймем, как бы это написать

На чем пишем?


Первым желанием, которое пришло мне в голову, было использовать какой-нибудь физический движок. Ну, Unity, например. Правда, после того как я посмотрел, сколько весят такие игрушки и сколько они жрут батарейки – идея использовать целый движок только для того, чтобы красиво катать шарики по полю умерла незамедлительно. Зато появилась идея написать свой собственный маленький движок специально для этой игры, тем более, что именно эта часть у меня и не получалась в детстве. Так что будем изобретать свой велосипед: с шариками и низким энергопотреблением. Кстати, изобретать его мы будем на Java, раз уж Android. Поехали?

Выделяем игровые элементы


Это первое, что нужно сделать при написании кода чего бы то ни было. Давайте посмотрим, что у нас есть… так, посмотрим на картинку еще раз…

Ага! На поле у нас есть… элементы! Логично?

public abstract class Item implements Serializable {
    private Color color;

    public Item(Color color) {
        setColor(color);
    }

    public Color getColor() {
        return color;
    }

    private void setColor(Color color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Item{" +
                "color=" + color +
                '}';
    }
}


Что, где координаты? А откуда шарик знает о том, где он там находится? Не его дело.

Теперь спустимся поглубже и посмотрим какие именно элементы у нас тут есть

Блок. С ним все просто: он квадратный, серый и никуда не двигается. Ни дать, ни взять – экономика какой-нибудь не сильно развитой страны. Правда, надо не забыть, что блок – это элемент.

public class Block extends Item {
    public Block() {
        super(Color.GRAY);
    }
}


Шарик. С шариком немного сложнее: он круглый, разноцветный и все время куда-то катается. И тоже элемент.
public class Ball extends Item {
    public Ball(Color color) {
        super(color);
    }
}


Дырка. Ну, или луза – как вам больше нравится. Она у нас что-то среднее между шариком и блоком: вроде бы квадратная и неподвижная, но тоже разноцветная.
public class Hole extends Item {
    public Hole(Color color) {
        super(color);
    }
}


Так, с базовыми элементами мы уже разобрались. Теперь, подумаем о том, где они все лежат

Пишем уровень


С самими уровнями мы будем разбираться чуть позже, а пока нам нужен класс, который будет отвечать за расположение элементов внутри поля, раз уж мы с вами решили, что сами они о себе ничего, кроме цвета и названия не знают.
public class Level implements Serializable {
    private Item[][] field;
    private int ballsCount;

    public Level(Item[][] field) {
        this.field = field;
    }
}


Ну вот, начало хорошее. У нас есть какой-то уровень, который хранит в себе какой-то двумерный массив из элементов. Поскольку все шарики-блоки-дырки у нас и есть элементы, так можно. Вторая переменная нам нужна для того, чтобы не подсчитывать количество оставшихся на поле шариков каждый раз. Впрочем, один раз это дело посчитать по-честному таки придется
private int countBallsOnLevel(Item[][] field) {
    int ballsCount = 0;

    for (Item[] aField : field) {
        for (int j = 0; j < field[0].length; j++) {
            if (aField[j] != null && aField[j].getClass().equals(Ball.class)) {
                ballsCount++;
            }
        }
    }

    return ballsCount;
}


Квадратичная сложность, ага. Именно поэтому я и не хочу пересчитывать это значение после очередного хода. Ну и добавим одну строчку в конструктор
this.ballsCount = countBallsOnLevel(field);


Так, уровень у нас готов. Теперь по плану самое интересное

Пишем движок


Пусть всей игровой механикой у нас занимается отдельный специальный класс. Ну, например, Field, который будет хранить в себе изменяемую конфигурацию уровня, а также количество оставшихся на поле шариков
private Level level;
private int ballsCount;

public Field(Level level) {
    this.level = level;
    this.ballsCount = level.getBallsCount();
}


Отлично. Теперь ненадолго отвлечемся от движка и напишем небольшой enum
public enum Direction {
    LEFT,
    RIGHT,
    UP,
    DOWN,
    NOWHERE
}


Ага, направление перемещения шарика. Теперь отвлечемся еще раз и напишем совсем маленький классик, который будет хранить в себе координаты нужного элемента на поле. Зачем? А чтобы потом писать меньше
public class Coordinates {
    private int horizontal;
    private int vertical;

    public Coordinates(int horizontal, int vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }
}


Ура, наконец-то можно вернуться обратно к движку и продолжить наш непосильный труд.

Первое, что хочется сделать – так это научить наше поле перемещать шарики.

private Coordinates moveRight(int xCoord, int yCoord) {
    try {
        while (level.getField()[yCoord][xCoord + 1] == null) {
            level.getField()[yCoord][xCoord + 1] = level.getField()[yCoord][xCoord];
            level.getField()[yCoord][xCoord++] = null;
        }
    } catch (ArrayIndexOutOfBoundsException ex) {
    }

    return new Coordinates(xCoord, yCoord);
}


Вот этот метод, например, будет катить шарик до тех пор, пока не встретится какое-нибудь приличное препятствие. Ну, или пока поле не закончится. Кстати, это, пожалуй, единственный случай, когда подавление исключений вообще хоть как-то оправдано.

Не сложнее, чем этот – пишутся и остальные методы для перемещения шарика влево, вверх и вниз. Надо только научиться вызывать эти методы где-нибудь уровнем выше.

private Coordinates moveItem(Coordinates coordinates, Direction direction) {

    int horizontal = coordinates.getHorizontal();
    int vertical = coordinates.getVertical();

    if (direction.equals(Direction.NOWHERE) || level.getField()[vertical][horizontal] == null) {
        return null;
    }

    Class clazz = level.getField()[vertical][horizontal].getClass();
    if (!clazz.equals(Ball.class)) {
        return null;
    }

    switch (direction) {
        case RIGHT:
            return moveRight(horizontal, vertical);

        case LEFT:
            return moveLeft(horizontal, vertical);

        case UP:
            return moveUp(horizontal, vertical);

        case DOWN:
            return moveDown(horizontal, vertical);
    }

    return null;
}


Ну вот и наши координаты пригодились. Я же говорил, что так меньше писать.

Так, кататься более-менее научились. Теперь будем учиться закатываться. Все то же самое, только метод у нас будет еще и возвращать результат операции – получилось скушать шарик или нет

private boolean acceptRight(Coordinates coordinates) {
    try {
        int horizontal = coordinates.getHorizontal();
        int vertical = coordinates.getVertical();

        Item upItem = level.getField()[vertical][horizontal + 1];
        Item item = level.getField()[vertical][horizontal];

        if (upItem == null || !upItem.getClass().equals(Hole.class) || !(upItem.getColor().equals(item.getColor()))) {
            return false;
        }

        level.getField()[vertical][horizontal] = null;
    } catch (ArrayIndexOutOfBoundsException ex) {
    }

    return true;
}


И точно такая же обертка уровнем выше
private boolean acceptHole(Coordinates coordinates, Direction direction) {
    boolean isAccepted = false;
    switch (direction) {
        case UP:
            isAccepted = acceptUp(coordinates);
            break;

        case DOWN:
            isAccepted = acceptDown(coordinates);
            break;

        case RIGHT:
            isAccepted = acceptRight(coordinates);
            break;

        case LEFT:
            isAccepted = acceptLeft(coordinates);
            break;
    }

    if (!isAccepted) {
        return false;
    }

    catchBall();

    return checkWin();

}


После того, как шарик получилось скушать, нужно еще пересчитать количество оставшихся. Нет, там не O(N).
private void catchBall() {
    ballsCount--;
}


Почему? Потому что за один ход мы можем перемещать только один шарик, а значит – и закатить больше у нас не получится. Проверка на то, что уровень окончен делается не сложнее
private boolean checkWin() {
    return ballsCount == 0;
}


Ну вот, теперь мы можем катать и закатывать шарики по полю. Осталось научиться ходить
public boolean makeTurn(Coordinates coordinates, Direction direction) {
    Coordinates newCoordinates = moveItem(coordinates, direction);
    return newCoordinates != null && acceptHole(newCoordinates, direction);
}


Ничего нового: приняли координаты с направлением, если получилось, переместили шарик на новое место и загнали его в дырку, если она там нашлась. Если нашлась – вернули true.

Ну вот и весь движок. И стоило из-за этого цеплять сюда какой-то unity?

Надо теперь только научить телефон все это дело показывать на экране.

Пишем свою вьюшку


Основной элемент интерфейса приложения для Android – View. Вьюшка, то есть. Это и кнопочка, и поле для ввода и… наше игровое поле. Правда, странно надеяться, что за нас его уже кто-то написал. Так что придется сделать это самим. Для этого мы создадим целый класс и отнаследуем его от встроенного View андройда, чтобы получить доступ к его жизненному циклу, возможности размещать это дело на экране и еще много чему
public class FieldView extends View {
    private final double ROUND_RECT_SIZE = 0.15;
    private final int PADDING_DIVIDER = 4;
    int paddingSize = 0;
    private int elementSize;
    private Field field;
    private Size fieldSize;
    private Size maxViewSize;

    public FieldView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

Зачем тут нужны константы, мы разберемся потом, а пока подумаем о том, какого размера должна быть вьюшка. Понятно, что она должна занимать как можно больше места на экране, но не вылезать за его пределы. И понятно, что размер элементов должен быть пропорционален размеру самой вьюхи. При этом задать что-то константно мы не можем – не писать же свою вьюшку под пару тысяч различных телефонов. Зато мы можем что-нибудь сделать с вьюшкой при размещении ее на экране. Поскольку в XML-разметке она у нас будет иметь размерности math_parent, то этот самый размер мы сможем определить runtime.

public Size countFieldSize() {
    if (maxViewSize == null) {
        maxViewSize = new Size(this.getWidth(), this.getHeight());
    }

    int horizontalElementsNum = field.getField()[0].length;
    int verticalElementsNum = field.getField().length;

    int maxHorizontalElSize = maxViewSize.getWidth() / horizontalElementsNum;
    int maxVerticalElSize = maxViewSize.getHeight() / verticalElementsNum;

    this.elementSize = (maxHorizontalElSize < maxVerticalElSize) ? maxHorizontalElSize : maxVerticalElSize;

    int newWidth = this.elementSize * horizontalElementsNum;
    int newHeight = this.elementSize * verticalElementsNum;

    return new Size(newWidth, newHeight);
}


Size у нас это примерно то же, что и координаты, только нужен для хранения размеров по Ox и Oy. Алгоритм простой: посмотрели, не определил ли эти размеры кто-нибудь до нас, получили высоту и ширину в пикселях, прикинули сколько будет занимать один элемент по горизонтали и по вертикали, выбрали меньший, да и пересчитали размер самой вьюхи домножив размер элемента на их количество по строке и столбцу.

А, ну и не забыть вызвать это дело:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    Size countedFieldSize = countFieldSize();
    if (fieldSize == null || !fieldSize.equals(countedFieldSize)) {
        this.fieldSize = countedFieldSize;
        setFieldSize(this.fieldSize);
        paddingSize = (int) (Math.sqrt(elementSize) / PADDING_DIVIDER);
    }

}


Что делает setFieldSize? Да пожалуйста!
public void setFieldSize(Size size) {
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(size.getWidth(), size.getHeight());
    params.gravity = Gravity.CENTER_HORIZONTAL;
    this.setLayoutParams(params);
}


Взяли вьюшку, да и прицепили к ней размеры. А что вы хотели?

Так, с размерами мы определились. Теперь нам надо как-то отрисовать игровое поле. Это не сложно и делается в onDraw. Правда, прежде чем что-то рисовать надо бы где-то найти сами игровые элементы.

Рисуем


Первое, что мне пришло в голову – завести целую кучу файлов разметки в drawable и подсовывать их на canvas по координатам. К великому несчастью, эта гениальная идея сломалась о невозможность задавать относительные размеры элементов. То есть, я могу сделать у блока скругленные углы и задать их в dp. И они на самом деле будут скругленными. Проблема только в том, что размер элемента у нас изменяется в зависимости от количества этих самых элементов на поле. И если поле у нас 6*6 (минимальный размер в игре), блоки будут квадратными со слегка скругленными углами. А если поле у нас аж 13*13 (максимальный размер) – это будут слегка квадратные шарики. Некрасиво.
Однако, сама идея рисовать на canvas готовыми элементами мне нравится больше, чем заморачиваться с какой-то низкоуровневой рисовкой, вроде drawRect. Давайте наделаем кучу элементов?

Генерацией Drawable у нас будет заниматься отдельный метод (хотя, мне почему-то хотелось вынести это в отдельную фабрику) selectDrawable, который принимает экземпляр элемента, выясняет кто он такой и делает для него drawable. Например, блок будет рисоваться примерно так:

Class clazz = item.getClass();
Color color = item.getColor();

if (clazz.equals(Block.class)) {
    GradientDrawable bgShape = new GradientDrawable();
    bgShape.setColor(ContextCompat.getColor(getContext(), R.color.gray));
    bgShape.setCornerRadius((float) (elementSize * ROUND_RECT_SIZE));
    return bgShape;
}


Ну вот и константы пригодились. Теперь радиус скругления у нас зависит от размера самого элемента. Как раз то, чего мы и добивались.

Теперь посмотрим на то, как строится drawable для шарика, который у нас разноцветный:

if (clazz.equals(Ball.class)) {
    GradientDrawable bgShape = new GradientDrawable();
    bgShape.setColor(ContextCompat.getColor(getContext(), R.color.gray));
    bgShape.setCornerRadius(elementSize);

    switch (color) {
        case GREEN:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.green));
            return bgShape;

        case RED:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.red));
            return bgShape;

        case BLUE:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.blue));
            return bgShape;

        case YELLOW:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.yellow));
            return bgShape;

        case PURPLE:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.purple));
            return bgShape;

        case CYAN:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.cyan));
            return bgShape;

    }
}


Да ненамного сложнее. Сначала нарисовали шарик, а потом полили его нужной краской. Зачем здесь switch и почему нельзя просто задать цвет тем, что мы достали из шарика?
Потому что это разные цвета. Цвет, который хранится в элементе, это обычный enum, который из Java, а то, что принимает drawable в качестве цвета – нормальный android-ресурс с нормальным строковым значением. Например, вот вам красненький:
<color name="red">#D81B60</color>


Склеивать одно с другим плохая идея, потому что когда-нибудь мне придет в голову, что красный – недостаточно синий и вообще пора поиграться со шрифтами и придется все это дело переписывать вместо того, чтобы просто исправить ресурсный файл.

Ну и на закуску – строим drawable из дырки:

if (clazz.equals(Hole.class)) {
    GradientDrawable bgShape = new GradientDrawable();
    bgShape.setCornerRadius((float) (elementSize * ROUND_RECT_SIZE));

    switch (color) {
        case GREEN:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.green));
            return bgShape;

        case RED:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.red));
            return bgShape;

        case BLUE:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.blue));
            return bgShape;

        case YELLOW:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.yellow));
            return bgShape;

        case PURPLE:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.purple));
            return bgShape;

        case CYAN:
            bgShape.setColor(ContextCompat.getColor(getContext(), R.color.cyan));
            return bgShape;
    }
}


Опять ничего нового: нарисовали дырку, покрасили и отдали ее просителю

Так, ничего не забыли? Хм… Дырки, шарики, блоки… А пустое место? Что, например, будет, если в массиве встретился null?

if (item == null) {
    GradientDrawable bgShape = new GradientDrawable();
    bgShape.setColor(ContextCompat.getColor(getContext(), android.R.color.transparent));
    bgShape.setCornerRadius((float) (elementSize * ROUND_RECT_SIZE));
    return bgShape;
}


Да ничего нового не будет, потому что это точно такой же красивый скругленный квадратик. Жалко, только что невидимый.

Готово, элементы мы строить умеем. На чем мы там остановились? А… да! На том, чтобы их отрисовать

@Override
protected void onDraw(Canvas canvas) {
    if (field == null) {
        return;
    }

    for (int i = 0; i < field.getField().length; i++) {
        for (int j = 0; j < field.getField()[0].length; j++) {
            Drawable d = selectDrawable(field.getField()[i][j]);
            d.setBounds(j * elementSize + paddingSize, i * elementSize + paddingSize, (j + 1) * elementSize - paddingSize, (i + 1) * elementSize - paddingSize);
            d.draw(canvas);
        }
    }
}

Ну вот. Прошлись по всему полю, для каждого элемента найдем его графическое представление, установили ему размеры и отступы друг от друга и отрисовали его на холсте. Кстати, интересно, что здесь именно drawable рисует на холсте, а не холст рисует на себе drawable. Для того, чтобы это сделать, пришлось бы каждый раз конвертить drawable в bitmap, а это долго.

Давайте посмотрим на то, что получилось? Для этого напишем какой-нибудь тестовый уровень, где элементы заданы прямо в конструкторе (уберем-уберем, не переживайте)

А вот так писать лучше не надо
public class Level {
    private Item[][] field;

    public Item[][] getField() {
        return field;
    }

    public Level() {
        field = new Item[6][6];
        field[0][0] = new Block();
        field[0][1] = new Block();
        field[0][2] = new Hole(Color.RED);
        field[0][3] = new Block();
        field[0][4] = new Block();
        field[0][5] = new Block();

        field[1][0] = new Block();
        field[1][1] = new Ball(Color.RED);
        field[1][2] = new Ball(Color.GREEN);
        field[1][3] = new Ball(Color.YELLOW);
        field[1][4] = new Ball(Color.CYAN);
        field[1][5] = new Block();

        field[2][0] = new Block();
        field[2][1] = new Hole(Color.GREEN);
        field[2][2] = new Hole(Color.YELLOW);
        field[2][3] = new Hole(Color.PURPLE);
        field[2][4] = new Hole(Color.CYAN);
        field[2][5] = new Hole(Color.BLUE);

        field[3][0] = new Block();
        field[3][1] = new Ball(Color.PURPLE);
        field[3][5] = new Block();

        field[4][0] = new Block();
        field[4][1] = new Block();
        field[4][3] = new Ball(Color.BLUE);
        field[4][5] = new Block();

        field[5][1] = new Block();
        field[5][2] = new Block();
        field[5][3] = new Block();
        field[5][4] = new Block();
    }
}



А теперь прицепим нашу вьюшку к какому-нибудь активити и запустим это дело

Наконец-то оно что-то показывает!

А теперь вдохновленные такой красивой картинкой научим нашу вьюшку интерактивности

Гоняем шары


Поскольку движок, который умеет перемещать элементы, у нас уже есть, нам остается только найти способ вызывать соответствующие методы как-то взаимодействуя с вьюхой.

Взаимодействовать с игровым полем можно по-разному. Если пользователей совсем не жалко, можно даже сделать управление таким же, как в оригинальной игре – приделать виртуальный джойстик и нажимать на него до посинения. А можно вспомнить о том, что нативный жест для сенсорного экрана, это все-таки свайп и смахивать шары в нужную сторону. Поняли, что мы собрались сделать? Тогда поехали

Вообще, для Android есть встроенный GestureManager, но то ли я так и не понял, как им пользоваться, то ли на моем тестовом девайсе он работает как попало, но запустить его так, чтобы не было ошибок распознавания нигде моими кривыми ручками почему-то не вышло. Так что сейчас возьмем и напишем свой собственный

Итак, двигаться наши с вами шарики могут ровно в четырех направлениях: вверх, вниз, влево и вправо. Правда, кроме этого они могут вообще никуда не двигаться, но это совсем не интересно. Так что для того, чтобы определять направление движения шарика, распознавать нам нужно всего 4 простых жеста.

Не особо заморачиваясь, начинаем писать еще один метод:

public Direction getSwipeDirection(float downHorizontal, float upHorizontal, float downVertical, float upVertical) {
    float xDistance = Math.abs(upHorizontal - downHorizontal);
    float yDistance = Math.abs(upVertical - downVertical);
    double swipeLength = getSwipeLength(xDistance, yDistance);

    if (swipeLength < elementSize / 2) {
        return Direction.NOWHERE;
    }

    if (xDistance >= yDistance) {
        if (upHorizontal > downHorizontal) {
            return Direction.RIGHT;
        }
        return Direction.LEFT;
    }

    if (yDistance > xDistance) {
        if (upVertical > downVertical) {
            return Direction.DOWN;
        }
        return Direction.UP;
    }

    return Direction.DOWN;
}


Direction – это Enum, который мы описали выше, а все остальное совсем просто: получили 4 координаты (откуда мы их получили пока не важно) и посчитали расстояние по вертикали и горизонтали. Потом вспомнили курс геометрии из средней школы и нашли длину самого свайпа. Если она совсем маленькая, подумаем, что юзер тут ни при чем и ничего делать не будем. Если же свайп был хороший, определим куда он такой хороший был и вернем пользователю направление. Классно? Мне тоже нравится.

Ну, допустим, направление свайпа мы определять с горем пополам научились. А какой из шариков мы, простите, свайпнули? Давайте разбираться.

Так, у нас есть координаты точки касания (еще у нас есть координаты точки отрыва, но что мы будем с ними делать?) и по этим координатам нам нужно найти элемент… Хм.

public Coordinates getElementCoordinates(float horizontal, float vertical) {
    float xElCoordinate = horizontal / elementSize;
    float yElCoordinate = vertical / elementSize;

    return new Coordinates((int) xElCoordinate, (int) yElCoordinate);
}


Ничего необычного. Если все элементы одного и того же размера, размер которого мы знаем (сами определяли), а размер поля мы и без того уже посчитали – остается только взять и поделить. А работать с элементом по его координатам – задача движка

Теперь мы точно знаем, что мы свайпнули и даже догадываемся куда. Остается только передать все это дело движку и пусть себе тарахтит. Вот только это уже не задача вьюшки. Ее дело – показывать, а, обрабатывать какие-то действия надо бы либо во фрагментах, либо в активити. С фрагментами у нас негусто, а вот какое-никакое активити есть. Повесим на вьюшку onTouchLictener.

private OnTouchListener onFieldTouchListener = new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downHorizontal = event.getX();
                downVertical = event.getY();
                break;

            case MotionEvent.ACTION_UP:
                upHorizontal = event.getX();
                upVertical = event.getY();

                boolean isWin = fieldView.getField().makeTurn(
                        fieldView.getElementCoordinates(downHorizontal, downVertical),
                        fieldView.getSwipeDirection(downHorizontal, upHorizontal, downVertical, upVertical)
                );
}


Ну вот. При касании будем сохранять координаты, при отпускании дисплея будем получать еще пару координат, а потом — собирать все в кучу и передавать вьюшке – пусть сама разбирается. Вьюшка отдаст все это дело дальше, достанет булевый результат, который ни много ни мало, а признак завершения уровня и вернет нам. Остается его только обработать ну и не забыть сказать вьюшке, что надо перерисоваться.

Дописываем в листенер:

fieldView.invalidate();

        if (isWin) {
            animateView(fieldView);

            try {
                levelManager.finishLevel();
                openLevel(levelManager.getCurrentLevelNumber());
              
            } catch (GameException ex) {
                onMenuClick();
            }
        }
}
return true;


Проверили и даже что-то сделали. Что именно мы сделали, поймем чуть позже, а пока давайте поиграемся. Уберем из нашего Level все лишнее, оставим только два шарика и попробуем загнать один из них в лузу

Хе-хе. Оно даже работает. Тогда двигаемся дальше

Ищем уровни


Ну вот. Рабочий движок мы написали, красиво это рисовать научились, теперь осталось только определиться с тем, что именно мы научились рисовать. С уровнями, ага.

Изначально, я не собирался рисовать уровни самостоятельно, потому что наша с вами цель – портировать игру, а не написать новую. Стало быть, надо где-то взять оригинальную Q, хорошенько ее встряхнуть, отцепить от нее трех-четырех юристов Sony (ну, это все-таки ее игрушка) и вуаля – готово.

Вот только поиски оригинальной игрушки ничем не закончились. Того самого T68 давно уж нет, картинок в сети почему-то не оказалось, а найти еще один оригинальный девайс в 2016-м году… проблематично найти, в общем. Трагедия.

Правда, пока я искал оригинал, я совершенно случайно наткнулся на порт этой игрушки под Windows 98. И каким же великим было мое удивление, когда я понял, что уровни к этой игрушке не только содраны с оригинала, но и лежат в обычных txt файлах, где каждый символ обозначает либо блок, либо шарик, либо дырку. Пообщавшись с автором и заручившись его согласием на использование, я со спокойной совестью забрал их себе и даже попробовал понять, что с ними делать.

Просто так, взять и засунуть txt-файлы в игрушку конечно можно, кто бы был против. Но заставить мой движок с ними работать – надо еще постараться. Надо так надо. Постараемся

Пишем Level Manager


Звучит красиво, на практике – еще один класс, который будет делать из txt-файлов нормальные объекты уровней и отдавать их кому скажут. А заодно будет разбираться с тем, какой уровень у нас текущий, сколько их всего есть и прочими прелестями жизни.

Поскольку менеджер у нас хранит в себе какое-то состояние (например, номер текущего уровня), неприятно будет, если в какой-то момент, мы сдуру сделаем себе новый, в котором этого самого состояния еще не будет (особенно неприятно будет старому менеджеру, которого система благополучно пристрелит). Так что сделаем его лучше синглтоном, от греха подальше

public class  LevelManager {
    private static final String LEVELS_FOLDER = "levels";
    private static final String LEVEL_FILE_EXTENSION = ".lev";

    private static final int EMPTY_CELL = 0;
    private static final int BLOCK_CELL = 1;
    private static final int GREEN_BALL_CELL = 2;
    private static final int RED_BALL_CELL = 3;
    private static final int BLUE_BALL_CELL = 4;
    private static final int YELLOW_BALL_CELL = 5;
    private static final int PURPLE_BALL_CELL = 6;
    private static final int CYAN_BALL_CELL = 7;

    private static final int GREEN_HOLE_CELL = 22;
    private static final int RED_HOLE_CELL = 33;
    private static final int BLUE_HOLE_CELL = 44;
    private static final int YELLOW_HOLE_CELL = 55;
    private static final int PURPLE_HOLE_CELL = 66;
    private static final int CYAN_HOLE_CELL = 77;

    private static Context context;
    private static SharedSettingsManager sharedSettingsManager;
    private static LevelManager instance;

    private LevelManager() {
    }

    public static LevelManager build(Context currentContext) {
        context = currentContext;
        sharedSettingsManager = SharedSettingsManager.build(currentContext);

        if (instance == null) {
            instance = new LevelManager();
        }
        return instance;
    }


Что это за куча констант? Это элементы легенды. На самом деле, спертый честно взятый уровень выглядит примерно так

И каждая циферка что-то да обозначает. А чтобы не лезть каждый раз в справку игры, поназаводим понятных глазу констант и будем работать только с ними. Про sharedSettingsManager, который тут зачем-то есть я вам расскажу в другой раз, а пока давайте научим наш менеджер открывать уровень и строить из него приличный объект

Для начала, попробуем достать сам уровень из файла и как-то его распарсить. Для выдергивания различных циферок и буковок из потока данных у нас есть Scanner, так что его мы на файлик и натравим

private Scanner openLevel(int levelNumber) throws IOException {
    AssetManager assetManager = context.getAssets();
    InputStream inputStream = assetManager.open(
            LEVELS_FOLDER +
                    "/" +
                    String.valueOf(levelNumber) +
                    LEVEL_FILE_EXTENSION);

    BufferedReader bufferedReader =
            new BufferedReader
                    (new InputStreamReader(inputStream));

    return new Scanner(bufferedReader);
}


Да, чуть не забыл. Все уровни у нас хранятся к assets андройда, достать их оттуда не представляет никакого труда, правда, только, если у нас есть контекст. А называются они по своим номерам. Поэтому, нам остается только передать номер требуемого уровня, чтобы получить готовый сканнер, который работает с нужным файликом.

Теперь – будем конвертировать элемент легенды в Item.

private Item convertLegendToItem(int itemLegend) {
    switch (itemLegend) {
        case EMPTY_CELL:
            return null;

        case BLOCK_CELL:
            return new Block();

        case GREEN_BALL_CELL:
            return new Ball(Color.GREEN);

        case RED_BALL_CELL:
            return new Ball(Color.RED);

        case BLUE_BALL_CELL:
            return new Ball(Color.BLUE);

        case YELLOW_BALL_CELL:
            return new Ball(Color.YELLOW);

        case PURPLE_BALL_CELL:
            return new Ball(Color.PURPLE);

        case CYAN_BALL_CELL:
            return new Ball(Color.CYAN);

        case GREEN_HOLE_CELL:
            return new Hole(Color.GREEN);

        case RED_HOLE_CELL:
            return new Hole(Color.RED);

        case BLUE_HOLE_CELL:
            return new Hole(Color.BLUE);

        case YELLOW_HOLE_CELL:
            return new Hole(Color.YELLOW);

        case PURPLE_HOLE_CELL:
            return new Hole(Color.PURPLE);

        case CYAN_HOLE_CELL:
            return new Hole(Color.CYAN);
    }

    return null;
}


Один большой-пребольшой switch и ничего сложного.

Ну и наконец – научимся обрабатывать весь уровень целиком:

public Level getLevel(int levelNumber) throws IOException {
    Scanner scanner = openLevel(levelNumber);

    int levelWidth = scanner.nextInt();
    int levelHeight = scanner.nextInt();

    Item levelMatrix[][] = new Item[levelHeight][levelWidth];

    for (int i = 0; i < levelHeight; i++) {
        for (int j = 0; j < levelWidth; j++) {
            levelMatrix[i][j] = convertLegendToItem(scanner.nextInt());
        }
    }

    Level level = new Level(levelMatrix);
    sharedSettingsManager.setCurrentLevel(levelNumber);
    return level;
}


Взяли номер – вернули уровень. Чудеса. Вот только, кроме открытия уровня, надо еще и «завершать» его в тот момент, когда шариков там не осталось. Определять – остались там шарики или нет это задача движка, а вот обрабатывать внезапно закончившийся уровень будем так
public void finishLevel() {
    sharedSettingsManager.setCurrentLevel(
            sharedSettingsManager.getCurrentLevel() + 1
    );

    if (sharedSettingsManager.getCurrentLevel() > sharedSettingsManager.getMaxLevel()) {
        throw new GameException(GameExceptionCodes.INCORRECT_LEVEL);
    }
}


Ага, сделали отметочку о том, что этот уровень пройден, изменили номер текущего уровня и, если все уровни кончились, плюнули исключение. Мило? Тогда попробуем запустить нашу игрушку на, например, втором уровне

Хе! И правда, работает. Тогда самое время научиться сохранять пользовательские достижения

Еще один менеджер


Если пользователя каждый раз заставлять проходить все уровни заново, пользователь будет зол и недоволен. Так что придется как-то хранить значение текущего и последнего доступного уровней так, чтобы данные не терялись между запусками.

Для хранения таких несложных штуковин, в Android есть SharedSettings. Обычно, его используют для хранения настроек, но нам оно тоже сгодится. Так что заведем еще одного менеджера

public static final String LAST_LEVEL = "current_level";
public static final String MAX_LEVEL = "max_level";
public static final String WAS_RAN_BEFORE = "was_ran_before";
private static final String APP_PREFS = "qook_prefs";
public static Context context;
public static SharedSettingsManager instance;
SharedPreferences sharedPreferences;

private SharedSettingsManager() {
    sharedPreferences = context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE);
}


Тоже синглтон, ага. Потому что мало ли, где мы там во что вляпаемся, а выяснять – threadsafe ли сами sharedsettings особого желания почему-то нет.

Теперь, научимся выдавать текущий и максимальный уровень.

Раз

public int getMaxLevel() {
    return sharedPreferences.getInt(MAX_LEVEL, 1);
}


И два
public int getCurrentLevel() {
    return sharedPreferences.getInt(LAST_LEVEL, 1);
}


Теперь, попробуем их записывать обратно. Вызывать где-то выше два отдельных метода особого желания нет, поэтому сделаем вот как
private void setMaxLevel(int maxLevel) {
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putInt(MAX_LEVEL, maxLevel);
    editor.apply();
}

public void setCurrentLevel(int currentLevel) {
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putInt(LAST_LEVEL, currentLevel);

    editor.apply();

    if (getMaxLevel() < currentLevel) {
        setMaxLevel(currentLevel);
    }
}


Теперь, по завершении очередного уровня достаточно просто изменить текущий, а бы он последним или нет – менеджер разберется и без нас. Иначе зачем он тут вообще сидит?

Дописываем разметку

Игрушка у нас уже играется, это хорошо. Ничего кроме этого в ней нет – плохо. А ведь так хочется и на кнопочку «сначала» нажать и в менюшку выйти и на каком мы уровне посмотреть. Так что добавим над игровым полем вот такую плашку

Код у кнопочек проще не бывает, так что рассказывать, как оно работает не хочу. Тем более, что, ничего кроме вызовов менеджеров или перехода в другое активити там нет

А вот как саму разметку имеет смысл показать, там интересно
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://ift.tt/nIICcg"
    xmlns:app="http://ift.tt/GEGVYd"
    xmlns:tools="http://ift.tt/LrGmb4"
    android:id="@+id/game_activity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background"
    android:gravity="center_vertical"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".ui.activities.LevelActivity">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/level_counter"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="16dp"
            android:paddingTop="5dp"
            android:text="01 / 60"
            android:textSize="34sp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="right"
        android:paddingBottom="10dp">

        <ImageButton
            android:id="@+id/back_level_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:src="@drawable/menu_icon" />

        <ImageButton
            android:id="@+id/reset_level_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:src="@drawable/restore_level" />

        <ImageButton
            android:id="@+id/undo_step_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:src="@drawable/undo_step" />

    </LinearLayout>

    </LinearLayout>

    <org.grakovne.qook.ui.views.FieldView
        android:id="@+id/field"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"
        android:background="@drawable/field_ground"
        android:foregroundGravity="center" />

</LinearLayout>



Ну вот с самой игрушкой мы разобрались. Осталось всего-то: дописать еще пару-тройку экранов, приделать выбор произвольного уровня, добавить ландшафтную разметку и выложить это дело в стор. Мелочи, сэр!

Пишем менюшку для уровней


За это дело будет отвечать отдельный экран, который будет выглядеть более, чем скучно: каждый уровень – квадратик, открытые и еще не открытые уровни разных цветов, по еще не открытым нажимать нельзя. Создадим разметочку?
<TextView
    android:id="@+id/title_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:gravity="center"
    android:paddingBottom="16dp"
    android:text="@string/app_name"
    android:textAllCaps="true"
    android:textSize="48sp"
    android:textStyle="bold" />

<GridView
    android:id="@+id/level_grid"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="9"
    android:numColumns="5">
</GridView>


Заголовочек и небольшая гридвьюшка – ничего нового. Теперь, придумаем как эту вьюшку наполнить чем-то полезным

Для этого нам придется написать адаптер, который будет создавать новую вьюшку, забивать ее данными, навешивать на нее clickListener и запихивать в родителя. Примерно так:

public View getView(int position, View convertView, ViewGroup parent) {
    LayoutInflater vi;
    vi = LayoutInflater.from(getContext());
    @SuppressLint("ViewHolder") View view = vi.inflate(R.layout.level_item, null);

    Integer currentLevelNumber = getItem(position);
    if (currentLevelNumber != null) {
        Button levelButton = (Button) view.findViewById(R.id.level_item_button);
        if (levelButton != null) {

            levelButton.setText(String.valueOf(currentLevelNumber));

            if (position < maxOpenedLevel) {
                levelButton.setBackgroundResource(R.drawable.opened_level_item);
                levelButton.setClickable(true);
                levelButton.setOnClickListener(clickListener);
                levelButton.setId(currentLevelNumber);
            } else {
                levelButton.setBackgroundResource(R.drawable.closed_level_item);
                levelButton.setClickable(false);
            }
        }
    }

    return view;
}


Здорово. Только мы хотим, чтобы все кнопочки уровней были квадратными. Для этого, создадим своего наследника Button и добавим немного магии:
public class LevelButton extends Button {
@Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
    }
}


Хе-хе. Какая ширина, такая и высота. Никто и не заметил. Осталось только вызывать это все при создании активити
@Override
public void onResume() {
    super.onResume();
   
    manager = LevelManager.build(getBaseContext());

    View.OnClickListener levelClick = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(getBaseContext(), LevelActivity.class);
            intent.putExtra(DESIRED_LEVEL, v.getId());
            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
            startActivity(intent);
        }
    };

    LevelGridAdapter adapter = new LevelGridAdapter(this, R.layout.level_item, getListOfLevelNumbers(), manager.getMaximalLevelNumber(), levelClick);
    adapter.setNotifyOnChange(false);
    levelGrid.setAdapter(adapter);
    levelGrid.setVerticalScrollBarEnabled(false);
}


Кликнули по кнопочке – уровень открылся. Красота.

Играемся


После того, как мы дописали к игре основное меню, помощь и, конечно же, «об авторе», самое время попробовать поиграть в то, что у нас там получилось.


Это у меня первые три уровня от зубов отскакивают – натестился до коликов. Зато работает: и уровни сохраняются и переворот дисплея не роняет приложение, как еще три сборки назад, да и выглядит все это дело симпатично – прямо чувствуешь себя человеком.

Ладно, раз это дело работает – выложим это добро в Google Play, глядишь, кому и понравится

Регистрируемся, платим $25 гуглу, ждем чуток времени, создаем проект, заполняем поля для данных и… получаем целую страничку в разделе «Головоломки», а, заодно, вот такую милую плашечку

Доступно на Google Play

Ну вот. Игрушку мы с вами написали. Остается только ее пройти и отметить еще одну мечту детства реализованной. А теперь, я, с вашего позволения, все же пойду дописывать кнопочку «отменить последний ход», пока особо увлекшиеся знакомые не разорвали меня за ее отсутствие окончательно

Искренне Ваш, портирующий Тетрис на стиральную машинку, GrakovNe

Комментарии (0)

    Let's block ads! (Why?)

    JPoint 2016: Быстрее, выше, производительнее

    Судный день: К чему приводят скрытые ошибки асинхронной обработки данных при росте нагрузки

    В нашем блоге мы рассказываем не только о развитии своего продукта — биллинга для операторов связи «Гидра», но и описываем сложности и проблемы, с которыми сталкиваемся на этом пути. Ранее мы уже описывали ситуацию, в которой бесконтрольный рост таблиц в базе данных одной компании-пользователя нашей системы привел к настоящему DoS.

    Сегодня речь пойдет о еще одном интересном случае внезапного сбоя, который сделал «день смеха» 1 апреля этого года совсем не смешным для службы поддержки «Латеры».

    Все пропало


    Один из операторов связи и, по совместительству, клиентов нашей компании, пользовался биллингом «Гидра» на протяжении нескольких лет. Изначально все было хорошо, однако со временем стали возникать проблемы — к примеру, различные элементы системы могли работать медленнее положенного.

    Однако утром первого апреля спокойный ход событий был нарушен. Что-то пошло не так — в техподдержку обратились крайне возбужденные представители клиента. Оказалось, что часть абонентов, не оплативших доступ в интернет, получила его (что не самое страшное), а пользователям, которые все оплачивали вовремя, доступ был заблокирован — и вот это уже вызвало просто бурю недовольства.

    Служба поддержки была поднята по тревоге. Предстояло максимально быстро найти и устранить проблему.

    Горячий день


    Зайдя на биллинговый сервер, инженеры техподдержки первым делом обнаружили, что полтора часа назад закончилось место в основном табличном пространстве рабочей БД. В результате этого остановилось несколько системных процессов биллинга — в частности, обновление профилей в модуле provisioning (отвечает за управление доступов абонентов к услугам). Техподдержка выдохнула и, предвкушая скорый обед, быстро увеличила табличное пространство и запустила процессы.

    Это не помогло — абонентам легче не стало. Клиент тем временем начал паниковать: поток возмущенных звонков «я заплатил, но ничего не работает» начал заваливать его колл-центр. Разбирательство продолжилось.

    На сервере RADIUS-авторизации, который за несколько дней до этого переехал на новое «железо», инженеры обнаружили сильнейшее замедление работы. Быстро выяснилось, что оно приводило к тому, что в БД сервера авторизации содержались неактуальные данные о пользовательских профилях — именно поэтому биллинг ошибочно открывал доступ в интернет одним абонентам и закрывал его тем, кто оплачивал услуги вовремя. Однако причина столь драматического падения производительности оставалась неясной.

    Для избавления от неактуальных данных было решено повторно отправить в RADIUS-сервер из провиженинга правильные данные, по сути дела реплицировать всю информацию заново. Эта операция изначально задумывалась как средство, которое применяется в отчаянном положении при разрушении БД.

    Через несколько минут стало понятно, что повторная отправка данных не просто не помогла, но существенно ухудшила ситуацию. Задержка от оплаты до включения доступа у абонентов выросла, по нашим расчетам, до нескольких часов.

    Единственное, что можно было сделать дополнительно — предварительно очистить данные в кэше перед репликацией, но это привело бы к отказам в обслуживании для абонентов, у которых все было хорошо (а их было большинство), и на это пойти мы не могли.

    В итоге решено было разбирать проблему по шагам, поделив ее на этапы и проверяя элементы системы на каждом из них. Это позволило реконструировать ход событий того дня, в конце концов понять причину и устранить проблему.

    Лирическое отступление: Когда город засыпает


    Итак, в ночь на 1 апреля биллинг начал генерировать новые данные для абонентских профилей на замену старым — если абонент не оплатил услугу, то нужно было отобрать у него доступ к ней. На этом шаге модуль provisioning сгенерировал большое количество новых профилей и асинхронно отправил их во внутреннюю очередь сообщений Oracle. Поскольку напрямую с очередями Oracle работать извне с нашим стеком технологий неудобно, для их дальнейшей передачи используется «прослойка» в виде брокера сообщений Apache ActiveMQ, к которому подключается RADIUS-сервер, записывающий данные в MongoDB.

    Биллинг должен отправлять данные об измененных абонентских профилях в строгом порядке. Чтобы порядок соблюдался и не происходило «путаницы» профилей на отключение и подключение, в системе реализован специальный процесс, который выбирает данные из очереди ActiveMQ и записывает их в MongoDB.

    Приходит понимание


    Система была развернута таким образом, что ActiveMQ и MongoDB находились на разных серверах, при этом в используемой конфигурации ActiveMQ не требовалось подтверждения получения сообщений — все отправленное из очереди считалось по умолчанию принятым. Нужно было лишь входящее соединение, затем данные отправлялись до исчерпания лимитов буферов ОС принимающей стороны.

    Ранее иногда случались случаи потери связи RADIUS-сервера с ActiveMQ — на такой случай мы предусмотрели функциональность восстановления соединений. Для детектирования случаев потери связи был реализован специальный механизм, использующий протокол STOMP — в нем есть настройка передачи heartbeat-пакетов. Если такой сигнал не получен, то соединение считается утерянным и принудительно переустанавливается.

    Когда мы все это осознали, причина проблемы стала сразу понятна.

    Сообщения из ActiveMQ всегда извлекаются в том же потоке исполнения, который записывает данные в профили абонентов. Извлечение и запись — то, что он делает по порядку, и если запись задержалась, то поток не может «достать» следующий пакет. И если в нем содержался heartbeat, то он не будет получен — и другой поток, который проверяет наличие таких пакетов, будет считать соединение потерянным и попытается его переустановить.

    Когда соединение переустанавливается, тот сокет и его буфер, которые использовались, закрываются, и все данные, которые там лежали и еще не были разобраны приложением, оказываются потерянными. При этом ActiveMQ пребывает в уверенности относительно успешности отправки, поскольку подтверждения получений не требуется, а принимающая сторона регулярно переустанавливает соединение.

    Получается, что часть обновлений профилей остается потерянной, но дальнейшая работа системы продолжается. В итоге реплицированный кэш базы данных оказывается частично невалидным, хотя многие обновления все же доходили.

    Вывести проблему из острой фазы удалось с помощью увеличения heartbeat-таймаута до большего числа — это позволило потоку, обрабатывающему данные, дождаться записи и успешно обработать все сообщения. После этого работоспособность системы была полностью восстановлена, а в базу данных авторизационного сервера попала корректная информация. В общей сложности с момента поступления заявки от клиента до завершения разбирательств прошло шесть часов.

    Кризис миновал, можно было выдохнуть, однако теперь требовалось понять, что послужило причиной такой некорректной работы, и предотвратить возможность повторения подобных сбоев.

    Поиск причин


    Как всегда бывает в моменты масштабных аварий, друг на друга наложились сразу несколько факторов — каждый по отдельности не привел бы к столь плачевным последствиям, но сработал эффект «снежного кома».

    Одной из причин стало желание руководства нашего клиента, видеть все платежи от всех клиентов в один день. Именно поэтому система была настроена таким образом, чтобы все списания и зачисления происходили в первый день нового месяца.

    Этот процесс устроен так: в биллинге по каждому абоненту ведется электронный документ, в котором учитывается факт оказания услуги. Каждый месяц такой документ создается заново. За несколько часов до окончания месяца происходит проверка баланса — документы анализируются, и на основании этого анализа абоненты после полуночи либо вновь получают доступ к услугам, либо теряют его.

    Поскольку такие проверки баланса у этого клиента происходят одномоментно в начале нового месяца, то первого числа каждого месяца нагрузка на систему всегда возрастает. Этому способствует не только необходимость проанализировать всех клиентов и отключить неплательщиков — тех из них, кто сразу же решит оплатить услуги, нужно быстро «допустить» к услугам снова и корректно этот факт учесть.

    Все это работает благодаря двум важным системным процессам. Один из них отвечает за выполнение команд на отключение абонентов. Когда биллинг понимает, что абонента нужно отключить, он через встроенный модуль provisioning отправляет команду на прерывание его сессии доступа.

    Второй процесс — предотвращение установления новой сессии отключенным абонентом. Для этого в независимую базу данных авторизационного сервера с пользовательскими конфигурациями нужно реплицировать данные из provisioning-модуля «Гидры». Это позволяет всем частям системы знать о том, что определенных абонентов обслуживать сейчас не нужно.

    Важное замечание: очевидно, что подобное желание видеть все деньги от всех абонентов в один день не способствует равномерному распределению нагрузки на систему. Напротив, в ходе «судного дня» для абонентов она вырастает в разы — причем, как на биллинг, так и на сервисы приема платежей. Кроме того, такая конфигурация способствует увеличению оттока пользователей (о том, как с ним бороться, мы рассказывали здесь и здесь).

    Вторая причина — желание сэкономить на инфраструктуре и отказ от разумного разнесения сервисов. Для биллинга был закуплен менее мощный сервер, чем было необходимо. Четыре года он отработал без проблем, однако объём данных в БД рос, кроме того, на сервер постепенно «навешивались» требовательные к ресурсам дополнительные сервисы (внешняя отчетная система на Java-машине, новый модуль provisioning также с использованием Java-машины и т.д.), а рекомендации инженеров, говоривших о необходимости разнесения сервисов, откладывались в долгий ящик с аргументом «работает же».

    Две из четырех предпосылок будущего сбоя уже были в наличии. Но даже в этом случае масштабных проблем удалось бы избежать, если бы не две трагических случайности, которые случились одновременно в самый неподходящий момент.

    Незадолго до 1 апреля наконец удалось получить отдельный сервер для авторизационной БД с профилями абонентов. Как выяснилось позже, RAID-массив на этом сервере оказался дефектным, и на нем авторизация при определенном уровне нагрузки тормозила еще сильнее, чем на старом «железе». Ни на одной из более чем 100 других инсталляций «Гидры» эта ошибка не проявлялась из-за того, что в нормальных условиях сервис авторизации работает быстро и ресурсов хватает с большим запасом.

    Непосредственным триггером аварии, вероятно, следует считать закончившееся место в табличном пространстве, которое привело к тому, что начали накапливаться изменения профилей. Как только свободное место вновь появилось, все эти изменения были обработаны и в дефектный RADIUS-сервер через системные очереди устремился поток репликационных сообщений, которые не смогли примениться.

    Определенные подозрения о возможном баге возникали и до описываемой ситуации — периодически у некоторых единичных абонентов в базе сохранялись неправильные профили. Однако вычислить проблему до первого числа нового месяца не удалось — событие было очень редким и расширенное логирование не помогло вовремя «отловить» ошибку (в том числе по причине переезда на новый сервер).

    Предотвращение проблем в будущем


    Устранив сбой и разобравшись в его причинах, уже в спокойной обстановке мы внесли корректировки, призванные исключить повторение ситуации в будущем. Вот, что мы сделали:
    • Прежде всего, в ActiveMQ была добавлена функциональность требования подтверждения доставки отправленных данных. При этом подобное подтверждение работает в кумулятивном режиме — сервер подтверждает получение не каждого сообщения, а некоторой их пачки (раз в пять секунд). Логика обработки сообщений поддерживает повторную обработку очереди начиная с определенного момента, даже если какие-то из данных уже попали в БД.
    • Кроме того была увеличена частота отправки heartbeat-пакетов — вместо пяти секунд время увеличилось до нескольких минут. В дополнение к механизму heartbeat соединение к брокеру сообщений стало устанавливаться с опцией keepalive с небольшими интервалами проверки активности соединения (несколько десятков секунд против пары часов, устанавливаемой операционной системой по умолчанию).
    • Также производились тесты, в ходе которых при отправке сообщений случайным образом перезапускались разные модули системы. В ходе одного из таких тестов какая-то часть данных все равно оказывалась потерянной. Тогда был заменен сам «движок» базы данных MongoDB — перешли на использование WiredTiger. Мы планировали сделать это раньше, но по случаю тестов решили совместить переезд.

    Внесенные изменения позволили свести число потерянных пакетов к нулю и предотвратить возникновение подобных проблем в будущем даже в условиях очень «агрессивной среды».

    Кроме того, по рекомендациям инженеров техподдержки «Латеры» сервисы системы были разнесены на разные серверы (деньги на них быстро нашлись). Это удалось успеть сделать до 1 мая — следующего дня массовых биллинговых операций.

    Главный урок


    Тревожные «звоночки», сигнализирующие о возможных проблемах, звучали и в марте, но выявить их до наступления планового всплеска активности не удалось. Если бы этого всплеска не было, или он произошел бы в другом месте — не на «тормозящем» RADIUS-сервере с максимальной скоростью последовательной записи 5 МБ/сек, то с высокой долей вероятности инженерам удалось бы локализовать проблему в апреле. Им не хватило буквально нескольких дней.

    Все это подводит к выводу о том, что в том случае, если существуют подозрения на наличие не до конца понятных проблем с работоспособностью систем, а также ожидается всплеск активности, то стоит также быть готовым, что именно в этот момент все пойдет по максимально неблагоприятному сценарию. При наличии таких подозрений необходимо интенсивнее проводить поиск возможных проблем, чтобы успеть разобраться с ними до серьезного повышения нагрузки.

    Другие технические статьи от «Латеры»:


    Комментарии (0)

      Let's block ads! (Why?)

      Преимущества стекирования Juniper

      Часть 1. Технология виртуального шасси

      Одним из основных преимуществ решений Juniper перед конкурентами являются их возможности стекирования. Вариантов довольно много, начиная от базового Virtual Chassis и заканчивая целым рядом датацентровых технологий.

      Часть 1. Virtual Chassis

      Рассмотрим возможности и особенности базовой технологии Virtual Chassis, которая позволяет объединить до 10 устройств (в зависимости от модели) EX серии и QFX серии в одно логическое устройство, а также способы ее настройки и мониторинга.

      Начнем с того, какие модели поддерживают технологию Virtual Chassis:

      • EX2200 Ethernet Switch до 4 устройств
      • EX3300 Ethernet Switch до 10 устройств
      • EX4200 Ethernet Switch до 10 устройств
      • EX4300 Ethernet Switch до 10 устройств
      • EX4550 Ethernet Switch до 10 устройств
      • EX4600 Ethernet Switch до 10 устройств
      • QFX5100 Switch до 10 устройств.

      Это те модели, которые на момент написания статьи производятся и доступны для заказа, и, соответственно, могут использоватся в виртуальном шасси.

      Технологии Virtual Chassis сочетают масштабируемость и компактный форм-фактор отдельностоящих коммутаторов с высокой доступностью, пропускной способностью и плотностью портов традиционных шасси. Virtual Chassis позволяет обеспечить экономичное развертывание коммутаторов и доступность сети в местах, где стоимость установки шассийных устройств может быть слишком высока, или где физически невозможно поставить шассийные коммутаторы.

      Благодаря тому, что несколько устройств управляются как единое целое, Virtual Chassis позволяет использовать такие же возможности по резервированию, как и использование нескольких Routing Engine (RE) в шассийных коммутаторах, включая технологию graceful Routing Engine switchover (GRES).

      Для подключения в виртуальное шасси могут применяться специализированные интерфейсы, которые находятся на устройствах EX4500, EX4550 и EX4200, а также медные и оптические порты. Порты, которые используются для подключения в виртуальное шасси, называются Virtual Chassis Ports (VCP). Например, в коммутаторе EX2200-24T-4G 24 10/100/1000Base-T и 4 SFP для подключения в виртуальное шасси можно использовать как SFP, так и медные порты. Это позволяет разнести членов одного виртуального шасси на расстояние до 80 км.

      Mixed Virtual Chassis дает возможность использовать в рамках одного виртуального шасси коммутаторы разных серий и даже разных линеек, как показано на рисунке ниже.


      Варианты стекирования коммутаторов в Mixed Virtual Chassis

      В виртуальном шасси есть разделение по ролям: два коммутатора получают роль RE0 и RE1, а остальные используются в качестве линейных карт.

      RE0 выполняет роль мастера, задача которого – управление остальными членами виртуального шасси, и именно он отвечает за создание таблиц маршрутизации и распространение их по линейным картам. На нем также хранятся файлы конфигурации. RE1 – backup, на случай выхода RE0 из строя.

      Рассмотрим распределение ролей коммутаторов в виртуальном шасси на примере ниже.

      Критерии выбора мастера:

      — Коммутатор с наивысшим приоритетом. Приоритет может быть от 1 до 255, по умолчанию он 128 на всех коммутаторах;
      — Коммутатор, который был мастером до перезагрузки;
      — Коммутатор с наибольшим uptime (разница должна быть больше 1 мин);.
      — Коммутатор с наименьшим MAC адресом.

      Каждый из членов виртуального шасси получает свой ID от 0 до 9. Настроить данный ID можно вручную с мастера (у которого ID 0) (после перезагрузки номер сохраняется и может служить как номер слота в определении интерфейса).

      Из-за того, что ID закрепляется за коммутатором, может возникнуть следующая ситуация: когда один коммутатор выходит из строя и вы его меняете, новый член получает не номер вышедшего из строя коммутатора, а следующий свободный или вовсе не подключается к виртуальному шасси, если все ID уже заняты.

      Поэтому, чтобы заменить вышедший из строя коммутатор новым, сперва нужно с мастера снять его ID следующим образом:

      {master:0}

      user@Switch-1> request virtual-chassis recycle member-id <member-id>

      Или можно это сделать с помощью ID команды:

      user@host> request virtual-chassis renumber member-id <current-member-id> new-member-id <new-member-id>

      Единый менеджмент-интерфейс и единая консоль

      В рамках виртуального шасси все management-интерфейсы объединяются в один, с единым IP адресом:

      {master:0}[edit]

      user@switch# show interfaces vme

      unit 0 {

      family inet {

      address 10.210.14.148/27;

      }

      }

      {master:0}[edit]

      user@Switch-1# run show interfaces terse vme

      Interface Admin Link Proto Local Remote

      vme up up

      vme.0 up up inet 10.210.14.148/27

      То же самое происходит и с консольным портом, в какой бы вы не подключились, вы попадете на RE0.

      Совместимость программного обеспечения

      Все члены виртуального шасси должны быть с одинаковой версией софта. Когда мы добавляем новый коммутатор в виртуальное шасси, мастер проверяет его версию софта, если софт отличается, то новый коммутатор получит ID, но не станет активным членом виртуального шасси. Для этого нужно обновить на новом коммутаторе программное обеспечение с помощью команды:

      request system software add member <member-id> reboot

      Эта команда загружает образ из главного коммутатора через VCPs нового коммутатора, а затем перезагружает его, при этом новый коммутатор может быть не подключен напрямую к главному.

      Автоматическое обновление программного обеспечения

      С помощью настройки автоматического обновления софта, при подключении каждого нового коммутатора с версией, отличной от мастера, его софт будет автоматически обновляться. set virtual-chassis auto-sw-update package-name <package-name>

      Nonstop Software Upgrade

      Технология Nonstop software upgrade (NSSU) позволяет обновлять ПО на всех членах виртуального шасси с минимальными потерями. Для корректной работы данной технологии нужно, чтобы:

      — Все члены виртуального шасси были подключены по топологии «кольцо»;
      — Master и backup были смежными;
      — Линейные карты были настроены в режиме преконфигурации;
      — На всех членах виртуального шасси должна быть одна версия софта;
      — Должны быть включены NSR и GRES; Опционально можно активировать NSB командой request system snapshot.

      Процесс NSSU:

      — Скачиваем образ софта. Если используем смешанное виртуальное шасси, то скачиваем оба образа.
      — Копируем его на RE0, рекомендовано в папку /var/tmp,
      — Из командной строки RE0 запускаем NSSU командой:
      request system software nonstop-upgrade /var/tmp/package-name.tgz
      или для смешанных виртуальных шасси
      request system software nonstop-upgrade set [/var/tmp/package-name.tgz /var/tmp/package-name.tgz]

      Рекомендованное расположение коммутаторов Master и Backup

      При построении виртуального шасси рекомендуются такие схемы подключения:

      Или

      Если элементы виртуального шасси разнесены территориально, то стоит использовать схему ниже:

      Рекомендованные топологии виртуального шасси

      На следующих рисунках показаны топологии виртуального шасси, которые могут быть развернуты на основе конкретных потребностей пользователей. Топология «кольцо» является наиболее часто используемой, но виртуальное шасси также можно развернуть в «Full mesh» топологии или топологии нескольких колец. Топология «кольцо» рекомендуется при развертывании виртуального шасси в смешанном режиме.


      Кольцо


      Full mesh


      Топология нескольких колец

      Настройка Virtual Chassis

      Настройка виртуального шасси происходит на уровне иерархии [edit virtual-chassis]

      {master:0}[edit virtual-chassis]
      user@switch# set?
      Possible completions:
      + apply-groups Groups from which to inherit configuration data
      + apply-groups-except Don't inherit configuration data from these groups
      > auto-sw-update Auto software update
      > fast-failover Fast failover mechanism
      id Virtual Chassis identifier, of type ISO system-id
      > mac-persistence-timer How long to retain MAC address when member leaves Virtual Chassis
      > member Member of Virtual Chassis configuration
      no-split-detection Disable split detection. Only recommended in a 2 member setup
      preprovisioned Only accept preprovisioned members
      > traceoptions Global tracing options for Virtual Chassis

      Чтобы свести к минимуму прерывания трафика во время сценария отказоустойчивости RE, нужно включить graceful Routing Engine switchover.

      {master:0}[edit chassis]
      user@switch# set redundancy graceful-switchover?
      Possible completions:
      > graceful-switchover Enable graceful switchover on supported hardware

      Есть несколько вариантов: динамический и Preprovisioning.

      Динамический:

      1. Включаем мастер коммутатор и устанавливаем ID 0 и приоритет 255.
      2. Подключаем backup коммутатор и устанавливаем ID 0 и приоритет 255.
      {master:0}[edit virtual-chassis]
      user@Switch-1# set member <member-id> mastership-priority <priority>
      3. При использовании только двух коммутаторов рекомендуется отключить split detection.
      [edit virtual-chassis]
      user@switch# set no-split-detection
      4. Включаем питание на остальных коммутаторах.
      5. Если мы используем смешанное виртуальное шасси, то добавляем команду:
      user@device> request virtual-chassis mode mixed reboot
      6. На каждом индивидуальном коммутаторе указываем порты, которые будут в роли VCPs.

      Preprovisioning

      1. Включаем мастер коммутатор (предварительно собираем все серийные номера коммутаторов, которые будут в кластере).
      2. Если у нас будет использоваться смешанное виртуальное шасси:
      user@device> request virtual-chassis mode mixed reboot
      3. Опционально настраиваем IP адрес на менеджмент интерфейсе:
      user@switch# set interfaces vme unit 0 family inet address /ip-address/mask/
      4. Ставим Preprovisioning mode:
      [edit virtual-chassis]
      user@switch# set preprovisioned
      5. Прописываем роль, ID для каждого члена виртуального шасси
      [edit virtual-chassis]
      user@switch# set member 0 serial-number BM0208105168 role routing-engine
      user@switch# set member 1 serial-number BM0208124111 role line-card
      user@switch# set member 2 serial-number BM0208124231 role routing-engine
      user@switch# set member 3 serial-number BM0208124333 role line-card
      6. При использовании только двух коммутаторов, рекомендуется отключить split detection.
      [edit virtual-chassis]
      user@switch# set no-split-detection
      7. Включаем остальных членов виртуального шасси.
      8. Если используем смешанное виртуальное шасси, то вводим на каждом коммутаторе
      user@device> request virtual-chassis mode mixed reboot
      9. Указываем какие интерфейсы будем использовать в роли VCPs
      user@switch> request virtual-chassis vc-port set pic-slot pic-slot-number port port-number local
      Например, чтобы использовать встроенные 40G порты на коммутаторе EX4600
      user@switch> request virtual-chassis vc-port set pic-slot 2 port 0 local
      Где порт 0 – первый 40G встроенный порт.
      user@switch> request virtual-chassis vc-port set pic-slot pic-slot-number port port-number local

      Мониторинг Virtual chassis

      Для мониторинга виртуального шасси используется команда show virtual-chassis с различными ключами:

      {master:0}
      user@switch> show virtual-chassis?
      Possible completions:
      <[Enter]> Execute this command
      active-topology Virtual Chassis active topology
      device-topology PFE device topology
      fast-failover Fast failover status
      login
      protocol Show Virtual Chassis protocol information
      status Virtual Chassis information
      vc-path Show virtual-chassis packet path
      vc-port Virtual Chassis port information
      | Pipe through a command

      Проверка состояния портов виртуального шасси:

      show virtual-chassis vc-port

      {master:0}
      user@Switch-1> show virtual-chassis vc-port
      fpc0:
      — Interface Type Trunk Status Speed Neighbor
      or ID (mbps) ID Interface
      PIC / Port
      vcp-0 Dedicated 2 Up 32000 1 vcp-0
      vcp-1 Dedicated 1 Up 32000 1 vcp-1
      fpc1:
      — Interface Type Trunk Status Speed Neighbor
      or ID (mbps) ID Interface
      PIC / Port
      vcp-0 Dedicated 2 Up 32000 0 vcp-0
      vcp-1 Dedicated 1 Up 32000 0 vcp-1

      Проверка информации о состоянии:

      show virtual-chassis status

      {master:0}
      user@Switch-1> show configuration virtual-chassis
      preprovisioned;
      member 0 {
      role routing-engine;
      serial-number BM0208105168;
      }
      member 1 {
      role line-card;
      serial-number BM0208124231;
      }
      {master:0}
      user@Switch-1> show virtual-chassis status
      Preprovisioned Virtual Chassis
      Virtual Chassis ID: 8d5c.a77f.8de8
      Mastership Neighbor List
      Member ID Status Serial No Model priority Role ID Interface
      0 (FPC 0) Prsnt BM0208105168 ex4200-24t 129 Master* 1 vcp-0
      1 vcp-1
      1 (FPC 1) Prsnt BM0208124231 ex4200-24t 0 Linecard 0 vcp-0
      0 vcp-1

      Вот так кратко мы рассмотрели технологию Virtual chassis, варианты настройки и мониторинга.

      Комментарии (0)

        Let's block ads! (Why?)

        [Перевод] Intel System Studio for Microcontrollers 2015: подробности о разработке и отладке

        Отчёт с Moscow Atlassian Meetup 20 апреля

        В конце апреля прошёл третий митап, посвящённый продуктам Atlassian. Программа была рассчитана в первую очередь на опытных администраторов и разработчиков. Предлагаем вашему вниманию презентации и видеозаписи выступлений докладчиков.

        «История разработки eazyBI», Raimonds Simanovskis (eazyBI, Латвия)

        eazyBI позволяет легко строить отчеты и графики, являясь одним из самих популярных JIRA-плагинов на Atlassian Marketplace. Основатель и главный разработчик eazyBI Раймондс Симановскиc приехал из Латвии, чтобы рассказать об эволюции eazyBI из отдельного веб-приложения в набор из нескольких продуктов с различными вариантами развертывания.

        Некоторые тезисы:

        • упаковка JRuby on Rails-приложения как плагина для JIRA;
        • проблемы с поддержкой многих версий JIRA, баз данных, операционных систем;
        • вынесение JIRA-плагинa из JIRA JVM-процесса;
        • основные архитектурные проблемы разработки Atlassian Connect-плагинa для JIRA Cloud.


        Видеозапись выступления: it.mail.ru/video/576

        «Как написать свой первый плагин для JIRA», Александр Кузнецов (StiltSoft, Белоруссия)

        Вы — опытный администратор JIRA? Легко настраиваете кастомные поля, никогда не теряетесь в переходах по workflow, но иногда вам не хватает стандартной функциональности JIRA? Ничего страшного — у вас всегда есть возможность написать плагин для JIRA. Не умеете? Научим!

        В докладе речь шла о том, какие первые шаги следует предпринять опытному администратору JIRA при разработке своего первого плагина. Александр рассказал про Atlassian SDK и про её наиболее часто применимые команды, упрощающие жизнь начинающему разработчику плагинов. Была досконально разобрана структура плагина, из каких основных «кирпичиков» он состоит и как эти «кирпичики» объединить в единое целое. В ходе выступления вместе с аудиторией попробовали написать свой первый плагин и запустить его в JIRA.


        Видеозапись выступления: it.mail.ru/video/574

        «Pocker — GUI для Docker», Владимир Василькин (ALMWorks, Санкт-Петербург)

        Простое и быстрое развертывание продуктов Atlassian в различных конфигурациях и управление ими. При разработке плагинов для решений Atlassian у всех участников процесса часто возникает необходимость развернуть JIRA или Confluence в определенном окружении:

        • разработчики делают это для отладки;
        • QA — для тестирования;
        • служба поддержки — для воспроизведения проблем заказчика;
        • продажи — для демонстрации новой функциональности;
        • системные администраторы — для разработчиков, QA и остальных.

        Владимир рассказал о том, как с помощью OpenSource-инструмента Pocker можно просто и быстро поднимать подобные конфигурации, выбирая разные СУБД, версии, плагины, базы, а затем управлять ими — запускать/выключать, просматривать логи и так далее.

        Видеозапись выступления: it.mail.ru/video/575

        Заключение

        Нам самим очень понравилась техническая направленность митапа. Мы планируем проводить следующие встречи именно в этом формате, менеджерских мероприятий хватает и без нас. :)

        Большое спасибо всем участникам и особенно — докладчикам! Приходите в следующий раз.

        P. S. У нас есть профессиональное сообщество в социальных сетях, где мы обсуждаем использование продуктов Atlassian, обмениваемся опытом и публикуем информацию о связанных мероприятиях (в том числе и менеджерских).

        Присоединяйтесь:

        Комментарии (0)

          Let's block ads! (Why?)

          пятница, 13 мая 2016 г.

          Multiple dispatch в C#

          Бусидо Mobius: Путь участника

          Как вы уже знаете, по итогам Mobius 2015 мы получили множество отзывов: как приятных, так и наполненных разной критикой — как конструктивной, так и не очень. Одни участники положительно отзывались о содержании докладов, подборе спикеров и организационных моментах, другие сетовали на очередь на обед и обилие секьюрити в программе (об этом мы писали ранее).

          При подготовке Mobius 2016 мы постарались учесть рекомендации и пожелания участников прошлогодней конференции: 4 июня увидим, что из этого получилось. А пока давайте разберемся, за что наши участники любят Mobius, и почему люди приходят к нам из года в год.



          Мы рассмотрели три «стадии» участия в конференции и спросили наших участников, что они думают о каждом из них:

          Сомнения.


          Любая конференция до момента проведения, как и любая другая услуга, это воздух: вы не можете взять триальный вариант или пощупать ее перед покупкой. Именно поэтому любая конференция начинается с… сомнений! Да-да.

          Основная проблема в выборе технологической конференции заключается в том, чтобы отделить зерна от плевел – докладчики и организаторы любят хлесткие названия, не имеющие ничего общего с сутью выступления. В итоге получается так, что доклады среднестатистической мобильной конференции разделяются на три категории:

          1. Поверхностные обзоры инструментов и технологий, которые использует докладчик. Иногда не выдерживающие и пары каверзных вопросов.
          2. Реальные доклады-кейсы энтузиастов, посвященные решению практических сложностей разработчиков
          3. Маркетинговые доклады, которые выглядя как 2 категория, в середине которой выясняется, что вам пытаются впихнуть очередную непонятную тулзу.

          В итоге выясняется, что доклады второй категории займут меньше трети конференционного хронометража. Это опасение – один из основных страхов участника любой конференции, в том числе и Mobius.

          Дмитрий Зайцев, Android-разработчик, Яндекс:

          Основные страхи? Что в докладах будет много воды и маркетинга. Я до этого уже был на [другой конференции, ред.] – все довольно-таки поверхностно. Однако на Mobius этого было по минимуму, как мне показалось.

          И вот тут мы можем говорить о том, что в прошлый раз в нашу программу действительно просочились «продуктовые», в частности, пара тем по секьюрити. Это обстоятельство дало нам, как организаторам, понять одну простую истину – никому нельзя доверять! Именно поэтому в этом году мы тестируем доклады, делаем прогоны с оргкомитетом и отсеиваем много предложений.

          Вторая причитая сомнений – платность независимых конференций: сразу возникает вопрос, почему надо платить за доклады, на которых расскажут о том, о чем пишут в блогах, книгах, рассказывают в подкастах и вообще – легко можно нарыть где угодно. И это аргумент, если у вас есть время и силы – книги позволяют получать глубокие знания и мастерски осваивать нужные инструменты.

          Денис Загаевский, Senior Android Developer, Yandex:

          Да, каждый день сидишь за компом, смотришь что-то, читаешь – вся эта информация идет одним потоком с нереальной скоростью, из которого что-то конкретное почерпнуть довольно трудно. Если говорить про книги, то тут другой вопрос. Я люблю читать классику: паттерны, «Чистый код», алгоритмы и прочее. А из книг по Андроиду – так все быстро развивается, что книг актуальных-то нет. Может быть на английском еще можно что-то найти, то на русском ничего просто нет, устаревает, не успев выйти.

          А на конференции мы фактически из первых рук или от тех, кто реально вникал в технологию, изучал исходники, получаем такую себе выжимку. Более того, в доклады чаще всего включают что-то такое крутое, до чего ты, читая сам, сходу не доберешься. Когда тебе это реально понадобится, фичи из докладов ты скорее всего вспомнишь.


          Сергей Егоров, Руководитель отдела разработки ПО, корпорация СКАЙРОС:
          Я предпочитаю конференцию, и тому есть несколько причин. Во-первых, на конференции всегда есть место живому общению, которое подразумевает возможность диалога, когда у зрителя есть, по крайней мере, иллюзия того, что он может задать спикеру вопрос и получить исчерпывающий ответ. У книги ничего не спросишь, она ограничена тем, что в ней счел нужным написать автор. Во-вторых, я, к сожалению, не быстро читаю, и информацию «изо рта в уши» я получаю быстрее, чем при чтении. В-третьих, в книгах часто много «воды» (как в моих ответах:)), которой на докладах обычно нет. В-четвертых, если книга написана скучно, то можно и уснуть, а доклады скучные встречаются куда реже.

          Дмитрий:
          Если сравнивать с книгами, очевидно, что у конференции другие задачи, в частности – это налаживание личных контактов, тут ни книжки, ни интернет не помогут.

          С точки зрения получения знаний, конечно, книжки выигрывают в том плане, что в них все подробно расписано. С другой стороны, надо понимать: чтобы в книгах охватить тот объем тем и направлений, который поднимается на конференции, нужно потратить значительно больше времени.


          Программа.


          Сомнения могут быть развеяны только благодаря самой сути конференции – проработанной программе. В описаниях докладов, не только три слова названия, а развернутые тезисы; в описании докладчиков не только имя, а полный список достижений и причин, по которым этот человек может выступать.

          Если вы вернетесь к трем категориям докладов, которые можно встретить на конференциях, ни слова про хардкор в этом списке вы не найдете, потому что на мобильных конференциях такого не найти: никто не расскажет вам как на подкорке работает Android ART или подсистемы безопасности iOS – если вы не идете на Mobius.

          Дело в том, что наши конференции (JPoint, DotNext) зарождались как ответ на пустоту в нише жестких технологических конференций, и Mobius не стал исключением (например, в прошлом году во главу угла была поставлена тема security) и нашел отклик в умах разработчиков:

          Дмитрий:

          В первую очередь меня интересовали темы докладов – был хороший блок по безопасности, хотел его послушать. По факту, сравнивая с другими конференциями, я был приятно удивлен – было много конкретики, технических деталей, докладов по существу.

          Денис:
          Ждал интересных докладов – хотелось услышать что-то новое, в частности про Android – я под Android разрабатываю – пообщаться с людьми, какой-то опыт перенять. В то время я работал в другой компании и был единственным разработчиком – в таких условиях вообще трудно что-то новое освоить и тем более внедрить. В этом Mobius хорошо помог, позволил вырваться из рутины и обратить внимание на новые вещи.

          Сергей:
          Рынок мобильных разработок для нашей компании отнюдь не основной, но может стать весьма перспективным. Мне хотелось познакомиться с ним изнутри – послушать об острых задачах, стоящих перед разработчиками, и посмотреть на актуальные передовые технологии и подходы, применяемые для решения этих задач. Важно было, чтобы обо всем этом рассказывали непосредственные участники событий. Все мои чаяния сбылись.

          Кстати, программа Mobius 2016 уже готова, посмотреть ее можно на сайте конференции.

          Результат.


          Третья ступень участия в конференции – это ее «правильное» использование. Дело в том, что можно посетить конференцию, «отсидеть» и уйти, ничего не поняв и не получив. Конференция – огромный живой организм из сотен людей: это и прелесть, и огромный недостаток конференционного формата! Вы можете не успеть задать вопрос спикеру, пропустить интересный доклад или вообще умотаться к вечеру так, что последние доклады просто «не зайдут».

          Конференция это не волшебная пилюля, она не сделает вас асом в одночасье. И здесь важно понимать, к каким результатам нужно стремиться и в каком направлении работать:

          Дмитрий:

          За один день охватить несколько новых областей и спланировать стратегию на будущее – дорогого стоит. Если честно, в прошлый раз был так круто, что в этот раз я просто пошел и взял билет, недолго раздумывая.

          Денис:
          С прошлого Mobius уже год прошел, и я замечаю, что конференция дала мне несколько вещей которые остались со мной в моей работе и по сей день: например, я начал использовать RxJava – доклад Матвея Малькова все разложил по полочкам и позволил правильно подойти к изучению этого подхода; сделал вывод по безопасности о том, что один разработчик не можем сам по себе сделать защиту, которую не сломать, хотя познакомился с инструментами, которые такую защиту помогают обеспечить.

          Помните, участие в конференции – это работа! Думайте, какой доклад выбрать, задавайте вопросы, обсуждайте доклады со спикерами и коллегами в перерывах. Вы удивитесь, насколько открыты к общению участники конференций.

          О том, как выжить на хардкорной конференции от JUG.RU Group, рассказал Дмитрий dbelob Белобородов, постоянный участник наших конференций. Точнее, он составил настоящий мануал (или, если хотите, бусидо) по участию в технической конференции:

          До участия в конференции:

          1. Ознакомиться с текущим списком докладов. Она уже на 95% готова.
          2. После появления точной программы докладов с параллельными треками выбрать доклады для просмотра во время конференции.
          3. Организаторы должны прислать письмо с просьбой поделиться информацией о том, какие доклады планируете посетить. Эта информация нужна для уточнения размеров залов для конкретных докладов. Сетка докладов после этого может быть немного скорректирована.
          4. Ознакомиться с видео докладов предыдущих конференций на канале JUG.ru на YouTube. Большинство докладчиков ранее уже выступали на других мероприятиях. Заинтересовавшие выбранные новые доклады на Mobius могут являться продолжением или сходным по тематике с предыдущими докладами этих же или других докладчиков.
          5. Подписаться на Хабрахабре на блог компании JUG.ru Group. В нём уже опубликованы анонсы конференции, публикуются интервью со спикерами и новости.
          6. Подписаться на твиттер-аккаунт @MobiusConf для отслеживания новостей о предстоящей конференции.

          Во время конференции:
          1. Если планируете написать отзыв о конференции, рекомендуется взять фотоаппарат. Качество камер смартфонов недостаточно для публикации, например, на Хабрахабре.
          2. Перед началом конференции убедиться, что неожиданно не изменилась сетка докладов. Если такое произошло, скорректировать список и порядок посещения докладов.
          3. Если площадка проведения конференции не знакома, изучить план залов для быстрого перемещения с одного доклада на другой, кофе-брейки, обед.
          4. Не стесняться задавать вопросы докладчикам. Уместные интересные вопросы хорошо дополняют доклад и полезны как для слушателей, так и для докладчика.

          После конференции:
          1. Если есть желание и возможность написать отзыв о конференции – сделайте это. Желательно проиллюстрировать отзыв сделанными на конференции фотографиями. Наилучшее место публикации отзыва – Хабрахабр.
          2. Просматривать хэштег #mobiusconf в Twitter для получения отзывов от участников конференции.
          3. Оценить доклады конференции по присланной ссылке в письме от организаторов конференции.
          4. На Хабрахабре через некоторое время должен появиться обзор «Видео лучших докладов Mobius 2016», который поможет выбрать следующие видео для просмотра.
          5. Поделиться впечатлениями о конференции с друзьями и коллегами. Если конференция понравилась, не стесняйтесь порекомендовать её другим.

          В этом году программу конференции можно условно разделить на три раздела:
          • On the Edge, посвященный последним новостям, инструментам и технологиям мира мобильной разработки;
          • Software Craftsmanship, с докладами о практических решениях и подходах;
          • и традиционный Hardcore, с кишочками.

          В заключение напоминаем, что у нас традиционно будет организована онлайн-трансляция для тех, кому далеко/неудобно добираться до Петербурга, а в этом году появился тариф «Online-Corporate (до 10 подключений)», удобный для крупных компаний.

          Зарегистрироваться можно здесь.
          До встречи на Mobius!

          Комментарии (0)

            Let's block ads! (Why?)

            Особенности тестирования технологии C/R в Linux

            В 2012 году Эндрю Мортон был пессимистично настроен в отношении будущего проекта CRIU (Checkpoint and Restore In Userspace), когда принимал первые изменения в Linux ядро для поддержки C/R (Checkpoint/Restore). Идея реализовать функциональность сохранения и восстановления запущенных процессов в пространстве пользователя выглядела сумасшедшей, а спустя 4 года проект не только жив, а всё больше вызывает интерес к себе. До старта проекта CRIU предпринимались попытки реализовать C/R в Linux (DMTCP, BLCR, OpenVZ, CKPT и т.д.), но и все они по разным причинам были обречены на провал в то время как CRIU стал жизнеспособным проектом. К сожалению от этого задача C/R в Linux не стала проще. В этой статье я расскажу об особенностях тестирования CRIU.

            О пользе юнит-тестирования, применения систем непрерывной интеграции уже написаны книги и сотни статей. Эти приёмы известны каждому опытному разработчику и являются абсолютным стандартом для любого программного проекта. Поэтому мы не будем здесь описывать преимущества использования этих техник, а вместо этого расскажем только о нюансах, которые отличают CRIU от других проектов.

            Сам по себе процесс разработки CRIU ничем не отличается от разработки ядра Linux: каждое новое изменение это одна законченная мысль. Все патчи приходят в рассылку criu@, где подвергаются ревью. Патчи прошедшие ревью добавляются в репозиторий ментейнером проекта. Хотя на стадии ревью и выявляется часть проблем в коде, но свести их к нулю не получится из-за количества сценариев и конфигураций. Поэтому для выявления деградаций мы запускаем тесты для каждого нового изменения. Гарант постоянного запуска тестов это автоматические рабочие тесты.

            На ранних этапах разработки мы начали использовать функциональные тесты из тестового набора ZDTM (Zero DownTime Migration), которыми уже успешно тестировали in-kernel реализацию C/R в OpenVZ. Сейчас каждый тест из этого набора запускается отдельно и проходит 3 стадии: подготовка окружения, «демонизация» и ожидание сигнала (который сигнализирует тесту о том, что пора проверять своё состояние), проверка результата. Тесты условно разделены для две группы. Первая группа — это статические тесты, которые подготавливают некое постоянное окружение или состояние и «засыпают» в ожидании сигнала. Вторая группа — динамические тесты, которые постоянно меняют своё состояние и/или окружение (к примеру пересылают данные через TCP соединение). Если в 2012 году система юнит-тестирования CRIU насчитывала порядка 70 отдельных тестовых программ, то на сегодняшний день их количество увеличилось до 200. Но поистине ужасает количество комбинаций, которое требуется запустить для полноценного тестирования CRIU.

            Основная конфигурация — запуск всего набора тестов на хосте, при котором каждая тестовая программа садится в определённую позу, процесс теста сохраняют и восстанавливают и потом просят проверить в той же позе он остался или нет. Следующей по важности конфигурацией является проверка, что C/R не только работает, но и после C основной процесс не поломался. Поэтому каждый тест нужно прогнать ещё и в варианте когда выполнена только первая часть (без восстановления) и проверить, что поза соблюдена. Это безресторный тест. Восстановленный процесс может оказаться в той же позе, но не пригоден к повторному C/R. Так появляется ещё одна конфигурация — повторный C/R. Потом появляются конфигурации со снапшотами, C/R в окружении пространств имён, C/R с правами обычного пользователя, C/R с проверкой обратной совместимости, проверка успешного восстановления на BTRFS и NFS (потому что эти ФС имеют свои «особенности»). Но помимо C/R для отдельных процессов, можно делать групповые C/R — сохранение группы процессов, когда все процессы находятся в одной позе и когда каждый процесс находится в своей позе.

            CRIU поддерживает несколько аппаратных архитектур, сейчас это x86_64, ARM, AArch64, PPC64le и на подходе i386. Суровая реальность заставляет нас тестировать еще и несколько версий ядер: последний официальный релиз ванильного ядра, ядро RHEL7 (которое базируется на 3.11) и ветку linux-next. Длительность тестов небольшая (2-10 мин), но если учесть количество комбинаций существующих сценариев и возможных конфигураций, то получается внушительная цифра.

            Как я уже писал, тесты приносят пользу только тогда, когда ими регулярно пользуются. До некоторых пор мы запускали тесты вручную, но в какой-то момент поняли что локальное тестирование отнимает много времени у разработчиков и настроили систему непрерывного запуска тестов на каждое новое изменение.

            Мы используем две CI системы: Travis CI используется для проверки компиляции на всех поддерживаемых аппаратных архитектурах. Так как Travis CI использует ядро ниже версии 3.8, в котором отсутствует большая часть патчей, необходимых для CRIU, то Travis не подходит для запуска тестов и дополнительно мы используем широко известный Jenkins CI.

            Выводы

            • сборка, тестирование и измерения покрытия кода должны быть автоматизированы
            • много тестов не бывает, у нас соотношение полезного кода к тестовому кода примерно 1.6 (48 KLOC vs 30 KLOC) и есть куда стремиться
            • если количество конфигураций для тестирования огромно, приоритезируйте
            • рабочих рук как всегда не хватает, приходите к нам в CRIU, а?

            Проект CRIU был начат в 2012 году инженерами компании Virtuozzo, но позднее его поддержали и другие компании, заинтересованные в создании технологии C/R в Linux.

            Комментарии (0)

              Let's block ads! (Why?)