...

суббота, 9 ноября 2019 г.

Сказ об опасном std::enable_shared_from_this, или антипаттерн «Зомби»

В статье приводится опасный антипаттерн «Зомби», в некоторых ситуациях естественным образом возникающий при использовании std::enable_shared_from_this. Материал — где-то на стыке техники современного C++ и архитектуры.

Введение


C++11 предоставил разработчику замечательные инструменты для работы с памятью — умные указатели std::unique_ptr и связку std::shared_ptr + std::weak_ptr. Использование умных указателей по удобству и безопасности существенно перевешивает использование сырых указателей. Умные указатели широко применяются на практике, т.к. позволяют разработчику сосредоточиться на более высокоуровневых вопросах, чем отслеживание корректности создания/удаления динамически создаваемых сущностей.
Частью стандарта является также шаблон класса std::enable_shared_from_this, при первом знакомстве кажущийся довольно странным.
В статье пойдёт речь о том, как можно вляпаться при его использовании.

Ликбез


RAII и умные указатели

Прямое назначение умных указателей — заботиться об участке оперативной памяти, выделенной в куче. Умные указатели реализуют идиому RAII (Resource acquisition is initialization), и их с лёгкостью можно адаптировать для заботы о других типах ресурсов, требующих инициализации и нетривиальной деинициализации, таких как:
— файлы;
— временные папки на диске;
— сетевые соединения (http, websockets);
— потоки выполнения (threads);
— мьютексы;
— прочее (на что хватит фантазии).
Для такого обобщения достаточно написать класс (на самом деле иногда можно даже класс не писать, а просто воспользоваться deleter — но сегодня сказ не о том), осуществляющий:
— инициализацию в конструкторе либо отдельном методе;
— деинициализацию в деструкторе,
после чего «завернуть» его в соответствующий умный указатель в зависимости от требуемой модели владения — совместного (std::shared_ptr) либо единоличного (std::unique_ptr). При этом получается «двухслойное RAII»: умный указатель позволяет передавать/разделять владение ресурсом, а инициализацию/деинициализацию нестандартного ресурса осуществляет пользовательский класс.
std::shared_ptr использует механизм подсчёта ссылок. Стандартом определены счётчик сильных ссылок (подсчитывает количество существующих копий std::shared_ptr) и счётчик слабых ссылок (подсчитывает количество существующих экземпляров std::weak_ptr, созданных для данного экземпляра std::shared_ptr). Наличие хотя бы одной сильной ссылки гарантирует, что уничтожение ещё не произведено. Данное свойство std::shared_ptr широко применяется для обеспечения валидности объекта до тех пор, пока работа с ним не будет завершена во всех участках программы. Наличие же слабой ссылки не препятствует уничтожению объекта и позволяет получить сильную ссылку только до момента его уничтожения.
RAII гарантирует освобождение ресурса намного надёжнее, чем явный вызов delete/delete[]/free/close/reset/unlock, т.к.:
— явный вызов можно просто забыть;
— явный вызов можно ошибочно осуществить более одного раза;
— явный вызов сложен при реализации совместного владения ресурсом;
— механизм раскрутки стека в c++ гарантирует вызов деструкторов для всех объектов, выходящих из области видимости в случае возникновения исключения.
Гарантия деинициализации в идиоме настолько важна, что по-хорошему заслуживает места в названии идиомы наравне с инициализацией.
У умных указателей есть и недостатки:
— наличие накладных расходов по производительности и памяти (для большинства применений не является существенным);
— возможность возникновения циклических ссылок, блокирующих освобождение ресурса и приводящих к его утечке.
Наверняка каждый разработчик не раз читал про циклические ссылки и видел синтетические примеры проблемного кода.
Опасность может казаться несущественной по следующим причинам:
— если память утекает часто и много — это заметно по её расходу, а если редко и мало — то проблема вряд ли проявится на уровне конечного пользователя;
— используется динамический анализ кода на предмет утечек (Valgrind, Clang LeakSanitizer и т.п.);
— «я ж так не пишу»;
— «у меня архитектура правильная»;
— «у нас код проходит ревью».


std::enable_shared_from_this
В C++11 появился вспомогательный класс std::enable_shared_from_this. Для разработчика, успешно строившего код без std::enable_shared_from_this, потенциальные применения этого класса могут быть неочевидны.
Что же делает std::enable_shared_from_this?
Он позволяет функциям-членам класса, экземпляр которого создан в std::shared_ptr, получить дополнительные сильные (shared_from_this()) или слабые (weak_from_this(), начиная с C++17) копии того std::shared_ptr, в котором он был создан. Вызывать shared_from_this() и weak_from_this() из конструктора и деструктора нельзя.

Зачем так сложно? Можно же просто сконструировать std::shared_ptr<T>(this)
Нет, нельзя. Все std::shared_ptr'ы, заботящиеся об одном и том же экземпляре класса, должны использовать один блок подсчёта ссылок. Без специальной магии тут не обойтись.

Обязательным условием применения std::enable_shared_from_this является изначальное создание объекта класса в std::shared_ptr. Создание на стеке, динамическое выделение в куче, создание в std::unique_ptr — это всё не подходит. Только строго в std::shared_ptr.

А разве можно ограничить пользователя в способах создания экземпляров класса?
Да, можно. Для этого надо всего-навсего:
— предоставить статический метод для создания экземпляров, изначально размещённых в std::shared_ptr;
— поместить конструктор в private или protected;
— запретить copy- и move-семантику.
Класс зашёл в клетку, закрыл её на замок и проглотил ключ — с этих пор все его экземпляры будут жить только в std::shared_ptr, и не существует законных способов вытащить их оттуда.
Такое ограничение нельзя назвать хорошим архитектурным решением, но стандарту этот способ соответствует полностью.
Кроме того, можно использовать идиому PIMPL: единственный пользователь капризного класса — фасад — будет создавать реализацию строго в std::shared_ptr, а сам фасад уже будет лишён ограничений такого рода.

std::enable_shared_from_this имеет существенные нюансы при наследовании, но их обсуждение выходит за рамки статьи.


Ближе к делу


Все примеры кода, приведённые в статье, опубликованы на гитхабе.
Код демонстрирует плохие техники, замаскированные под обычное безопасное применение современного C++

SimpleCyclic


Вроде бы ничего не предвещает проблем. Объявление класса выглядит просто и понятно. За исключением одной «мелкой» детали — зачем-то применено наследование от std::enable_shared_from_this.
SimpleCyclic.h
#pragma once

#include <memory>
#include <functional>

namespace SimpleCyclic {
class Cyclic final : public std::enable_shared_from_this<Cyclic>
{
public:
    static std::shared_ptr<Cyclic> create();

    Cyclic(const Cyclic&) = delete;
    Cyclic(Cyclic&&) = delete;
    Cyclic& operator=(const Cyclic&) = delete;
    Cyclic& operator=(Cyclic&&) = delete;

    ~Cyclic();

    void doSomething();

private:
    Cyclic();

    std::function<void(void)> _fn;
};
} // namespace SimpleCyclic


А в реализации:
SimpleCyclic.cpp
#include <iostream>

#include "SimpleCyclic.h"

namespace SimpleCyclic {
Cyclic::Cyclic() = default;

Cyclic::~Cyclic()
{
    std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
}

std::shared_ptr<Cyclic> Cyclic::create()
{
    return std::shared_ptr<Cyclic>(new Cyclic);
}

void Cyclic::doSomething()
{
    _fn = [shis = shared_from_this()](){};

    std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
}
} // namespace SimpleCyclic


main.cpp
#include "SimpleCyclic/SimpleCyclic.h"

int main()
{
    auto simpleCyclic = SimpleCyclic::Cyclic::create();
    simpleCyclic->doSomething();

    return 0;
}


Вывод в консоль
N12SimpleCyclic6CyclicE::doSomething


В теле функции doSomething() экземпляр класса сам создёт дополнительную сильную копию того std::shared_ptr, в котором он был размещён. Затем эта копия с помощью обобщённого захвата помещается в лямбда-функцию, присваиваемую полю данных класса под видом безобидного std::function. Вызов doSomething() приводит к возникновению циклической ссылки, и экземпляр класса уже не будет разрушен даже после уничтожения всех внешних сильных ссылок.
Возникает утечка памяти. Деструктор SimpleCyclic::Cyclic::~Cyclic не вызывается.

