Executando verificação de segurança...
5

RFC: Script que irá preencher histórico de TabCoins

Turma, esta publicação é importante então peço que leiam com carinho.


Nem dá para acreditar, mas faz apenas 45 dias que publicamos a feature de TabCoins aqui no TabNews... parece que sempre existiu a qualificação dos conteúdos e o algoritmo na Home, mas tudo isso é super recente.

Bom, o deploy e o anúncio desta feature aconteceu exatamente no mesmo momento e isso foi feito no dia 3 de Julho, onde na publicação de comemoração, escrevi o seguinte no Tópico bônus:

E todos os conteúdos que já foram publicados no passado, ninguém vai ganhar TabCoins por isso? Vão ganhar sim, e em dobro. Iremos fazer um script para historicamente creditar os autores de todas as publicações que foram feitas até hoje, onde ao invés de creditar 5 TabCoins, serão creditadas 10 TabCoins como agradecimento por terem participado do projeto num estágio tão inicial.

Então estamos aqui para preencher esse histórico 🤝 E apesar de que isto é um feito técnico e poderia ser discutido dentro de uma issue no GitHub, eu preferi trazer isso para cá, pois é aqui no TabNews que as pessoas criam os conteúdos... então nada mais justo do que deixar transpartente aqui o que será feito para todo mundo conseguir acompanhar, opinar ou descobrir alguma falha no script que iremos rodar.

O que foi feito

Então nesses últimos dias eu fiz um script que, em resumo, pega todos os conteúdos que foram criados antes do deploy da feature de TabCoins e que não creditaram TabCoins nem para o Conteúdo, nem para o Usuário, e cria esses créditos.

O script é simples, mas como não fiz nenhuma modelagem nem nada, inseri um monte de comentários para guiar qualquer pessoa que estiver lendo. Um cuidado extra é que comentários podem ser mentirosos e a única coisa real é o código, então não deixe de ler ele com total atenção.

O script está logo abaixo, mas gostaria de começar pelo resultado que ele gera.

Resultado

Basicamente o bloco logo abaixo é o log que o script gera utilizando os dados de Produção. O script rodou no modo Dry Run, ou seja, ele executa de verdade, mas não registra nada de verdade no sistema. Assim podemos rodar eles várias vezes para ir lapidando o que deve ser feito.

E nesse resultado começo informando a quantidade total de Conteúdos (independente se é root ou child) e lista também a quantidade desses Conteúdos que foram criados antes da existência da feature de TabCoins.

Depois somo a quantidade de TabCoins no sistema inteiro para os Conteúdos (antes e depois de rodar o script), e a mesma coisa para a quantidade de TabCoins dos Usuários.

Contents: 4435
Contents before TabCoins existence: 2816
---
Content TabCoins (before): 4886
Content TabCoins (after): 7702 (+2816)
---
User TabCoins (before): 11492
User TabCoins (after): 39652 (+28160)

Script

Este foi o script que rodei e qualquer furo ou dúvida não hesite em deixar um comentário:

