2016-10-09 1 views
1

현재 dropwizard 프레임 워크와 함께 dropWizard-hibernate, JPA/Hibernate (PostgreSQL 데이터베이스 사용)와 함께 REST API 웹 서비스를 구현 중입니다. 전체 요청에 대해 하나의 트랜잭션을 얻기 위해 @UnitOfWork이라는 주석이 달린 리소스 내에 메소드가 있습니다. 리소스 메서드는 내 DAO 중 하나의 메서드를 호출하고 AbstractDAO<MyEntity>을 확장하며 내 엔터티 (유형 MyEntity)의 검색 또는 수정을 데이터베이스와 교환하는 데 사용됩니다.Dropwizard/JPA/Hibernate에서 트랜잭션/요청의 자동 재 시도

DAO 메서드는 다음을 수행합니다. 먼저 엔티티 인스턴스를 선택하고 데이터베이스의 행을 선택합니다. 그런 다음 엔티티 인스턴스가 검사되고 속성에 따라 일부 속성이 변경 될 수 있습니다. 이 경우, 데이터베이스의 행을 갱신해야합니다. 어디에서나 캐싱, 잠금 또는 트랜잭션 관련 사항을 지정하지 않았으므로 기본값이 Hibernate에 의해 강제되는 일종의 낙관적 잠금 메커니즘이라고 가정합니다. 따라서 현재 인스턴스의 데이터베이스에서 엔티티 인스턴스를 선택한 후 다른 스레드에서 엔티티 인스턴스를 삭제할 때 업데이트해야하는 엔티티 인스턴스가 이전에 삭제 되었기 때문에 트랜잭션을 커밋하려고하면 StaleStateException이 발생합니다. 다른 스레드.

@UnitOfWork 주석을 사용할 때는 DAO 메서드 나 리소스 메서드에서이 예외를 catch 할 수 없다는 점을 이해했습니다. 저어지가 Retry-After header 또는 그와 비슷한 HTTP 503 응답을 클라이언트에 전달하여 요청을 다시 시도하도록 클라이언트에 알려주도록 ExceptionMapper<StaleStateException>을 구현할 수 있습니다. 하지만 우선 서버에서 요청/트랜잭션 (기본적으로 @UnitOfWork 주석으로 인해 동일 함)을 다시 시도하고 싶습니다.

Dropwizard를 사용할 때 서버 측 트랜잭션 재시도 메커니즘에 대한 구현 예가 있습니까? 구성 가능한 시간 (예 : 3) 재 시도하고 예외/HTTP 503 응답으로 실패하는 것과 같습니다. 어떻게 구현하나요? 먼저 내 리소스에 추가 할 수있는 @Retry(exception = StaleStateException.class, count = 3)과 같은 또 다른 주석이 마음에 들었습니다. 이것에 대한 제안 사항이 있으십니까? 다른 잠금/트랜잭션 관련 문제를 고려하여 내 문제의 대체 솔루션이 있습니까?

+0

내가 guice 및 인터셉터와 같은 일을하고 있어요 . 저지를 통해 동일을 적용하는 것을 막을 수있는 방법은 없습니다. 그러나 IMO 저지는 요청을 해할 수 있는지 확인해야하는 문제가 있습니다 (예 : 필터를 통해 수행 한 경우).guice method 인터셉터를 사용하면 예외를 잡아 내고 동일한 메소드를 다시 호출 할 수 있습니다. – pandaadb

+0

방금 ​​나는이 PR을 발견했다 : https://github.com/dropwizard/dropwizard/pull/1361 이것으로 나는 DAO의 일을하는 비공개 적 방법 ('@ UnitOfWork'으로 주석 됨)을 만들 수 있었다. 메서드는'StaleStateException'을 발생시킵니다. 그런 다음 첫 번째 방법을 사용하고 재 시도 메커니즘을 구현하는 또 다른 방법을 작성했습니다. 그래서 기본적으로 한 것은 자원 메소드에서 다른 메소드로 세션 라이프 사이클을 이동 시켜서 재 시도 메커니즘을 구축 할 수 있도록하는 것입니다. 아무도 더 나은 접근법을 제시 할 수없는 경우 곧 답변으로 추가하겠습니다. – mxscho