Экземпляр класса «держит» себя сам.
Код завязался в узел.


(изображение взято отсюда)

И что, это и есть антипаттерн «Зомби»?
Нет, это только разминка. Всё самое интересное ещё впереди.

Зачем разработчик такое написал?
Пример синтетический. Мне не известны какие-либо ситуации, в которых гармонично получался бы такой код.

И что, неужели динамический анализ кода промолчал?
Нет, Valgrind честно сообщил о состоявшейся утечке памяти:

Сообщение Valgrind
96 (64 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 29 of 46
in SimpleCyclic::Cyclic::create() in /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
1: malloc in /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: operator new(unsigned long) in /usr/lib/libc++abi.dylib
3: SimpleCyclic::Cyclic::create() in /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
4: main in /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/main.cpp:5


PimplCyclic


В данном случае заголовочный файл выглядит совершенно корректно и лаконично. В нём объявлен фасад, хранящий некую реализацию в std::shared_ptr. Наследование — в том числе от std::enable_shared_from_this — отсутствует, в отличие от прошлого примера.
PimplCyclic.h
#pragma once

#include <memory>

namespace PimplCyclic {
class Cyclic
{
public:
    Cyclic();
    ~Cyclic();

private:
    class Impl;
    std::shared_ptr<Impl> _impl;
};
} // namespace PimplCyclic


А в реализации:
PimplCyclic.cpp
#include <iostream>
#include <functional>

#include "PimplCyclic.h"

namespace PimplCyclic {

class Cyclic::Impl : public std::enable_shared_from_this<Cyclic::Impl>
{
public:
    ~Impl()
    {
        std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
    }

    void doSomething()
    {
        _fn = [shis = shared_from_this()](){};

        std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
    }

private:
    std::function<void(void)> _fn;
};

Cyclic::Cyclic()
    : _impl(std::make_shared<Impl>())
{
    if (_impl) {
        _impl->doSomething();
    }
}

Cyclic::~Cyclic()
{
    std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
}
} // namespace PimplCyclic


main.cpp
#include "PimplCyclic/PimplCyclic.h"

int main()
{
    auto pimplCyclic = PimplCyclic::Cyclic();

    return 0;
}


Вывод в консоль
N11PimplCyclic6Cyclic4ImplE::doSomething
N11PimplCyclic6CyclicE::~Cyclic


Вызов Impl::doSomething() приводит к образованию циклической ссылки в экземпляре класса Impl. Фасад уничтожается корректно, а вот реализация утекает. Деструктор PimplCyclic::Cyclic::Impl::~Impl не вызывается.
Пример опять синтетический, но на сей раз более опасный — вся плохая техника расположена в реализации и никак не проявляется в объявлении.
Более того, для возникновения циклической ссылки от пользовательского кода не потребовалось никаких действий, кроме конструирования.
Динамический анализ в лице Valgrind и в этот раз выявил утечку:
Сообщение Valgrind
96 bytes in 1 blocks are definitely lost in loss record 29 of 46
in PimplCyclic::Cyclic::Cyclic() in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
1: malloc in /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: operator new(unsigned long) in /usr/lib/libc++abi.dylib
3: std::__1::__libcpp_allocate(unsigned long, unsigned long) in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/new:252
4: std::__1::allocator<std::__1::__shared_ptr_emplace<PimplCyclic::Cyclic::Impl, std::__1::allocator<PimplCyclic::Cyclic::Impl> > >::allocate(unsigned long, void const*) in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:1813
5: std::__1::shared_ptr<PimplCyclic::Cyclic::Impl> std::__1::shared_ptr<PimplCyclic::Cyclic::Impl>::make_shared<>() in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4326
6: _ZNSt3__1L11make_sharedIN11PimplCyclic6Cyclic4ImplEJEEENS_9enable_ifIXntsr8is_arrayIT_EE5valueENS_10shared_ptrIS5_EEE4typeEDpOT0_ in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4706
7: PimplCyclic::Cyclic::Cyclic() in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
8: PimplCyclic::Cyclic::Cyclic() in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:29
9: main in /Users/User/Projects/Zomby_antipattern_concept/PimplCyclic/main.cpp:5


Немного подозрительно видеть Pimpl, в котором реализация хранится в std::shared_ptr.
Классический Pimpl на базе сырого указателя слишком архаичен, а std::unique_ptr имеет побочный эффект в виде распространения запрета copy-семантики на фасад. Такой фасад будет реализовывать идиому единоличного владения, что может не соответствовать архитектурной задумке. Из применения std::shared_ptr для хранения реализации следует сделать вывод, что класс задуман для обеспечения совместного владения.

Чем это отличается от классической утечки — выделения памяти с помощью явного вызова new без последующего удаления? Точно так же в интерфейсе было бы всё красиво, а в реализации — баг.
Мы тут обсуждаем современные способы прострелить себе ногу.

Антипаттерн «Зомби»


Итак, из вышеприведённого материала понятно:
— умные указатели могут завязываться в узлы;
— применение std::enable_shared_from_this может этому способствовать, т.к. позволяет экземпляру класса завязаться в узел почти без посторонней помощи.

А теперь — внимание — ключевой вопрос статьи: имеет ли значение тип ресурса, завёрнутого в умный указатель? Есть ли разница между RAII-заботой о файле и RAII-заботой об HTTPS-соединении в асинхронном исполнении?

SimpleZomby


Общий для всех последующих примеров зомби код вынесен в библиотеку Common.

Абстрактный интерфейс зомби со скромным названием Manager:

Common/Manager.h
#pragma once

#include <memory>

namespace Common {
class Listener;

class Manager
{
public:
    Manager() = default;
    Manager(const Manager&) = delete;
    Manager(Manager&&) = delete;
    Manager& operator=(const Manager&) = delete;
    Manager& operator=(Manager&&) = delete;

    virtual ~Manager() = default;

    virtual void runOnce(std::shared_ptr<Common::Listener> listener) = 0;
};
} // namespace Common


Абстрактный интерфейс Listener'a, готового потокобезопасно принимать текст:
Common/Listener.h
#pragma once

#include <string>
#include <memory>

namespace Common {
class Listener
{
public:
    virtual ~Listener() = default;

    using Data = std::string;

    // thread-safe
    virtual void processData(const std::shared_ptr<const Data> data) = 0;
};
} // namespace Common


Listener, отображающий текст в консоль. Реализует концепцию SingletonShared из моей статьи Техника избежания неопределённого поведения при обращении к синглтону:
Common/Impl/WriteToConsoleListener.h
#pragma once

#include <mutex>

#include "Common/Listener.h"

namespace Common {
class WriteToConsoleListener final : public Listener
{
public:
    WriteToConsoleListener(const WriteToConsoleListener&) = delete;
    WriteToConsoleListener(WriteToConsoleListener&&) = delete;
    WriteToConsoleListener& operator=(const WriteToConsoleListener&) = delete;
    WriteToConsoleListener& operator=(WriteToConsoleListener&&) = delete;

    ~WriteToConsoleListener() override;

    static std::shared_ptr<WriteToConsoleListener> instance();

    // blocking
    void processData(const std::shared_ptr<const Data> data) override;

private:
    WriteToConsoleListener();

    std::mutex _mutex;
};
} // namespace Common


Common/Impl/WriteToConsoleListener.cpp
#include <iostream>

#include "WriteToConsoleListener.h"

namespace Common {
WriteToConsoleListener::WriteToConsoleListener() = default;

WriteToConsoleListener::~WriteToConsoleListener()
{
    auto lock = std::lock_guard(_mutex);
    std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
}

std::shared_ptr<WriteToConsoleListener> WriteToConsoleListener::instance()
{
    static auto inst = std::shared_ptr<WriteToConsoleListener>(new WriteToConsoleListener);
    return inst;
}

void WriteToConsoleListener::processData(const std::shared_ptr<const Data> data)
{
    if (data) {
        auto lock = std::lock_guard(_mutex);
        std::cout << *data << std::flush;
    }
}

} // namespace Common


И, наконец, первый зомби, самый простой и бесхитростный.
SimpleZomby.h
#pragma once

#include <memory>
#include <atomic>
#include <thread>

#include "Common/Manager.h"

namespace Common {
class Listener;
} // namespace Common

namespace SimpleZomby {
class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby>
{
public:
    static std::shared_ptr<Zomby> create();

    ~Zomby() override;

    void runOnce(std::shared_ptr<Common::Listener> listener) override;

private:
    Zomby();

    using Semaphore = std::atomic<bool>;

    std::shared_ptr<Common::Listener> _listener;
    Semaphore _semaphore = false;
    std::thread _thread;
};
} // namespace SimpleZomby


SimpleZomby.cpp
#include <sstream>

#include "SimpleZomby.h"
#include "Common/Listener.h"

namespace SimpleZomby {
std::shared_ptr<Zomby> Zomby::create()
{
    return std::shared_ptr<Zomby>(new Zomby());
}

Zomby::Zomby() = default;

Zomby::~Zomby()
{
    _semaphore = false;

    _thread.detach();

    if (_listener) {
        std::ostringstream buf;
        buf << typeid(*this).name() << "::" << __func__ << std::endl;
        _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));
    }
}

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener)
{
    if (_semaphore) {
        throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice");
    }

    _listener = listener;
    _semaphore = true;

    _thread = std::thread([shis = shared_from_this()](){
        while (shis && shis->_listener && shis->_semaphore) {
            shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n"));
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });
}
} // namespace SimpleZomby