async function runTabCoinsScript(request, response) {

  // Para quem não conhece, o conceito de "dry run" é tentar rodar um código,
  // mas que ao final não faz nada de verdade. No caso desse script, é
  // iniciada uma transação no banco de dados, que de fato executa tudo,
  // mas ao final é feito o rollback, o que cancela tudo que foi feito.
  const DRY_RUN = true;
  const transaction = await database.transaction();

  try {
    await transaction.query('BEGIN');

    // 0) CRIAR EVENTO
    //    Nós primeiro criamos um evento, pois este evento será usado
    //    como lastro em todas as entradas de balanço que iremos criar
    //    mais para frente. A única coisa chata de "marretar" um evento
    //    assim fora do fluxo normal é que eu precisei hardcode o meu
    //    usuário e ip (localhost).
    const currentEvent = await event.create(
      {
        type: 'system:update:tabcoins',
        originatorUserId: '07ea33ea-78bd-4578-bad2-1cf5323cab07', // filipedeschamps
        originatorIp: '127.0.0.1',
        metadata: {
          description: `Backfill all TabCoins from contents created before '2022-07-03 16:48:12.457+00'.`,
        },
      },
      {
        transaction: transaction,
      }
    );

    // 1) PEGAR NÚMERO TOTAL DE CONTEÚDOS PUBLICADOS
    //    Para referência, eu pego o número total de Conteúdos publicados
    //    e isso vai dar um comparativo bom com a próxima query.
    const totalContents = await transaction.query(`
      SELECT
        count(*)
      FROM
        contents
      WHERE
        status = 'published'
    ;`);

    console.log(`Contents: ${totalContents.rows[0].count}`);

    // 2) PEGAR CONTEÚDOS CRIADOS ANTES DO DEPLOY SOBRE AS TABCOINS
    //    Estes Conteúdos hoje podem ter saldo de TabCoins, mas não
    //    importa, pois precisamos mais para frente creditar as TabCoins
    //    originais do Conteúdo e do Usuário criador do Conteúdo. Então
    //    dessa query, a gente extrai o ID do Conteúdo e o ID do Autor
    //    e não importa se o Conteúdo é "root" ou "child", precisa apenas
    //    estar com status "published" e ter sido criado antes do deploy.
    const totalContentsBeforeTabcoins = await transaction.query(`
      SELECT
       id as content_id,
       owner_id as user_id
      FROM
        contents
      WHERE
        contents.status = 'published'
        AND contents.created_at < '2022-07-03 16:48:12+00'
    ;`);

    console.log(`Contents before TabCoins existence: ${totalContentsBeforeTabcoins.rows.length}`);
    console.log(`---`);

    // 3) PEGAR TOTAL DE TABCOINS POSITIVAS DOS CONTEÚDOS (ANTES)
    //    Apenas como referência, eu somo o total de TabCoins positivas
    //    de todos os Conteúdos do sistema só para entender o que vai
    //    acontecer com esse valor depois de rodarmos o script.
    const totalContentsTabcoinsBefore = await transaction.query(`
      SELECT
        sum(amount)
      FROM
        balance_operations
      WHERE
        balance_type = 'content:tabcoin'
        and amount > 0
    ;`);

    console.log(`Content TabCoins (before): ${totalContentsTabcoinsBefore.rows[0].sum}`);

    // 4) CRIAR TABCOINS PARA OS CONTEÚDOS
    //    Basicamente é criado uma entrada na tabela de balanço para cada
    //    Conteúdo que foi criado antes do deploy e está na variável "totalContentsBeforeTabcoins"
    //    acima. Fora isso, usamos o evento também criado acima para registrar o lastro
    //    dessa movimentação.
    for await (const contentWithoutTabcoins of totalContentsBeforeTabcoins.rows) {
      const contentId = contentWithoutTabcoins.content_id;

      await balance.create(
        {
          balanceType: 'content:tabcoin',
          recipientId: contentId,
          amount: 1, // Valor padrão que o Conteúdo ganha ao ser criado
          originatorType: 'event',
          originatorId: currentEvent.id,
        },
        {
          transaction: transaction,
        }
      );
    }

    // 4) PEGAR TOTAL DE TABCOINS POSITIVAS DOS CONTEÚDOS (DEPOIS)
    //    Então agora nós conseguiremos comparar o que aconteceu com
    //    o número total de TabCoins no sistema, que deveria ter a mais
    //    o mesmo número de Conteúdo sem TabCoins, dado que é creditado
    //    1 TabCoin por criação de conteúdo.
    const totalContentsTabcoinsAfter = await transaction.query(`
      SELECT
        sum(amount)
      FROM
        balance_operations
      WHERE
        balance_type = 'content:tabcoin'
        and amount > 0
    ;`);

    console.log(
      `Content TabCoins (after): ${totalContentsTabcoinsAfter.rows[0].sum} (+${
        totalContentsTabcoinsAfter.rows[0].sum - totalContentsTabcoinsBefore.rows[0].sum
      })`
    );
    console.log(`---`);

    // 5) PEGAR TOTAL DE TABCOINS POSITIVAS DOS USUÁRIOS (ANTES)
    //    Mesma lógica usada no caso das TabCoins dos Conteúdos, mas agora
    //    para os Usuários.
    const totalUsersTabcoinsBefore = await transaction.query(`
      SELECT
        sum(amount)
      FROM
        balance_operations
      WHERE
        balance_type = 'user:tabcoin'
        and amount > 0
    ;`);

    console.log(`User TabCoins (before): ${totalUsersTabcoinsBefore.rows[0].sum}`);

    // 6) CRIAR TABCOINS PARA OS USUÁRIOS
    //    Praticamente a mesma lógica que antes, mas agora bonificando os Usuários.
    for await (const contentWithoutTabcoins of totalContentsBeforeTabcoins.rows) {
      const userId = contentWithoutTabcoins.user_id;

      await balance.create(
        {
          balanceType: 'user:tabcoin',
          recipientId: userId,
          amount: 10, // O valor bônus para quem se arriscou a criar um Conteúdo no passado
          originatorType: 'event',
          originatorId: currentEvent.id,
        },
        {
          transaction: transaction,
        }
      );
    }

    // 7) PEGAR TOTAL DE TABCOINS POSITIVAS DOS USUÁRIOS (DEPOIS)
    //    Isso deveria gerar para os usuários 10x mais TabCoins que foram
    //    geradas para os Conteúdos.
    const totalUsersTabcoinsAfter = await transaction.query(`
      SELECT
        sum(amount)
      FROM
        balance_operations
      WHERE
        balance_type = 'user:tabcoin'
        and amount > 0
    ;`);

    console.log(
      `User TabCoins (after): ${totalUsersTabcoinsAfter.rows[0].sum} (+${
        totalUsersTabcoinsAfter.rows[0].sum - totalUsersTabcoinsBefore.rows[0].sum
      })`
    );

    if (DRY_RUN) {
      throw new UnprocessableEntityError({
        message: 'Dry run',
      });
    } else {
      await transaction.query('COMMIT');
      await transaction.release();
      return response.status(200).json({});
    }
  } catch (error) {
    await transaction.query('ROLLBACK');
    await transaction.release();
    throw error;
  }
}

