Nós da empresa sempre nos esforçamos para aumentar a capacidade de manutenção de nosso código, usando práticas geralmente aceitas, inclusive em questões de multithreading. Isso não resolve todas as dificuldades que uma carga cada vez maior traz, mas simplifica o suporte - também ganha a legibilidade do código e a velocidade do desenvolvimento de novos recursos.
Agora, temos 47.000 usuários diariamente, cerca de 30 servidores em produção, 2.000 solicitações de API por segundo e lançamentos diários. O serviço Miro está em desenvolvimento desde 2011 e, na implementação atual, as solicitações dos usuários são processadas em paralelo por um cluster de servidores heterogêneos.

Subsistema de controle de acesso competitivo
O principal valor de nosso produto são as placas de usuário colaborativas, portanto, a principal carga recai sobre elas. O principal subsistema que controla a maior parte do acesso competitivo é o sistema estável de sessões do usuário no quadro.
Para cada placa que pode ser aberta em um dos servidores, o estado aumenta. Ele armazena os dados de tempo de execução do aplicativo necessários para garantir a colaboração e a exibição do conteúdo, bem como os dados do sistema, como a ligação aos threads de processamento. As informações sobre o servidor em que o estado está armazenado são gravadas em uma estrutura distribuída e ficam disponíveis para o cluster enquanto o servidor estiver em execução e pelo menos um usuário estiver no quadro. Usamos o Hazelcast para fornecer essa parte do subsistema. Todas as novas conexões com a placa são enviadas ao servidor com esse estado.
Ao conectar-se ao servidor, o usuário entra no fluxo receptor, cuja única tarefa é vincular a conexão ao estado da placa correspondente, nos fluxos nos quais todo o trabalho adicional ocorrerá.
Dois fluxos estão associados à placa: rede, conexões de processamento e "comercial", responsável pela lógica comercial. Isso permite transformar a execução de tarefas heterogêneas de processamento de pacotes de rede e execução de comandos de negócios de serial em paralelo. Os comandos de rede processados dos usuários formam tarefas de negócios aplicadas e os direcionam para o fluxo de negócios, onde são processados sequencialmente. Isso evita a sincronização desnecessária ao desenvolver o código do aplicativo.
A divisão do código em negócios / aplicativo e sistema é nossa convenção interna. Permite distinguir entre o código responsável pelos recursos e capacidades dos usuários, e os detalhes de baixo nível de comunicação, agendamento e armazenamento, que são a ferramenta de serviço.
Se o fluxo receptor detectar que não há estado para a placa, a tarefa de inicialização correspondente será definida. A inicialização do estado é tratada por um tipo separado de encadeamento.
Os tipos de tarefas e sua direção podem ser representados da seguinte maneira:

Essa implementação nos permite resolver os seguintes problemas:
- Não há lógica comercial no fluxo de recebimento que possa retardar a nova conexão. Esse tipo de fluxo no servidor existe em uma única cópia; portanto, os atrasos afetarão imediatamente o tempo de abertura das placas e, se houver um erro no código comercial, ele poderá ser facilmente interrompido.
- A inicialização do estado não é executada no fluxo de negócios das placas e não afeta o tempo de processamento dos comandos de negócios dos usuários. Pode levar algum tempo, e os fluxos de negócios processam várias placas ao mesmo tempo, para que a abertura de novas placas não afete diretamente as existentes.
- A análise de comandos de rede geralmente é mais rápida do que executá-los diretamente, portanto, a configuração do conjunto de encadeamentos de rede pode ser diferente da configuração do conjunto de encadeamentos de negócios para usar com eficiência os recursos do sistema.
Coloração de fluxo
O subsistema descrito acima na implementação não é trivial. O desenvolvedor deve ter em mente o esquema do sistema e levar em conta o processo inverso de painéis de fechamento. Ao fechar, é necessário remover todas as assinaturas, excluir entradas dos registros e fazer isso nos mesmos fluxos em que foram inicializadas.
Percebemos que os erros e dificuldades de modificar o código que surgiam nesse subsistema estavam frequentemente associados à falta de entendimento do contexto de execução. A manipulação de threads e tarefas dificultou a resposta à pergunta em qual thread específico um determinado código está executando.
Para resolver esse problema, usamos o método de colorir linhas - esta é uma política que visa regular o uso de linhas no sistema. As cores são atribuídas aos threads e os métodos definem o escopo para execução nos threads. A cor aqui é uma abstração, pode ser qualquer entidade, por exemplo, uma enumeração. Em Java, as anotações podem servir como a linguagem de marcação de cores:
@Color @IncompatibleColors @AnyColor @Grant @Revoke
As anotações são adicionadas ao método, usando-as, você pode definir a validade do método. Por exemplo, se a anotação de um método permitir amarelo e vermelho, o primeiro encadeamento poderá chamar o método e, no segundo, essa chamada será incorreta.