Зомби запускает в отдельном потоке лямбда-функцию, периодически отправляющую строку в listener. Лямбда-функции для работы нужны семафор и listener, являющиеся полями класса зомби. Лямбда-функция не захватывает их как отдельные поля, а использует объект в качестве агрегатора. Уничтожение экземпляра класса зомби до завершения работы лямбда-функции приведёт к неопределённому поведению. Чтобы этого избежать, лямбда-функция захватывает сильную копию shared_from_this().
В деструкторе зомби семафор устанавливается в false, после чего вызывается detach() для потока. Установка семафора сообщает потоку о необходимости завершения работы.

В деструкторе надо было вызывать не detach(), а join()!
… и получить деструктор, блокирующий выполнение на неопределённое время, что может являться неприемлемым.

Так это же нарушение RAII! RAII должно было выйти из деструктора только после освобождения ресурса!
Если строго — то да, деструктор зомби не осуществляет освобождение ресурса, а только гарантирует, что освобождение будет произведено. Когда-нибудь произведено — может скоро, а может и не очень. И возможно даже, что main завершит работу раньше — тогда поток будет принудительно зачищен операционной системой. Но на самом деле, грань между «правильным» и «неправильным» RAII может быть очень тонкой: например, «правильное» RAII, осуществляющее в деструкторе вызов std::filesystem::remove() для временного файла, вполне может вернуть управление в тот момент, когда команда на запись ещё будет находиться в каком-нибудь из энергозависимых кэшей и не будет честно записана на магнитную пластину жёсткого диска.

main.cpp
#include <chrono>
#include <thread>
#include <sstream>

#include "Common/Impl/WriteToConsoleListener.h"
#include "SimpleZomby/SimpleZomby.h"

int main()
{
    auto writeToConsoleListener = Common::WriteToConsoleListener::instance();

    {
        auto simpleZomby = SimpleZomby::Zomby::create();
        simpleZomby->runOnce(writeToConsoleListener);

        std::this_thread::sleep_for(std::chrono::milliseconds(4500));
    } // Zomby should be killed here

    {
        std::ostringstream buf;
        buf << "============================================================\n"
            << "|                      Zomby was killed                    |\n"
            << "============================================================\n";
        if (writeToConsoleListener) {
            writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str()));
        }
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(5000));

    return 0;
}


Вывод в консоль
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
============================================================
| Zomby was killed |
============================================================
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!
SimpleZomby is alive!


Что видно из вывода программы:
— зомби продолжил работу даже после выхода из области видимости;
— не были вызваны деструкторы ни для зомби, ни для WriteToConsoleListener.
Возникла утечка памяти.
Возникла утечка ресурса. А ресурс в данном случае — поток выполнения.
Код, который должен был остановиться, продолжил работу в отдельном потоке.
Утечку WriteToConsoleListener можно было бы предотвратить применением техники SingletonWeak из моей статьи Техника избежания неопределённого поведения при обращении к синглтону, но я намеренно не стал этого делать.


(изображение взято отсюда)

Почему «Зомби»?
Потому что его убили, а он всё ещё жив.

Чем это отличается от циклических ссылок из предыдущих примеров?
Тем, что потерянный ресурс — это не просто участок памяти, а нечто, самостоятельно выполняющее код независимо от запустившего его потока.

Можно ли уничтожить «Зомби»?
После выхода из области видимости (т.е. после уничтожения всех внешних сильных и слабых ссылок на зомби) — нельзя. Зомби уничтожится тогда, когда сам решит уничтожиться (да-да, это же нечто с активным поведением), возможно — никогда, т.е. доживёт до момента зачистки операционной системой при завершении приложения. Конечно, пользовательский код может иметь какое-то влияние на условие выхода из зомби-кода, но это влияние будет опосредованным и зависящим от реализации.

А до выхода из области видимости?
Можно явно вызвать деструктор зомби, но при этом вряд ли удастся избежать неопределённого поведения из-за повторного уничтожения объекта ещё и деструктором умного указателя — это борьба с RAII. Или можно добавить функцию явной деинициализации — а это отказ от RAII.

Чем это отличается от простого запуска потока с последущим detach()?
В случае с зомби, в отличие от простого вызова detach(), присутствует задумка на остановку потока. Только она не срабатывает. Присутствие правильной задумки способствует маскировке проблемы.

Пример всё ещё синтетический?
Частично. В данном простом примере не было достаточных оснований для применения shared_from_this() — например, можно было обойтись захватом weak_from_this() или захватом всех нужных полей класса. Но при усложнении задачи баланс может смещаться в сторону
shared_from_this().

Valgrind, Valgrind! У нас же есть дополнительная линия защиты от зомби!
Увы и ах — но Valgrind не выявил утечку памяти. Почему — я не знаю. В диагностике присутствуют только записи «possibly lost», указывающие на системные функции — примерно такие же и примерно в том же количестве, что и при отработке пустого main. Указания на пользовательский код отсутствуют. Возможно, другие инструменты динамического анализа справились бы лучше, но если Вы всё ещё надеетесь на них — читайте дальше.

SteppingZomby


Код в данном примере продвигается по шагам resolveDnsName ---> connectTcp ---> establishSsl ---> sendHttpRequest ---> readHttpReply, имитируя работу клиентского HTTPS-соединения в асинхронном исполнении. Каждый шаг занимает примерно секунду.
SteppingZomby.h
#pragma once

#include <memory>
#include <atomic>
#include <thread>

#include "Common/Manager.h"

namespace Common {
class Listener;
} // namespace Common

namespace SteppingZomby {
class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby>
{
public:
    static std::shared_ptr<Zomby> create();

    ~Zomby() override;

    void runOnce(std::shared_ptr<Common::Listener> listener) override;

private:
    Zomby();

    using Semaphore = std::atomic<bool>;

    std::shared_ptr<Common::Listener> _listener;
    Semaphore _semaphore = false;
    std::thread _thread;

    void resolveDnsName();
    void connectTcp();
    void establishSsl();
    void sendHttpRequest();
    void readHttpReply();
};
} // namespace SteppingZomby


SteppingZomby.cpp
#include <sstream>
#include <string>

#include "SteppingZomby.h"
#include "Common/Listener.h"

namespace {
void doSomething(Common::Listener& listener, std::string&& callingFunctionName)
{
    listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " started\n"));
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " finished\n"));
}
} // namespace

namespace SteppingZomby {
Zomby::Zomby() = default;

std::shared_ptr<Zomby> Zomby::create()
{
    return std::shared_ptr<Zomby>(new Zomby());
}

Zomby::~Zomby()
{
    _semaphore = false;

    _thread.detach();

    if (_listener) {
        std::ostringstream buf;
        buf << typeid(*this).name() << "::" << __func__ << std::endl;
        _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));
    }
}

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener)
{
    if (_semaphore) {
        throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice");
    }

    _listener = listener;
    _semaphore = true;

    _thread = std::thread([shis = shared_from_this()](){
        if (shis && shis->_listener && shis->_semaphore) {
            shis->resolveDnsName();
        }
        if (shis && shis->_listener && shis->_semaphore) {
            shis->connectTcp();
        }
        if (shis && shis->_listener && shis->_semaphore) {
            shis->establishSsl();
        }
        if (shis && shis->_listener && shis->_semaphore) {
            shis->sendHttpRequest();
        }
        if (shis && shis->_listener && shis->_semaphore) {
            shis->readHttpReply();
        }
    });
}