Conclusão

Caso algo não ficou claro ou você ficou com alguma dúvida, por favor pergunte. Caso você tenha alguma sugestão, não pense duas vezes em sugerir. Vamos conversando para que a reconstrução desse histórico de TabCoins seja feita da melhor forma 🤝

Carregando publicação patrocinada...
6

E com isso o Guga e o Filipe já podem se aposentar... hahaha

Dá pra acrescentar essa checagem antes do COMMIT.

if (totalContentsBeforeTabcoins.rows.length !==
    (totalContentsTabcoinsAfter.rows[0].sum - totalContentsTabcoinsBefore.rows[0].sum)) {
    throw new error...

} else if (totalContentsBeforeTabcoins.rows.length !==
    ((totalUsersTabcoinsAfter.rows[0].sum - totalUsersTabcoinsBefore.rows[0].sum) / 10)) {
    throw new error...

} else if (DRY_RUN) {
    throw new error...

} else {
    await transaction.query('COMMIT');
      ...
}

Ela pode acusar um falso positivo se alguém fizer alguma transação no momento do script, mas pode ser uma garantia de que tudo ocorreu como deveria.

4

Show!!! To adicionando aqui e o script caiu no primeiro if. Acho que para esse primero check deveria ser:

    if (
      totalContentsBeforeTabcoins.rows.length !==
      totalContentsTabcoinsAfter.rows[0].sum - totalContentsTabcoinsBefore.rows[0].sum
    ) {
      throw new UnprocessableEntityError({
        message: 'Number of added Content TabCoins does not match',
        context: {
          totalContentsBeforeTabcoins: totalContentsBeforeTabcoins.rows.length,
          totalContentsTabcoinsBefore: totalContentsTabcoinsBefore.rows[0].sum,
          totalContentsTabcoinsAfter: totalContentsTabcoinsAfter.rows[0].sum,
          addedTabCoins: totalContentsTabcoinsAfter.rows[0].sum - totalContentsTabcoinsBefore.rows[0].sum,
        },
      });

Porque no final das contas, o totalContentsBeforeTabcoins (conteúdos criados antes das TabCoins) deveria ser o mesmo número das TabCoins adicionadas, que é a subtração de totalContentsTabcoinsAfter com totalContentsTabcoinsBefore.

O que acha?

3
4

No segundo if, essa substração:

totalContents.rows[0].count - totalContentsBeforeTabcoins.rows.length

Acaba retornando a quantidade de conteúdos que foram criados depois da feature de TabCoins. Então talvez precise ficar assim:

    } else if (
      totalContentsBeforeTabcoins.rows.length !==
      (totalUsersTabcoinsAfter.rows[0].sum - totalUsersTabcoinsBefore.rows[0].sum) / 10
    ) {
      throw new UnprocessableEntityError({
        message: 'Number of added User TabCoins does not match',
        context: {
          totalContentsBeforeTabcoins: totalContentsBeforeTabcoins.rows.length,
          totalUsersTabcoinsBefore: totalUsersTabcoinsBefore.rows[0].sum,
          totalUsersTabcoinsAfter: totalUsersTabcoinsAfter.rows[0].sum,
        },

O que acha?

3