Метод Spring Retry @Recover для компонента-прототипа, реализующего Runnable, без использования поля компонента-прототипа

У меня есть прототип bean-компонента, реализующий Runnable, который должен повторить свой run-метод и что-то сделать, если достигнуто максимальное количество повторов. Теперь у меня проблема в том, что метод восстановления, кажется, всегда вызывается из одного и того же компонента Spring, а не из соответствующего экземпляра.

Это мой код на данный момент:

RetryableRunnable

@Slf4j
@AllArgsConstructor
public class RetryableRunnable implements Runnable {

   private final RetryDemoService retryDemoService;
   private final Status someStatus;

   @Override
   @Retryable(
         value = { RuntimeException.class },
         maxAttempts = 2,
         backoff = @Backoff(delay = 2000))
   public void run() {
      log.info( "+++ RetryableRunnable executed! +++" );
      retryDemoService.demoRun();
   }

   @Recover
   private void recover() {
      retryDemoService.demoRecover();
      log.info( String.valueOf( someStatus ) );
   }
}

Конфигурация

@Configuration
@AllArgsConstructor
@EnableRetry(proxyTargetClass = true)
public class RetryDemoConfig {

   private final RetryDemoService retryDemoService;

   @Bean
   @Scope( "prototype" )
   public RetryableRunnable retryableRunnable(Status status) {
       return new RetryableRunnable( retryDemoService, status );
   }
}

Сервис

@Service
@Slf4j
public class RetryDemoService {

   void demoRun() {
      log.info( "+++ Run! +++" );
   }

   void demoRecover() {
      log.info( "+++ Recover! +++" );
   }

}

Статусное перечисление

public enum Status {
   STATUS1, STATUS2
}

Протестируйте, чтобы выявить проблему

@RunWith( SpringRunner.class )
@SpringBootTest
public class RetryableRunnableTest {

   @Autowired
   private BeanFactory beanFactory;

   @MockBean
   RetryDemoService retryDemoService;

   @Test
   public void retrieableRunnableIsRetriedOnlyThreeTimesAndRecoverMethodIsRun() throws InterruptedException {
      RetryableRunnable testInstance1 = beanFactory.getBean( RetryableRunnable.class, Status.STATUS1 );
      RetryableRunnable testInstance2 = beanFactory.getBean( RetryableRunnable.class, Status.STATUS2 );
      doThrow( new RuntimeException() )
          .doThrow( new RuntimeException() )
          .doThrow( new RuntimeException() )
          .when( retryDemoService ).demoRun();

      Thread thread1 = new Thread( testInstance1 );
      thread1.start();
      thread1.join();

      Thread thread2 = new Thread( testInstance2 );
      thread2.start();
      thread2.join();
    }
}

Теперь вывод журнала:

+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS1
+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS1

Пока должно быть:

+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS1
+++ RetryableRunnable executed! +++
+++ RetryableRunnable executed! +++
STATUS2

Когда я отлаживаю этот тестовый метод, RetryRunnable @ 3053 вызывает метод восстановления в первый и второй раз!

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


person Nas3nmann    schedule 27.11.2017    source источник


Ответы (1)


Область действия прототипа в настоящее время не поддерживается.

Есть только один AnnotationAwareRetryOperationsInterceptor, и он кэширует делегат RetryOperationsInterceptors на основе Method, а не экземпляра объекта ...

private MethodInterceptor getDelegate(Object target, Method method) {
    if (!this.delegates.containsKey(method)) {
         ...
    }
    return this.delegates.get(method);
}

Вызывается соответствующий метод @Retryable, но все экземпляры будут вызывать первый кешированный @Recoverer.

Кэш должен быть изменен на ключевой для комбинации целевого объекта и Method.

Вы можете открыть проблему на github, указав этот вопрос.

Взносы приветствуются.

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

@SpringBootApplication
@EnableRetry
public class So47513907Application {

    private static final Log log = LogFactory.getLog(So47513907Application.class);

    public static void main(String[] args) {
        SpringApplication.run(So47513907Application.class, args);
    }

    @Bean
    public ApplicationRunner runner(ApplicationContext ctx) {
        return args -> {
            Baz baz1 = ctx.getBean(Baz.class, "one");
            Baz baz2 = ctx.getBean(Baz.class, "two");
            baz1.foo();
            baz2.foo();
        };
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Baz baz(String arg) {
        return new Baz(arg);
    }

    @Service
    public static class Foo {

        void demoRun(Baz baz) {
            log.info(baz.instance + " +++ Run! +++");
            throw new RuntimeException();
        }

        void demoRecover(Baz baz) {
            log.info(baz.instance + " +++ Recover! +++");
        }

    }

    public interface Bar {

        void foo();

        void bar();

    }

    public static class Baz implements Bar {

        public String instance;

        @Autowired
        private Foo foo;

        public Baz(String instance) {
            this.instance = instance;
        }

        @Retryable
        @Override
        public void foo() {
            log.info(this.instance);
            foo.demoRun(this);
        }

        @Recover
        @Override
        public void bar() {
            log.info("recover: " + this.instance);
            foo.demoRecover(this);
        }

    }

}

ИЗМЕНИТЬ

Самый простой обходной путь - использовать RetryTemplate вместо аннотации:

@Bean
public RetryTemplate retryTemplate() {
    RetryTemplate template = new RetryTemplate();
    template.setRetryPolicy(new SimpleRetryPolicy(2));
    return template;
}

public static class Baz implements Bar {

    public String instance;

    @Autowired
    private Foo foo;

    @Autowired
    private RetryTemplate retryTemplate;

    public Baz(String instance) {
        this.instance = instance;
    }

//  @Retryable
    @Override
    public void foo() {
        this.retryTemplate.execute(context -> {
            log.info(this.instance);
            foo.demoRun(this);
            return null;
        }, context -> {
            bar();
            return null;
        });
    }

//  @Recover
    @Override
    public void bar() {
        log.info("recover: " + this.instance);
        foo.demoRecover(this);
    }

}

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

person Gary Russell    schedule 27.11.2017
comment
Нет ли обходного пути? Может ли передача STATUS в качестве аргумента метода быть возможным решением? - person Nas3nmann; 28.11.2017