답변

0

나를 도울 수있는 Dropwizard 저장소에 a pull request이 있습니다. 기본적으로 자원 메소드가 아닌 @UnitOfWork 주석을 사용할 수 있습니다.

이것을 사용하여 리소스 메소드에서 @UnitOfWork 주석을 리소스 메소드에서 DAO 메소드로 이동시킴으로써 리소스 메소드에서 세션 열기/닫기 및 트랜잭션 생성/커밋 라이프 사이클을 분리 할 수있었습니다. StaleStateException. 그런 다음이 DAO 메서드를 중심으로 재시도 메커니즘을 만들 수있었습니다.

예시적인 설명 :

// class MyEntityDAO extends AbstractDAO<MyEntity> 
@UnitOfWork 
void tryManipulateData() { 
    // Due to optimistic locking, this operations cause a StaleStateException when 
    // committed "by the @UnitOfWork annotation" after returning from this method. 
} 

// Retry mechanism, implemented wheresoever. 
void manipulateData() { 
    while (true) { 
     try { 
      retryManipulateData(); 
     } catch (StaleStateException e) { 
      continue; // Retry. 
     } 
     return; 
    } 
} 

// class MyEntityResource 
@POST 
// ... 
// @UnitOfWork can also be used here if nested transactions are desired. 
public Response someResourceMethod() { 
    // Call manipulateData() somehow. 
} 

물론 하나는 않고 대신 직접 DAO 방식에 적용하는 DAO를 활용하는 서비스 클래스 내부의 방법에 @UnitOfWork 주석을 첨부 할 수 있습니다. 어노테이션이 사용되는 모든 클래스에서 풀 요청에 설명 된대로 UnitOfWorkAwareProxyFactory을 사용하여 인스턴스의 프록시를 만드는 것을 잊지 마십시오.

+0

이 방법이 가장 적합한 지 확실하지 않으므로이 대답을 수락하기 전에 더 나은 방법을 아직 열어두고 있습니다. – mxscho

+0

원한다면 guice 인터셉터로 답변을 게시 할 수 있습니다. – pandaadb

+0

너무 많은 일을하지 않으면 정말보고 싶습니다. :) – mxscho

1

대체 접근법은 내 경우에는 guice 인 주입 프레임 워크를 사용하고 이에 대한 메소드 인터셉터를 사용하는 것입니다. 이것은보다 일반적인 솔루션입니다. guice와

DW의 integreates 매우 원활 https://github.com/xvik/dropwizard-guicey

을 통해 나는 어떤 예외를 재 시도 할 수있는 일반적인 구현을 가지고있다. 다음은, 주석에, 당신 같이 작동합니다

@Target({ElementType.TYPE, ElementType.METHOD}) 
@Retention(RetentionPolicy.RUNTIME) 
public @interface Retry { 

} 

인터셉터는 다음 (문서 도구)을 수행합니다

/** 
* Abstract interceptor to catch exceptions and retry the method automatically. 
* Things to note: 
* 
* 1. Method must be idempotent (you can invoke it x times without alterint the result) 
* 2. Method MUST re-open a connection to the DB if that is what is retried. Connections are in an undefined state after a rollback/deadlock. 
* You can try and reuse them, however the result will likely not be what you expected 
* 3. Implement the retry logic inteligently. You may need to unpack the exception to get to the original. 
* 
* @author artur 
* 
*/ 
public abstract class RetryInterceptor implements MethodInterceptor { 

    private static final Logger log = Logger.getLogger(RetryInterceptor.class); 

    @Override 
    public Object invoke(MethodInvocation invocation) throws Throwable { 
     if(invocation.getMethod().isAnnotationPresent(Retry.class)) { 
      int retryCount = 0; 
      boolean retry = true; 
      while(retry && retryCount < maxRetries()) { 
       try { 
        return invocation.proceed(); 
       } catch(Exception e) { 
        log.warn("Exception occured while trying to executed method", e); 
        if(!retry(e)) { 
         retry = false; 
        } { 
         retryCount++; 
        } 
       } 
      } 
     } 
     throw new IllegalStateException("All retries if invocation failed"); 
    } 