void Zomby::resolveDnsName()
{
    doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__);
}

void Zomby::connectTcp()
{
    doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__);
}

void Zomby::establishSsl()
{
    doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__);
}

void Zomby::sendHttpRequest()
{
    doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__);
}

void Zomby::readHttpReply()
{
    doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__);
}
} // namespace SteppingZomby


main.cpp
#include <chrono>
#include <thread>
#include <sstream>

#include "SteppingZomby/SteppingZomby.h"
#include "Common/Impl/WriteToConsoleListener.h"

int main()
{
    auto writeToConsoleListener = Common::WriteToConsoleListener::instance();

    {
        auto steppingZomby = SteppingZomby::Zomby::create();
        steppingZomby->runOnce(writeToConsoleListener);

        std::this_thread::sleep_for(std::chrono::milliseconds(1500));
    } // Zombies should be killed here

    {
        std::ostringstream buf;
        buf << "============================================================\n"
            << "|                      Zomby was killed                    |\n"
            << "============================================================\n";
        if (writeToConsoleListener) {
            writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str()));
        }
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(5000));

    return 0;
}


Вывод в консоль
N13SteppingZomby5ZombyE::resolveDnsName started
N13SteppingZomby5ZombyE::resolveDnsName finished
N13SteppingZomby5ZombyE::connectTcp started
============================================================
| Zomby was killed |
============================================================
N13SteppingZomby5ZombyE::connectTcp finished
N13SteppingZomby5ZombyE::establishSsl started
N13SteppingZomby5ZombyE::establishSsl finished
N13SteppingZomby5ZombyE::sendHttpRequest started
N13SteppingZomby5ZombyE::sendHttpRequest finished
N13SteppingZomby5ZombyE::readHttpReply started
N13SteppingZomby5ZombyE::readHttpReply finished
N13SteppingZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener


Как и в предыдущем примере, вызов runOnce() привёл к возникновению циклической ссылки.
Но на этот раз деструкторы Zomby и WriteToConsoleListener были вызваны. Все ресурсы были корректно освобождены до момента завершения приложения. Утечки памяти не произошло.

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

Ну подумаешь, получили никому не нужный ответ....
В случае с клиентским HTTPS-соединением последствия на нашей стороне могут быть следующими:
— расход памяти;
— расход процессора;
— расход TCP-портов;
— расход полосы пропускания канала связи (как запрос, так и ответ могут быть объёмом в мегабайты);
— нежданные данные могут нарушить работу вышестоящей бизнес-логики — вплоть до перехода на неправильную ветвь выполнения или до неопределённого поведения, т.к. механизмы обработки ответа могут быть уже уничтожены.
А на удалённой стороне (не забывайте — HTTPS-запрос кому-то предназначался) — точно такая же растрата ресурсов, плюс возможно:
— опубликование фотографий котиков на корпоративном сайте;
— отключение тёплого пола у Вас на кухне;
— исполнение торгового приказа на бирже;
— перевод денег с Вашего счёта;
— запуск межконтинентальной баллистической ракеты.
Бизнес-логика пыталась остановить зомби, удалив все сильные и слабые ссылки на него. Остановка продвижения HTTPS-запроса должна была произойти — было ещё не слишком поздно, данные прикладного уровня ещё не были отправлены.
Но зомби решил по-своему.

Бизнес-логика может создавать новые объекты на место зомби и снова пытаться их уничтожить, кратно увеличивая утечку ресурсов.
В случае с длящимся процессом (например, Websocket-соединением) растрата ресурсов может продолжаться часами, а при наличии в реализации механизма авто-переподключения при обрыве соединения — вообще до остановки программы.

Valgrind?
Без шансов. Всё корректно освобождено и подчищено. Поздно и не из главного потока, но полностью корректно.

BoozdedZomby


В данном примере используется библиотека boozd::azzio, являющаяся имитацией boost::asio. Несмотря на то, что имитация довольно грубая, она позволяет продемонстрировать суть проблемы. В библиотеке есть функция io_context::async_read (в оригинале она свободная, но сути это не меняет), принимающая:
— stream, из которого могут приходить данные;
— буфер, позволяющий эти данные накапливать;
— callback-функцию, которая будет вызвана по завершении считывания данных.
Функция io_context::async_read выполняется мгновенно и никогда не вызывает callback, даже если результат выполнения уже известен (например, ошибка). Вызов коллбэка происходит только из блокирующей функции io_context::run() (в оригинале есть и другие функции, предназначенные для вызова коллбэков по мере готовности данных).
buffer.h
#pragma once

#include <vector>

namespace boozd::azzio {
using buffer = std::vector<int>;
} // namespace boozd::azzio


stream.h
#pragma once

#include <optional>

namespace boozd::azzio {
class stream
{
public:
    virtual ~stream() = default;

    virtual std::optional<int> read() = 0;
};
} // namespace boozd::azzio


io_context.h
#pragma once

#include <functional>
#include <optional>

#include "buffer.h"

namespace boozd::azzio {
class stream;

class io_context
{
public:
    ~io_context();

    enum class error_code {no_error, good_error, bad_error, unknown_error, known_error, well_known_error};
    using handler = std::function<void(error_code)>;

    // Start an asynchronous operation to read a certain amount of data from a stream.
    // This function is used to asynchronously read a certain number of bytes of data from a stream.
    // The function call always returns immediately.
    void async_read(stream& s, buffer& b, handler&& handler);

    // Run the io_context object's event processing loop.
    void run();

private:
    using pack = std::tuple<stream&, buffer&>;
    using pack_optional = std::optional<pack>;
    using handler_optional = std::optional<handler>;

    pack_optional _pack_optional;
    handler_optional _handler_optional;
};
} // namespace boozd::azzio


io_context.cpp
#include <iostream>
#include <thread>
#include <chrono>

#include "io_context.h"
#include "stream.h"

namespace boozd::azzio {
io_context::~io_context()
{
    std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
}

void io_context::async_read(stream& s, buffer& b, io_context::handler&& handler)
{
    _pack_optional.emplace(s, b);
    _handler_optional.emplace(std::move(handler));
}

void io_context::run()
{
    if (_pack_optional && _handler_optional) {
        auto& [s, b] = *_pack_optional;
        using namespace std::chrono;
        auto start = steady_clock::now();
        while (duration_cast<milliseconds>(steady_clock::now() - start).count() < 1000) {
            if (auto read = s.read())
                b.emplace_back(*read);
            std::this_thread::sleep_for(milliseconds(100));
        }

        (*_handler_optional)(error_code::no_error);
    }
}
} // namespace boozd::azzio


Единственная реализация интерфейса boozd::azzio::stream, выдающая случайные данные:
impl/random_stream.h
#pragma once

#include "boozd/azzio/stream.h"

namespace boozd::azzio {
class random_stream final : public stream
{
public:
    ~random_stream() override;

    std::optional<int> read() override;
};
} // namespace boozd::azzio


impl/random_stream.cpp
#include <iostream>

#include "random_stream.h"

namespace boozd::azzio {
boozd::azzio::random_stream::~random_stream()
{
    std::cout << typeid(*this).name() << "::" << __func__ << std::endl;
}

std::optional<int> random_stream::read()
{
    if (!(rand() & 0x1))
        return rand();

    return std::nullopt;
}
} // namespace boozd::azzio


BoozdedZomby запускает в отдельном потоке лямбда-функцию. Лямбда-функция регистрирует обработчик с помощью вызова async_read(), после чего отдаёт управление внутренним механизмам boozd::azzio с помощью run(). После этого внутренние механизмы boozd::azzio могут производить обращения к буферу и потоку (источнику данных) в любой момент до вызова callback-функции. Для обеспечения гарантии валидности множества объектов, агрегированных в экземпляре класса, лямбда-функция захватывает shared_from_this.
BoozdedZomby.h
#pragma once

#include <memory>
#include <atomic>
#include <thread>