Cores inválidas podem ser especificadas:

Você pode adicionar e remover privilégios de encadeamento na dinâmica:

A ausência de anotação ou anotação, como no exemplo abaixo, diz que o método pode ser executado em qualquer encadeamento:

Os desenvolvedores do Android podem estar familiarizados com esta abordagem para anotações MainThread, UiThread, WorkerThread, etc.
A coloração de linhas usa o princípio do código de auto-documentação e o próprio método se presta bem à análise estática. Usando a análise estática, você pode dizer, antes que o código seja executado, que ele foi gravado corretamente ou não. Se excluirmos as anotações Grant e Revoke e presumirmos que o fluxo na inicialização já possui um conjunto imutável de privilégios, será uma análise que não diferencia o fluxo - uma versão simples da análise estática que não leva em consideração a ordem das chamadas.
No momento da implementação do método de coloração de fluxo, não havia soluções prontas para análise estática em nossa infraestrutura de devops; portanto, seguimos o caminho mais simples e barato - apresentamos nossas anotações, associadas exclusivamente a cada tipo de fluxo. Começamos a verificar sua correção com a ajuda de aspectos em tempo de execução.
@Aspect public class ThreadAnnotationAspect { @Pointcut("if()") public static boolean isActive() { …
Para aspectos, usamos a biblioteca aspectj e o plug-in maven, que fornece tecelagem ao compilar o projeto. A tecelagem foi inicialmente configurada para carregar o tempo ao carregar as classes com o ClassLoader. No entanto, fomos confrontados com o fato de que o tecelão às vezes se comportava incorretamente ao carregar a mesma classe competitivamente, como resultado do qual o byte original do código da classe permaneceu inalterado. Como resultado, isso resultou em imprevisíveis e difíceis de reproduzir o comportamento da produção. Talvez nas versões atuais da biblioteca não exista esse problema.
A solução sobre os aspectos encontrou rapidamente a maioria dos problemas no código.
É importante não esquecer de manter sempre as anotações atualizadas: elas podem ser excluídas, adicionar preguiça, os aspectos de tecelagem podem ser totalmente desligados - nesse caso, a coloração perde rapidamente sua relevância e valor.
Guardedby
Uma das variedades de cores é a anotação GuardedBy de java.util.concurrent. Delimita o acesso a campos e métodos, indicando quais bloqueios são necessários para o acesso correto.
public class PrivateLock { private final Object lock = Object(); @GuardedBy (“lock”) Widget widget; void method() { synchronized (lock) {
Os IDEs modernos até apoiam a análise desta anotação. Por exemplo, o IDEA exibe esta mensagem se algo estiver errado com o código:
O método de colorir threads não é novo, mas parece que em linguagens como Java, onde o acesso multiencadeado costuma ser direcionado a objetos mutáveis, seu uso não apenas como parte da documentação, mas também no estágio de compilação, a montagem pode simplificar bastante o desenvolvimento de código multiencadeado.
Ainda usamos a implementação em aspectos. Se você estiver familiarizado com uma solução ou ferramenta de análise mais elegante que permita aumentar a estabilidade dessa abordagem às alterações do sistema, compartilhe-a nos comentários.