Planejando Regras

As regras de planejamento de tarefas podem ser utilizadas para controle quando suas tarefas são executadas em relação a outras tarefas. Em particular, as regras de planejamento permitem que você evite que várias tarefas sejam executadas ao mesmo tempo em situações em que a coincidência pode levar a resultados inconsistentes. Elas também permitem que você garanta o pedido de execução de uma série de tarefas. O poder de planejar regras é melhor ilustrado por um exemplo. Vamos começar com a definição de duas tarefas que são utilizadas para ligar e desligar um interruptor ao mesmo tempo:

      public class LightSwitch {
      private boolean isOn = false;
      public boolean isOn() {
         return isOn;
      }
      public void on() {
         new LightOn().schedule();
      }
      public void off() {
         new LightOff().schedule();
      }
      class LightOn extends Job {
         public LightOn() {
            super("Turning on the light");
         }
         public IStatus run(IProgressMonitor monitor) {
            System.out.println("Turning the light on");
            isOn = true;
            return Status.OK_STATUS;
         }
      }
      class LightOff extends Job {
         public LightOff() {
            super("Turning off the light");
         }
         public IStatus run(IProgressMonitor monitor) {
            System.out.println("Turning the light off");
            isOn = false;
            return Status.OK_STATUS;
         }
      }
   }

Agora, criamos um programa simples que cria um interruptor e o liga e desliga novamente:

      LightSwitch light = new LightSwitch();
   light.on();
   light.off();
   System.out.println("The light is on? " + switch.isOn());

Se executarmos este programinha um número suficiente de vezes, finalmente obteremos a seguinte saída:

      Apagando a luz
   Acendendo a luz
   A luz está acesa? true

Como isso pode acontecer? Pedimos que a luz acendesse e apagasse, então o estado final deveria ser apagada! O problema é que não houve nada que evitasse que a tarefa LightOff fosse executada ao mesmo tempo que a tarefa LightOn. Então, embora a tarefa "ligar" tenha sido planejada primeiro, a execução ao mesmo tempo significa que não há como prever a ordem de execução exata das duas tarefas simultâneas. Se a tarefa LightOff encerrar a execução antes da tarefa LightOn, obteremos este resultado inválido. O que precisamos é uma maneira de evitar que as duas tarefas sejam executadas ao mesmo tempo e é aí que as regras de planejamento entram.

Podemos corrigir este exemplo, criando uma regra de planejamento simples que age como um mutex (também conhecido como semáforo binário):

      class Mutex implements ISchedulingRule {
      public boolean isConflicting(ISchedulingRule rule) {
         return rule == this;
      }
      public boolean contains(ISchedulingRule rule) {
         return rule == this;
      }
   }

Esta regra é incluída nas duas tarefas de interruptor do exemplo anterior:

      public class LightSwitch {
      final MutextRule rule = new MutexRule();
      ...
      class LightOn extends Job {
         public LightOn() {
            super("Turning on the light");
            setRule(rule);
         }
         ...
      }
      class LightOff extends Job {
         public LightOff() {
            super("Turning off the light");
            setRule(rule);
         }
         ...
      }
   }

Agora, quando as duas tarefas de interruptor são planejadas, a infra-estrutura de tarefa chama o método isConflicting para comparar as regras de planejamento das duas tarefas. Ele observará que as duas tarefas têm regras de planejamento em conflito e se certificará de que elas são executadas na ordem correta. Ele também se certificará de que elas nunca são executadas ao mesmo tempo. Se o programa de exemplo for executado um milhão de vezes, você sempre obterá o mesmo resultado:

      Acendendo a luz
   Apagando a luz
   A luz está acesa? false

As regras também podem ser utilizadas independentemente das tarefas como um mecanismo de travamento geral. O seguinte exemplo adquire uma regra em um bloqueio try/finally, evitando que outros encadeamentos e tarefas sejam executados com essa regra na duração entre as chamadas de beginRule e endRule.

      IJobManager manager = Platform.getJobManager();
try {      manager.beginRule(rule, monitor);
      ... do some work ...
  } finally {
      manager.endRule(rule);
   }

É necessário ter muito cuidado ao adquirir e liberar regras de planejamento utilizando um padrão de codificação. Se houver falha ao encerrar uma regra para a qual você chamou beginRule, essa regra terá sido travada para sempre.

Fazendo suas Próprias Regras

Embora a API de tarefa defina o contrato das regras de planejamento, ela não fornece implementações de regra de planejamento. Essencialmente, a infra-estrutura genérica não tem como saber quais conjuntos de tarefas estão ok para serem executadas ao mesmo tempo. Por padrão, as tarefas não têm regras de planejamento e uma tarefa planejada é executada tão rapidamente quando um encadeamento pode ser criado para executá-la.