#include "Common/Manager.h"
#include "boozd/azzio/buffer.h"
#include "boozd/azzio/io_context.h"
#include "boozd/azzio/impl/random_stream.h"

namespace Common {
class Listener;
} // namespace Common

namespace BoozdedZomby {
class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby>
{
public:
    static std::shared_ptr<Zomby> create();

    ~Zomby() override;

    void runOnce(std::shared_ptr<Common::Listener> listener) override;

private:
    Zomby();

    using Semaphore = std::atomic<bool>;

    Semaphore _semaphore = false;
    std::shared_ptr<Common::Listener> _listener;
    boozd::azzio::random_stream _stream;
    boozd::azzio::buffer _buffer;
    boozd::azzio::io_context _context;
    std::thread _thread;
};
} // namespace BoozdedZomby


BoozdedZomby.cpp
#include <iostream>
#include <sstream>

#include "boozd/azzio/impl/random_stream.h"
#include "BoozdedZomby.h"
#include "Common/Listener.h"

namespace BoozdedZomby {
Zomby::Zomby() = default;

std::shared_ptr<Zomby> Zomby::create()
{
    return std::shared_ptr<Zomby>(new Zomby());
}

Zomby::~Zomby()
{
    if (_semaphore && _thread.joinable()) {
        if (_thread.get_id() == std::this_thread::get_id()) {
            _thread.detach();
        } else {
            _semaphore = false;
            _thread.join();
        }
    }

    if (_listener) {
        std::ostringstream buf;
        buf << typeid(*this).name() << "::" << __func__ << std::endl;
        _listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));
    }
}

void Zomby::runOnce(std::shared_ptr<Common::Listener> listener)
{
    if (_semaphore) {
        throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice");
    }

    _listener = listener;
    _semaphore = true;

    _thread = std::thread([shis = shared_from_this()]() {
        while (shis && shis->_semaphore && shis->_listener) {
            auto handler = [shis](auto errorCode) {
                if (shis && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) {
                    std::ostringstream buf;
                    buf << "BoozdedZomby has got a fresh data: ";
                    for (auto const &elem : shis->_buffer)
                        buf << elem << ' ';
                    buf << std::endl;

                    shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str()));
                }
            };
            shis->_buffer.clear();
            shis->_context.async_read(shis->_stream, shis->_buffer, handler);
            shis->_context.run();
        }
    });
}
} // namespace BoozdedZomby


main.cpp
#include <chrono>
#include <thread>
#include <sstream>

#include "BoozdedZomby/BoozdedZomby.h"
#include "Common/Impl/WriteToConsoleListener.h"

int main()
{
    auto writeToConsoleListener = Common::WriteToConsoleListener::instance();

    {
        auto boozdedZomby = BoozdedZomby::Zomby::create();
        boozdedZomby->runOnce(writeToConsoleListener);

        std::this_thread::sleep_for(std::chrono::milliseconds(4500));
    } // Zombies should be killed here

    {
        std::ostringstream buf;
        buf << "============================================================\n"
            << "|                      Zomby was killed                    |\n"
            << "============================================================\n";
        if (writeToConsoleListener) {
            writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str()));
        }
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(5000));

    return 0;
}


Вывод в консоль
BoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
============================================================
| Zomby was killed |
============================================================
BoozdedZomby has got a fresh data: 1927702196 130060903 1083454666 2118797801 2035308228 824938981
BoozdedZomby has got a fresh data: 2020739063 1635339425 34075629
BoozdedZomby has got a fresh data: 2146319451 500782188 1269406752 884936716 892053144
BoozdedZomby has got a fresh data: 330111137 1723153177 1070477904
BoozdedZomby has got a fresh data: 343098142 280090412 589673557 889688008 2014119113 388471006


В результате вызова run_once() возникла циклическая ссылка. Зомби продолжил работу даже после выхода из области видимости. Не были вызваны деструкторы для множества объектов, созданных в ходе работы программы:
— boozdedZomby;
— writeToConsoleListener;
— полей данных зомби.
Возникла утечка памяти.
Возникла утечка ресурса.

Чем этот пример отличается от предыдущих?
Он намного ближе к реальному коду. Это уже совсем не синтетический пример. Такой код вполне может естественным образом возникать при использовании boost::asio. Более того, его не получится исправить простым отказом от захвата сильной ссылки в пользу слабой — это помешает обеспечению валидности буфера и потока (источника данных).

Valgrind?
Мимо. Хотя вроде бы должен был обнаружить утечки.

Зомби в дикой природе


Проблема надуманная! Так никто не пишет!
Ещё как пишет.
Пример HTTP-клиента
Пример Websocket-клиента
Официальная документация на boost учит, как написать гибрид BoozdedZomby + SteppingZomby. Остановить его невозможно, но никто и не пытается. Конкретно в демонстрационном коде основное свойство зомби не проявляется, но стоит перенести это в production — и вот Вы уже ходите вдоль края, скорее всего даже на тёмной стороне.

Можно остановить зомби, уничтожив экземпляр boost::asio::io_context!
… попутно уничтожив ещё n сущностей (возможно, не-зомби), живущих в данном контексте.

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

Заключение


Конечно, в статье описаны не все разновидности антипаттерна «Зомби».

Он может встречаться как в виде гибридов вышеприведённых типов, так и в виде новых самостоятельных типов.

Антипаттерн может возникать не только при запуске std::thread в Вашем коде — эту часть работы может взять на себя сторонняя многопоточная библиотека.

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

Архитектура может быть как event-driven, так и на основе периодического опроса состояний (polling-based).

Это всё не очень важно.

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

Динамический анализ кода может оказаться не в силах обнаруживать этот антипаттерн, особенно его разновидность SteppingZomby. На статический анализ тоже надежды мало — очень уж тонкая грань между корректным и некорректным использованием shared_from_this (все примеры кода, приведённые в статье, можно исправить внесением очень небольших правок — всего от 1 до 6 строк кода).

Автотесты могут помочь в его выявлении и проверке корректности устранения — но для этого надо знать, что искать. Совершенно точно знать.

Искать антипаттерн, сюдя по всему, придётся вручную. А для этого надо пересматривать все применения std::enable_shared_from_this — они очень опасны.

Let's block ads! (Why?)

Обсуждение: работа интернета держится на open source — какие аргументы есть у критиков

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


Фото — James Sutton — Unsplash

Open source — основа интернета


По данным Linux Foundation, 72% компаний из Fortune 2000 используют open source инструменты для решения своих задач. При этом 55% задействуют открытый код в коммерческих продуктах. Открытое ПО распространено в дата-центрах — например, с ним работают Facebook, Rackspace, NASA и AT&T. Ряд облачных провайдеров и ИТ-компаний даже основал организацию Open Compute Project. Она разрабатывает открытый стандарт архитектуры серверных стоек (Open Rack) и требования к модульному серверу для облачных дата-центров (OpenCloud Server).

Значительная часть популярных open source продуктов — это масштабные проекты вроде Kubernetes, TensorFlow или Ansible. Их разрабатывают и финансируют крупные ИТ-компании. Но есть и небольшие продукты (например, cURL), которые поддерживают энтузиасты. Часто они делают это на добровольной основе и в свободное время. И здесь кроются подводные камни.

Почему такую модель критикуют


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

Значительную часть изменений в open source проект вносит или маленькая команда, или один мейнтейнер. Например, из 25 тыс. коммитов в репозитории cURL, 14 тыс. принадлежат автору — Даниэлю Стенбергу (Daniel Stenberg). Долгое время число разработчиков библиотеки OpenSSL не превышало четырех человек. Большую часть коммитов сделал один из них — Стив Хенсон (Steve Henson). Поэтому в таких условиях легко недоглядеть и «пропустить» баг.

Так, пять лет назад в OpenSSL обнаружили одну из самых крупных уязвимостей в софте — Heartbleed. Она позволяет несанкционированно читать память на сервере или клиенте. Тогда число уязвимых веб-сайтов оценили в полмиллиона. Патч выпустили сразу, но еще в 2017 году работали 200 тыс. сайтов, подверженных Heartbleed.


Фото — James Sutton — Unsplash