    protected boolean retry(Exception e) { 
     return false; 
    } 

    protected int maxRetries() { 
     return 0; 
    } 

} 

몇 가지이 방법에 대한주의.

  • 재 시도 된 방법

  • 데이터베이스 예외가 있습니다 (다음 번 실행 단위의 형태의 방법을 저장 임시 결과는 두 배 증가 할 수 예를 들어 경우) 어떤 결과 변경하지 않고 여러 번 호출 할 수 있도록 설계되어야한다 일반적으로 재 시도를 위해 저장되지 않습니다. (내 경우 재 시도 교착 상태 인 경우 특히) 그들은이 기본 구현은 단순히 대표에게 다음 구현하는 클래스에 재시도 횟수 및 탐지 아무것도 잡아보다

기타 새로운 연결을 열어야합니다. 예를 들어, 내 특정 교착 상태 재시도 인터셉터 :

public class DeadlockRetryInterceptor extends RetryInterceptor { 

    private static final Logger log = Logger.getLogger(MsRetryInterceptor.class); 

    @Override 
    protected int maxRetries() { 
     return 6; 
    } 

    @Override 
    protected boolean retry(Exception e) { 
     SQLException ex = unpack(e); 
     if(ex == null) { 
      return false; 
     } 
     int errorCode = ex.getErrorCode(); 
     log.info("Found exception: " + ex.getClass().getSimpleName() + " With error code: " + errorCode, ex); 
     return errorCode == 1205; 
    } 

    private SQLException unpack(final Throwable t) { 
     if(t == null) { 
      return null; 
     } 

     if(t instanceof SQLException) { 
      return (SQLException) t; 
     } 

     return unpack(t.getCause()); 
    } 
} 

그리고 마지막으로, 내가 수행하여 guice이 바인딩 할 수 있습니다 :

bindInterceptor(Matchers.any(), Matchers.annotatedWith(Retry.class), new MsRetryInterceptor()); 
모든 클래스를 확인

및 재시 주석 어떤 방법을. 재 시도에 대한

예 방법은 다음과 같습니다

@Override 
    @Retry 
    public List<MyObject> getSomething(int count, String property) { 
     try(Connection con = datasource.getConnection(); 
       Context c = metrics.timer(TIMER_NAME).time()) 
     { 
      // do some work 
      // return some stuff 
     } catch (SQLException e) { 
      // catches exception and throws it out 
      throw new RuntimeException("Some more specific thing",e); 
     } 
    } 

내가 압축 풀기를 필요로하는 이유는 오래된 기존의 경우는,이 ​​DAO의 IMPL처럼, 이미 자신의 예외를 잡을 것입니다.

메소드 (A GET가) 내 데이터 소스 풀에서 두 번 호출 새 연결을 검색하는 방법도

주, 별도의 수정 작업이 그 안에 이루어지지하는 방법 (따라서 : 안전 재시도)

나는이 도움이되기를 바랍니다.

ApplicationListeners 또는 RequestFilters 또는 유사한 구현하여 비슷한 일을 할 수 있지만이 guice 바인딩 된 모든 메서드에 대한 모든 종류의 오류를 다시 시도 할 수있는보다 일반적인 방법이라고 생각합니다. 또한

이 클래스를 구성하는 경우에만 방법 인터셉트 (주석 생성자 등을 주입) 할 수 guice에 유의하는 데 도움이

희망,

아르투르

+0

당신에 대한 자세한 설명에 진심으로 감사드립니다! 시간이 들었을 때 이것을 들여다 보겠다. 매우 유망한 것처럼 보이기 때문에 @UnitOfWork 주석을 옮긴 경우에도 리소스와 DAO 사이에 레이어를 추가 할 수있다. 따라서 upvote. – mxscho

+0

확실히. 인터셉터를 올바로 주문하는지 확인해야합니다. :) – pandaadb

관련 문제