Quando uma tarefa tem uma regra de planejamento, o método isConflicting é utilizado para determinar se a regra está em conflito com as regras que estão em execução atualmente. Assim, sua implementação de isConflicting pode definir exatamente quando é seguro executar sua tarefa. No exemplo do interruptor, a implementação de isConflicting simplesmente utiliza uma comparação de identidade com a regra fornecida. Se outra tarefa tem a regra idêntica, elas não serão executadas ao mesmo tempo. Ao gravar suas regras de planejamento, certifique-se de ler e seguir o contrato de API para isConflicting cuidadosamente.

Se a sua tarefa tem várias restrições não relacionadas, você pode compor várias regras de planejamento juntas utilizando uma MultiRegra. Por exemplo, se a sua tarefa precisa ligar um interruptor e além disso gravar informações em um soquete de rede, ela pode ter uma regra para o interruptor e uma regra para acesso de gravação ao soquete, combinada em uma única regra utilizando o método do depósito de informações do fornecedor MultiRule.combine.

Hierarquias de Regras

Discutimos o método isConflicting em ISchedulingRule, mas até agora não mencionamos o método contains. Esse método é utilizado para um aplicativo especializado corretamente das regras de planejamento que muitos clientes não exigirão. As regras de planejamento podem ser compostas logicamente nas hierarquias para controlar o acesso aos recursos naturalmente hierárquicos. O exemplo mais simples para ilustrar esse conceito é um sistema de arquivos com base em árvore. Se um aplicativo deseja adquirir uma trava exclusiva em um diretório, normalmente significa que também deseja acesso exclusivo aos arquivos e aos subdiretórios neste diretório. O método contains é utilizado para especificar o relacionamento hierárquico entre as travas. Se você não precisar criar hierarquias de travas, você pode implementar o método contains para simplesmente chamar isConflicting.

Aqui está um exemplo de uma trava hierárquica para controlar o acesso de gravação aos controles de java.io.File.

      public class FileLock implements ISchedulingRule {
      private String path;
      public FileLock(java.io.File file) {
         this.path = file.getAbsolutePath();
      }
      public boolean contains(ISchedulingRule rule) {
         if (this == rule)
         return true;
         if (rule instanceof FileLock)
            return path.startsWith(((FileLock) rule).path);
         if (rule instanceof MultiRule) {
            MultiRule multi = (MultiRule) rule;
            ISchedulingRule[] children = multi.getChildren();
            for (int i = 0; i < children.length; i++)
               if (!contains(children[i]))
    return false;
         return true;
            }
    return false;
      }
      public boolean isConflicting(ISchedulingRule rule) {
         if (!(rule instanceof FileLock))
    return false;
         String otherPath = ((FileLock)rule).path;
         return path.startsWith(otherPath) || otherPath.startsWith(path);
      }
   }

O método contains entra em reprodução se um encadeamento tentar adquirir uma segunda regra quando já tem uma. Para evitar a possibilidade de conflito, qualquer encadeamento fornecido pode ter somente uma regra de planejamento a qualquer momento. Se um encadeamento chama beginRule quando já possui uma regra, por uma chamada anterior de beginRule ou pela execução de uma tarefa com uma regra de planejamento, o método contains é consultado para verificar se as duas regras são equivalentes. Se o método contains para a regra já foi adquirido, ele retorna true, a chamada de beginRule será bem-sucedida. Se o método contains retorna false, ocorrerá um erro.

Para colocar isso em termos mais concretos, digamos que um encadeamento possui nossa regra FileLock de exemplo no diretório em "c:\temp". Enquanto possuir essa regra, só é permitido modificar arquivos dentro dessa subárvore de diretório. Se tentar modificar arquivos em outros diretórios que não estejam em "c:\temp", deve haver falha. Deste modo, uma regra de planejamento é uma especificação concreta para o que uma tarefa ou um encadeamento tem ou não permissão para fazer. Violar essa especificação resultará em uma exceção do tempo de execução. Na literatura de coincidência, essa técnica é conhecida como trava de duas fases. Em um esquema de trava de duas fases, um processo deve especificar, com antecedência, todas as travas necessárias para uma tarefa específica e não tem permissão para adquirir mais travas durante a operação. O travamento de duas fases elimina a condição de espera que é um pré-requisito para o conflito de espera circular. Portanto, é impossível para um sistema utilizando somente regras de planejamento como um travamento primitivo a digitação de um conflito.