Многие open source проекты испытывают проблемы с финансированием. Тот же OpenSSL существует за счет пожертвований комьюнити и доходов с корпоративных контрактов — сумма не превышает миллиона долларов в год. Бывший CEO проекта говорит, что одной из причин появления Heartbleed стал именно недостаток финансирования. Инженерам бывает сложно привлечь средства даже за консультации. По словам Даниэля Стенберга, к нему часто обращаются международные компании с просьбами помочь решить проблему в cURL. Но каждый раз, когда он просит оплатить его работу, беседа почему-то прекращается.

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

Автор проекта, Доминик Тарр (Dominic Tarr), переключился на другие задачи и оставил свое детище без внимания. Некий пользователь предложил взять поддержку модуля на себя.

Тарр согласился и предоставил ему доступ к репозиторию на GitHub и npm. Со временем новый мейнтейнер внедрил в утилиту скрипт, который воровал данные биткоин-кошельков и загружал их на его сервер. Уязвимость затронула большое число пользователей, учитывая, что у event-stream 1,9 млн скачиваний в неделю.

Как исправляют ситуацию


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

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

Есть мнение, что подход противоречит концепции открытого ПО. При этом он годится не для всех. В 2017 году HTTP/2 веб-сервер Caddy анонсировал коммерческую лицензию. Но по каким-то причинам месяц назад проект вновь вернули в open source.


Фото — Artem Beliaikin — Unsplash

Мировая интернет-инфраструктура зависит от открытых проектов. Поэтому важно уделять внимание их поддержке. И работа в этом направлении ведется. В Linux Foundation регулярно появляются новые резиденты. Все больше в open source инвестируют крупные компании. Возможно, такие инициативы помогут избежать повторения истории, аналогичной Heartbleed.

Дополнительное чтение в блоге 1cloud.ru:

Спасет ли облако ультра-бюджетные смартфоны
Почему Apple изменила требования к разработчикам приложений
Эволюция архитектуры облака 1cloud

Что нового в Linux kernel 5.3 — графические драйверы, виртуализация и другие обновления
Почему разработчики мейнстрим-браузера снова отказались от отображения поддомена
Почему Apple изменила требования к разработчикам приложений

Let's block ads! (Why?)

Низкорисковые биржевые инвестиции: как использовать счета ИИС и облигации как альтернативу банковским вкладам

Изображение: Unsplash

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

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

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

Что такое ИИС


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

По своей сути ИИС похож на сберегательный счет: на него нужно класть деньги, которые должны пролежать на счете не менее трех лет. В таком случае инвестор получает право на налоговые льготы разных типов. Изначально размещать на счете можно было до 400 000 руб., позже сумму увеличили до 1 млн руб.

Что это дает: льготы инвестиционного счета


Владельцы счетов ИИС могут рассчитывать на получение льгот двух типов. Первая из них – налоговый вычет (13%). Чтобы его получить, нужно внести на инвестиционный счет деньги, а также иметь официальный, облагаемый налогом (НДФЛ) доход. Например, если внести на ИИС 400 тысяч рублей, то максимально в виде вычета можно будет вернуть 52 тысячи – для этого понадобится зарплата от 33 тысяч в месяц. По правилам деньги должны лежать на счете ИИС три года, вывести их раньше можно, но тогда вычеты придется вернуть.

То есть эта льгота подходит пассивным инвесторам, которые не планируют совершать транзакции.

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

Низкорисковые инвестиции и ИИС


Добиться минимальных рисков и дохода, которые превышает проценты по банковским депозитам, можно с помощью облигаций. Здесь есть разные варианты. Один из них – облигации федерального займа России.

ОФЗ – это ценные бумаги, которые выпускает государство, чтобы привлечь дополнительные деньги в бюджет – и платит за это процент. Доходность ОФЗ в ноябре 2019 года составляет от 6-6,6%, то есть она примерно равна ставкам по большинству депозитов (хотя есть и депозиты с более высокими ставками).

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

Облигации выпускают и крупные российские компании – доходность по ним выше, чем в случае ОФЗ. В итоге, используя модельный портфель, где объединены облигации разных компаний, в том числе и с государственным участием, можно заработать до 12,9% годовых.

Вот пример описания такого портфеля, созданного аналитиками ITI Capital:

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

Полезные ссылки по теме инвестиций и биржевой торговли:


Читайте обзоры, аналитику рынков и инвестидеи в Telegram-канале ITI Capital

Let's block ads! (Why?)

Сон, релаксация и музыка: как профессиональные атлеты преодолевают усталость, и что нам с этого

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

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


Фото JC Gellidon / Unsplash

«Маленькая тайна, о которой знают все»


Хроническую усталость, связанную с тем, что игроки NBA попросту не высыпаются, называют «маленькой тайной, о которой знают все». «Через пару лет, — считает форвард команды «Филадельфия Севенти Сиксерс» Тобиас Харрис, — эту тему будут обсуждать, как и сотрясения мозга у хоккеистов». В сезон, который в среднем длится шесть месяцев, команда проводит игры каждые два-три дня, а почти всё оставшееся время, за исключением тренировок, занимают авиаперелеты.
В среднем путешествовать приходится по 400 километров в сутки. Постоянная смена часовых поясов и напряженный график приводят к тому, что на сон у спортсменов остается не больше пяти-шести часов в день. Многие говорят, что спят еще меньше: всего три-четыре часа.

Правда, не все. У 42-летнего Винсента Картера, одного из наиболее возрастных игроков ассоциации, таких сложностей нет, и это он считает ключевым фактором своего спортивного долголетия. Но Картер — исключение из правил. Дефицит сна приводит к снижению скорости реакции и общему упадку сил. Уже через два месяца работы в таком графике у игроков наблюдается снижение числа результативных бросков и увеличивается количество травм (в 1,7 раза). Вдобавок, гормон, который регулирует рост и размножение клеток, вырабатывается гипофизом только во сне, поэтому его дефицит приводит к снижению скорости восстановления после нагрузок и преждевременному старению.

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

Чуть подробнее про сон


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

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


Фото Annie Theby / Unsplash

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

Побольше отдыхайте и медитируйте


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

Некоторые предпочитают медитативные практики. По мнению Джорджа Мамфорда, который учил медитации Коби Брайанта, Майкла Джордана, Шакилла О’Нила и Криса Пола (занимается не меньше 15-20 минут в день), она доказала свою пользу для спортсменов. Во-первых, медитация снижает уровень кортизола, который выделяется при стрессе и повышает давление. Во-вторых, перед сном она успокаивает. В-третьих, улучшает выносливость и, наконец, позволяет тренировать внимание и концентрацию. «Медитация — это не попытка отстраниться и уйти, а наоборот, — способ быть на поле и видеть его», — считает Джордж Мамфорд, который работает с игроками NBA еще с 90-х годов.


Фото Charles / Unsplash

Леброн Джеймс включает авиарежим в своем смартфоне за 45 минут до сна и медитирует под звуки дождя. «Сначала я чувствовал себя немного странно, — объясняет он. — Но теперь я знаю, что это помогает». Клей Томпсон из «Голден Стэйт Уорриорз» старается медитировать под Моцарта и Бетховена час в день. Это — сложнее, чем тренироваться, считает он.

Что нам с этого


Среднестатистический IT-специалист в России, конечно, не работает в таком напряженном графике, как баскетболисты NBA или его коллеги в странах Азии, где трудятся по десять часов в сутки шесть дней в неделю. Но нагрузки в любом случае приличные, поэтому самое время пробовать самые различные методики восстановления: изучить, как звуки влияют на наш сон и продуктивность, побороть шум с помощью музыки, начать медитировать или погрузиться в мир ASMR.

Что почитать в нашем «Мире Hi-Fi»:

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

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

Наушники на работе: что говорят исследования
Музыка для эффективной работы: что нужно знать
Экосистема звука: что это такое и как с ней работать


Let's block ads! (Why?)

Лицемерие google. PageSpeed Insights

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

Есть такая рекомендация:

Используйте современные форматы изображений:
Форматы JPEG 2000, JPEG XR и WebP обеспечивают более эффективное сжатие по сравнению с PNG или JPEG, поэтому такие изображения загружаются быстрее и потребляют меньше трафика
С этим не поспоришь, а WebP, когда я его первый раз увидел, я был потрясен. Отличное сжатие без явной потери качества. Но там же сразу можно перейти по ссылке и увидеть, какова же поддержка браузерами данного формата?

image
На момент написания данной статьи, это всего 80%. Вполне не плохо, но еще слишком мало чтобы использовать повсеместно. И как вы думаете что делает с этой информацией сам PageSpeed Insights? Правильно, он использует PNG:

image

Ну ладно, не то что сами рекомендуют, но почему бы не SVG? Нужно же подать пример, но зачем? А давайте проверим на оптимизацию сам сайт developers.google.com на котором находится данный сервис:

image

Мобильная версия всего лишь 51, а вы видели эту страницу? Она практически пустая, несколько меню сверху и снизу, пара новостей и поиск:

image

Очевидно, что они клали на эту оптимизацию, ведь оно им не надо. Они даже не попытались показать пример… Хотя можем это и есть пример? Пример того, что не нужно бездумно пытаться реализовать все рекомендации в ущерб функционалу и здравому смыслу?

В общем любая оптимизация полезна, любая рекомендация имеет смысл быть, но давайте без фанатизма. Спасибо за внимание.

Let's block ads! (Why?)

Хождение по мукам или долгая история одной попытки восстановления данных

На дворе стоял 2019 год. В нашу лабораторию поступил не совсем обычный для нашего времени накопитель QUANTUM FIREBALL Plus KA емкостью 9.1Гб. Со слов владельца накопителя отказ случился в далеком 2004 году по вине вышедшего из строя блока питания, который прихватил за собой жесткий диск и другие компоненты ПК. Далее были хождения по различным сервисам с попытками отремонтировать накопитель и восстановить данные, которые не увенчались успехом. Где-то обещали дешево, но так и не решили проблему, где-то слишком дорого и клиент не пожелал восстанавливать данные, но в итоге диск прошел путь через множество сервисных центров. Неоднократно терялся, но благодаря тому, что владелец заблаговременно позаботился о записи информации с различных наклеек на накопителе ему удалось добиться, чтобы именно его жесткий диск был возвращен из некоторых сервисных центров. Хождения не прошли бесследно, на оригинальной плате контроллера остались множественные следы пайки, а также визуально ощущался недостаток SMD элементов (забегая вперед скажу, что это наименьшая из проблем этого накопителя).

Рис. 1 HDD Quantum Fireball Plus KA 9,1Гб
Первым делом пришлось потрудиться с поиском в донорском архиве столь древнего брата-близнеца этого накопителя с исправной платой контроллера. Когда этот квест был пройден, то стало возможным произвести развернутые диагностические мероприятия. Проверив обмотки двигателя на короткое замыкание и убедившись в его отсутствии, устанавливаем плату с накопителя – донора на накопитель – пациент. Подаем питание и слышим нормальный звук раскрутки вала, прохождение калибровочного теста с загрузкой микропрограммы, и через несколько секунд накопитель по регистрам рапортует о готовности реагировать на команды со стороны интерфейса.

Рис. 2 Индикаторы DRD DSC означают готовность к принятию команд.

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


Рис. 3. Таблица зон.

Обращаем внимание на таблицу зонного распределения, замечаем, что количество цилиндров равно 13845.


Рис. 4 P-list (primary list – список дефектов, внесенных в процессе производственного цикла).

Обращаем внимание на слишком малое количество дефектов и их локализацию. Просматриваем модуль лог скрытия заводских дефектов (60h) и обнаруживаем, что он пуст и не содержит ни одной записи. На основании этого можем предполагать, что в каком-то из предыдущих сервисных центров, возможно, проделывались некие манипуляции со служебной зоной накопителя, и случайно или намеренно был записан чужой модуль, либо подчищен список дефектов в оригинальном. Для проверки этого предположения создаем задачу в Data Extractor с включенными опциями «создание посекторной копии» и «создание виртуального транслятора».


Рис. 5 Параметры задачи.

Создав задачу, просматриваем записи в таблице разделов в нулевом секторе (LBA 0)


Рис. 6 Главная загрузочная запись и таблица разделов.

По смещению 0x1BE находится единственная запись (16 байт). Тип файловой системы на разделе — NTFS, смещение до начала 0x3F (63) сектора, размер раздела 0x011309A3 (18 024 867) секторов.
В редакторе сектора открываем LBA 63.


Рис. 7 Загрузочный сектор NTFS

По информации в загрузочном секторе NTFS раздела можно сказать следующее: размер сектора, принятый в томе, 512 байт (по смещению 0x0B записано слово 0x0200 (512)), количество секторов в кластере равно 8 (по смещению 0x0D записан байт 0x08), размер кластера равен 512х8=4096 байт, первая запись MFT расположена по смещению 6 291 519 секторов от начала диска (по смещению 0x30 учетверенное слово 0x00 00 00 00 00 0C 00 00 (786 432) номер первого кластера MFT. Номер сектора вычисляется по формуле: Номер кластера * количество секторов в кластере + смещение до начала раздела 786 432* 8+63= 6 291 519).
Переходим к сектору 6 291 519.


Рис. 8

Но данные содержащиеся в этом секторе совершенно не похожи на запись MFT. Это хоть и указывает на возможную неправильную трансляцию из-за некорректного дефект-листа, но не доказывает этот факт. Для дальнейшей проверки проведем чтение диска по 10 000 секторов в обоих направлениях относительно 6 291 519 сектора. И после проведем поиск регулярных выражений в прочитанном.


Рис. 9 Первая запись MFT

В секторе 6 291 551 обнаруживаем первую запись MFT. Ее положение от расчетного отличается на 32 сектора, и далее непрерывно следует группа из 16 записей (от 0 до 15). Впишем в таблицу сдвигов положение сектора 6 291 519 сдвинуть вперед на 32 сектора.


Рис. 10

Положение записи №16 должно быть по смещению 12 551 431, но там обнаруживаем нули, вместо записи MFT. Проведем аналогичный поиск в окрестностях.


Рис. 11 Запись MFT 0x00000011 (17)

Обнаруживается крупный фрагмент MFT, начинающийся с записи под номером 17 протяженностью 53 646 записей) со сдвигом в 17 секторов. Для позиции 12 155 431 поставим сдвиг +17 секторов в таблицу сдвигов.
Определив положение фрагментов MFT в пространстве можем сделать вывод, что это не похоже на случайный сбой и запись фрагментов MFT по некорректным смещениям. Версию с некорректным транслятором можно считать подтвержденной.
Для дальнейшей локализации точек сдвигов установим максимально возможное смещение. Для этого определим, насколько сдвинут конечный маркер NTFS раздела (копия загрузочного сектора). На рисунке 7 по смещению 0x28 четверное слово — это значение размера раздела 0x00 00 00 00 01 13 09 A2 (18 024 866) секторов. Прибавим смещение самого раздела от начала диска к его длине получим смещение конечного маркера NTFS 18 024 866 + 63= 18 024 929. Ожидаемо там не оказалось нужной копии загрузочного сектора. При поиске в окрестностях он был обнаружен с нарастающим сдвигом в +12 секторов по отношению к последнему фрагменту MFT.


Рис. 12 Копия загрузочного сектора NTFS

Другую копию загрузочного сектора по смещению 18 041 006 игнорируем, так как она не имеет отношения к нашему разделу. На основании предыдущих мероприятий установлено, что внутри раздела имеются вкрапления из «всплывших» в трансляции 61 сектора, которые раздвинули данные.
Выполняем полное чтение накопителя, результатом которого остается 34 непрочитанных сектора. К сожалению, достоверно гарантировать, что все они являются дефектами, удаленными из P-list невозможно, но при дальнейшем анализе желательно учитывать их положение, так как в некоторых случаях можно будет достоверно определять точки сдвига с точностью до сектора, а не до файла.


Рис. 13 Статистика чтения диска.

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


Рис. 14 Цепочки расположения файлов, либо их фрагментов.

Далее, двигаясь от файла к файлу, ищем, с какого момента вместо ожидаемого заголовка файла будут иные данные, а нужный заголовок обнаружится с неким положительным сдвигом. И по мере уточнения точек сдвигов заполняем таблицу. Результатом ее заполнения будет свыше 99% файлов без повреждений.


Рис. 15 Список пользовательских файлов (от клиента получено согласие на публикацию этого скриншота)

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

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

Предыдущая публикация: Экономия на спичках или восстановление данных из скрежещущего HDD Seagate ST3000NC002-1DY166

Let's block ads! (Why?)

[Из песочницы] Делим Laravel на компоненты

Привет, Хабр. Недавно получил в руки интересный проект, который, несмотря на свою простоту требовал не использовать какой-либо фреймворк. О пакетах речи не шло, поэтому было принято решение использовать привычные компоненты Laravel. Если есть необходимость в использовании очередей, Eloquent или контейнера — добро пожаловать под кат.


О простоте деления фреймворка на компоненты

Начиная с Laravel 4, все компоненты — отдельные пакеты, которые в теории можно заставить работать в любом PHP проекте. Однако, некоторые компоненты требуют дополнительной настройки, которая в основном фреймворке спрятана под капот.


Компоненты


Контейнер

Из всех рассматриваемых компонентов illuminate/container — самый простой в установке и использовании.

<?php

use Illuminate\Container\Container;

$container = Container::getInstance();

return $container->make(Foo::class);

Впрочем, вызов статичного метода на классе Container при каждом использовании контейнера — не самая лучшая идея. В фреймворке доступен хелпер app(), который вернёт нам инстанс глобального контейнера, однако, нам такой нужно создать вручную.

Создадим файл helpers.php для таких хелперов, и добавим его в автозагрузку.

"autoload": {
    "files": [
        "src/helpers.php"
    ],
    "psr-4": {
        "YourApp\\": "src/"
    }
}

Добавляем хелпер app() в файл.

<?php

use Illuminate\Container\Container;

if (! function_exists('app')) {
    /**
     * Get the available container instance.
     *
     * @param  string|null  $abstract
     * @param  array  $parameters
     * @return mixed|\Illuminate\Container\Container
     */
    function app($abstract = null, array $parameters = [])
    {
        if (is_null($abstract)) {
            return Container::getInstance();
        }
        return Container::getInstance()->make($abstract, $parameters);
    }
}

Готово. Можно пробовать использовать хелпер.

<?php

return app(Foo::class);

Query Builder и Eloquent

Для использования компонента illuminate/database мы будем использовать Capsule\Manager — класс, предназначенный для работы с построителем запросов вне Laravel.

Начнём с настройки подключения к БД. Желательно эту настройку проводить на этапе запуска вашего приложения, сразу после подключения autoload.php.

<?php

require_once __DIR__.'/../vendor/autoload.php';

use Illuminate\Database\Capsule\Manager;

$manager = new Manager;

$manager->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'database',
    'username'  => 'root',
    'password'  => 'password',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
]);

// Позволяет использовать статичные вызовы при работе с Capsule.
$manager->setAsGlobal();

Можно начинать работу с построителем запросов.

<?php

use Illuminate\Database\Capsule\Manager;

return Manager::table('orders')->get();

Что насчёт Eloquent?

<?php

namespace YourApp\Models;

use Illuminate\Database\Eloquent\Model;

class Order extends Model {

    protected $fillable = [
        'name',
    ];
}
<?php

use YourApp\Models\Order;

Order::first();

С миграциями ситуация сложнее — с одной стороны, в комплекте есть Schema Builder. С другой — автоматический механизм запуска миграций отсутствует.

<?php

use Illuminate\Database\Capsule\Manager;

Manager::schema()->create('orders', function ($table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->timestamps();
});

Для запуска достаточно выполнить файл с миграцией: php migration.php


Очереди

У очередей тоже есть свой Capsule, однако аналоги queue:work и queue:listen необходимо писать вручную.

Начнём с класса Application. В Laravel этот класс используется как глобальный инстанс контейнера, который помимо методов Illuminate\Container\Container содержит вспомогательные методы для работы с фреймворком (текущая версия, пути к хранилищу, ресурсам). Однако, наш класс будет содержать лишь один метод — isDownForMaintenance. Он необходим для работы воркера, так как определяет состояние работы приложения в данный момент.

<?php

namespace YourApp;

use Illuminate\Container\Container;

class Application extends Container {
    public function isDownForMaintenance()
    {
        return false;
    }
}

Далее, нам необходимо зарегистрировать провайдер illuminate/events, и забиндить контракт Illuminate\Contracts\Events\Dispatcher к алиасу events.

<?php

use YourApp\Application;
use Illuminate\Contracts\Events\Dispatcher;

$application = Application::getInstance();
$application->bind(Dispatcher::class, 'events');

Теперь необходимо создать инстанс Capsule, и добавить туда конфигурации соединений.

Пример конфигурации для БД

<?php

use YourApp\Application;
use Illuminate\Queue\Capsule\Manager;
use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\ConnectionResolver;

$container = Application::getInstance();
$queue = new Manager($container);

$queue->addConnection([
    'driver' => 'database',
    'table' => 'jobs',
    'connection' => 'default',
    'queue' => 'default',
], 'default');

// Также, как и с Illuminate\Database\Capsule\Manager позволяет использовать статичные вызовы для очередей
$queue->setAsGlobal();

$connection = Capsule::schema()->getConnection();
$resolver = new ConnectionResolver(['default' => $connection]);

$queue->getQueueManager()->addConnector('database', function () use ($resolver) {
    return new DatabaseConnector($resolver);
});

Пример конфигурации для Redis (требует установленного illuminate/redis)

<?php

use Illuminate\Redis\RedisManager;
use Illuminate\Queue\Capsule\Manager;

$container->bind('redis', function () use ($container) {
    return new RedisManager($container, 'predis', [
        'default' => [
            'host' => '127.0.0.1',
            'password' => null,
            'port' => 6379,
            'database' => 0,
        ]
    ]);
});

$queue = new Manager($container);
$queue->addConnection([
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => 'default',
], 'default');

$queue->setAsGlobal();

Последний этап в конфигурации — добавление аналога Exception Handler.

<?php

use Illuminate\Contracts\Debug\ExceptionHandler;

class Handler implements ExceptionHandler {

    public function report(Exception $e)
    {
        // Действие, если Exception подпадает под критерии об уведомлении.
        // Пример: отправка в Sentry, отправка сообщения на почту.
    }

    public function render($request, Exception $e)
    {
        // Отображение ошибки
    }

    public function renderForConsole($output, Exception $e)
    {
        // Отображение ошибки, если среда запуска приложения - терминал
    }

    public function shouldReport(\Exception $e)
    {
        // Необходимо ли уведомлять об ошибке
    }
}

app()->bind('exception.handler', function () {
    return new Handler;
});

Конфигурация очередей завершена. Теперь можно приступать к написанию queue:work.

<?php

require_once __DIR__.'/bootstrap/bootstrap.php';

use Illuminate\Queue\Worker;
use Illuminate\Queue\Capsule\Manager;
use Illuminate\Queue\WorkerOptions;

$queueManager = Manager::getQueueManager();

$worker = new Worker($queueManager, app('events'), app('exception.handler'));

$worker->daemon('default', 'default', new WorkerOptions);

Теперь для запуска воркера очередей достаточно написать php worker.php.

Если же есть необходимость в использовании queue:listen, то нужно создавать два отдельных файла. Один — демон, который слушает очередь, и запускает второй файл на каждый новый job. Второй файл, в свою очередь, выступает в роли исполнителя задачи.

worker.php

<?php

require_once __DIR__.'/bootstrap/bootstrap.php';

use Illuminate\Queue\Listener;
use Illuminate\Queue\ListenerOptions;

// По умолчанию, в качестве "второго файла", в Laravel выступает artisan, однако в нашем случае это будет файл work.php.
// https://github.com/laravel/framework/blob/6.x/src/Illuminate/Queue/Listener.php#L72
define('ARTISAN_BINARY', 'work.php');

$worker = app(Listener::class, ['commandPath' => __DIR__]);

$worker->setOutputHandler(function ($type, $line) {
    echo $line;
});

$worker->listen('default', 'default', new ListenerOptions);

work.php

<?php

require_once 'bootstrap/bootstrap.php';

use Illuminate\Queue\Worker;
use Illuminate\Queue\WorkerOptions;
use Illuminate\Queue\Capsule\Manager;

$queueManager = Manager::getQueueManager();

$worker = new Worker($queueManager, app('events'), app('exception.handler'));

$worker->runNextJob('default', 'default', new WorkerOptions);

Переходим к использованию. Все методы можно просмотреть в API

<?php
use Illuminate\Queue\Capsule\Manager;

Manager::push(SomeJob::class);

Let's block ads! (Why?)