2018년 2월 28일 수요일

Async Method Junit Test

When making junit test, it's hard to test async method.
I introduce the way to test async method by changing async to sync task executor.

@Service
@Slf4j
public class TestServiceImpl implements TestService {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    @Async
    public void getString() {

        log.info("start");
        restTemplate.getForEntity("http://www.test.com", String.class);
    }
}

We will test this async method as above.

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class TestServiceTest {

    @Configuration
    @Import(AsyncTestApplication.class)
    static class ContextConfiguration {
        @Bean
        @Primary
        public Executor executor() {
            return new SyncTaskExecutor();
        }
    }

    @Autowired
    private TestService testService;

    @MockBean
    private RestTemplate restTemplate;

    @Test
    public void testGetString() {

        //given
        given(restTemplate.getForEntity(anyString(), eq(String.class))).willReturn(new ResponseEntity(HttpStatus.OK));

        //when
        testService.getString();

        //then
        verify(restTemplate, times(1)).getForEntity(anyString(), eq(String.class));
    }
}


By using Configuration annotation, you can set the junit test config.
I will change the asyncExecutor to syncTaskExecutor,so that we could check the internal logic of async method.


2018-03-01 15:54:47.729  INFO 6836 --- [           main] o.b.test.service.impl.TestServiceImpl    : start

You can find your test case passed.


You can download whole project from below git url.

https://gitlab.com/shashaka/async-junit-test-project

2017년 11월 27일 월요일

filtering custom exception stacktrace

When you are using custom exception class,
there are a lot of stacktrace when it occurred.
So we can override the method named fillInStackTrace() as below.

 @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }

After that, when exception occured, it shows just exception message.

Caused by: org.blog.test.common.CustomException: custom exception occured

all the example is in the below url.

https://gitlab.com/shashaka/custom-exception-project

2017년 10월 26일 목요일

[Cassandra 레퍼런스] Replication 복제

key space의 복제 전략은 주어진 노드 범위에서 어떤 노드가 복제본이 될지 결정합니다.
두 가지 주요 복제 전략은 SimpleStrategy와 NetworkTopologyStrategy입니다.


SimpleStrategy

SimpleStrategy는 단일 정수가 replication_factor로 정의되도록 합니다.
이것은 각 행의 사본을 포함해야하는 노드의 수를 결정합니다.
예를 들어, replication_factor가 3이면 세 개의 다른 노드가 각 행의 사본을 저장해야합니다.

SimpleStrategy는 구성된 모든 데이터 센터 또는 랙을 무시하고 모든 노드를 동일하게 취급합니다.
토큰 범위의 복제본을 결정하기 위해 Cassandra는 관심있는 토큰 범위부터 시작하여 링의 토큰을 반복합니다.
각 토큰에 대해 소유 노드가 복제본 집합에 추가되었는지 여부를 확인하고 그렇지 않은 경우 집합에 추가됩니다.
이 프로세스는 replication_factor 갯수의 별개 노드가 복제본 세트에 추가 될 때까지 계속됩니다.


NetworkTopologyStrategy

NetworkTopologyStrategy를 사용하면 클러스터의 각 데이터 센터에 replication factor를 지정할 수 있습니다.
클러스터가 단일 데이터 센터 만 사용하는 경우 NetworkTopologyStrategy를 SimpleStrategy보다 우선적으로 사용해야 나중에 새로운 실제 또는 가상 데이터 센터를 클러스터에 쉽게 추가 할 수 있습니다.

DC별로 복제 요소를 지정할 수 있을 뿐만 아니라 NetworkTopologyStrategy는 다른 랙의 데이터 센터에서 복제본을 선택하려고 시도합니다.
랙 수가 DC의 복제 인수보다 크거나 같으면 각 복제본이 다른 랙에서 선택됩니다.
그렇지 않으면 각 랙에는 최소한 하나의 복제본이 보관되지만 일부 랙에는 둘 이상의 복제본이 보관 될 수 있습니다.
이 랙 인식 동작에는 잠재적으로 놀라운 영향이 있습니다.
예를 들어 각 랙에 짝수 개의 노드가 없으면 가장 작은 랙의 데이터로드가 훨씬 커질 수 있습니다.
마찬가지로 단일 노드가 새 랙으로 부트 스트랩되는 경우 전체 링의 복제본으로 간주됩니다.
이러한 이유 때문에 많은 운영자가 단일 "랙"에 모든 노드를 구성하려고합니다.


Tunable Consistency

Cassandra는 일관성 레벨을 통해 일관성과 가용성 간의 작업 당 절충점을 지원합니다.
기본적으로 작업의 일관성 수준은 작업을 성공으로 간주하기 위해 코디네이터에 응답해야하는 복제본 수를 지정합니다.

다음과 같은 일관성 수준을 사용할 수 있습니다 :

ONE : 하나의 복제본 만 응답해야합니다.
TWO : 두개의 복제본이 응답해야합니다.
THREE : 세개의 복제본이 응답해야합니다.
QUORUM : 대다수 (n / 2 + 1)의 복제본이 응답해야합니다.
ALL : 모든 복제본이 응답해야 합니다.
LOCAL_QUORUM : 로컬 데이터 센터의 복제본 (코디네이터가있는 데이터 센터 중 대부분)이 응답해야합니다.
EACH_QUORUM : 각 데이터 센터의 대부분의 복제본이 응답해야합니다.
LOCAL_ONE : 하나의 복제본 만 응답해야합니다. 다중 데이터 센터 클러스터에서는 읽기 요청이 원격 데이터 센터의 복제본으로 보내지지 않습니다.
ANY : 단일 복제본이 응답하거나 코디네이터가 힌트를 저장할 수 있습니다. 힌트가 저장되면 코디네이터는 나중에 힌트를 재생하여 복제본에 전달합니다. 이 일관성 레벨은 쓰기 조작에만 허용됩니다.

쓰기 작업은 일관성 수준에 관계없이 항상 모든 복제본으로 전송됩니다.
일관성 레벨은 클라이언트에 응답하기 전에 코디네이터가 대기하는 응답 수를 제어합니다.

읽기 조작의 경우, 코디네이터는 보통 일관성 레벨을 충족시키기에 충분한 복제본에 대해 읽기 명령을 행합니다.
여기에는 몇 가지 예외가 있습니다. :

- 위험한 재시도는 다른 복제본이 지정된 시간 창 내에서 응답하지 않은 경우 추가 복제본에 중복 읽기 요청을 발행 할 수 있습니다.

- read_repair_chance 및 dclocal_read_repair_chance (테이블 스키마의 일부)를 기반으로, 잠재적으로 일치하지 않는 데이터를 복구하기 위해 모든 복제본에 읽기 요청이 무작위로 전송 될 수 있습니다.


Picking Consistency Levels

겹칠 정도로 높은 읽기 및 쓰기 일관성 수준을 선택하면 "강한"일관성이 발생합니다.
이것은 일반적으로 W + R> RF로 표시됩니다. 여기서 W는 쓰기 일관성 레벨이고 R은 읽기 일관성 레벨이며 RF는 복제 인수입니다.
예를 들어 RF = 3 인 경우 QUORUM 요청에는 세 개의 복제본 중 두 개 이상의 응답이 필요합니다.
QUORUM이 쓰기 및 읽기에 모두 사용되는 경우 최소한 하나의 복제본이 쓰기 요청과 읽기 요청에 모두 참여하도록 보장되므로 가장 최근 쓰여진 데이터가 읽히게 됩니다.
다중 데이터 센터 환경에서는 LOCAL_QUORUM을 사용하여 더 약하지만 유용한 보증을 제공 할 수 있습니다 : 동일한 데이터 센터 내에서 가장 최근 쓰여진 데이터를 볼 수 있습니다.

이러한 유형의 강력한 일관성이 필요하지 않으면 처리량, 대기 시간 및 가용성을 향상시키기 위해 ONE과 같은 낮은 일관성 수준을 사용할 수 있습니다.

참조 : http://cassandra.apache.org/doc/latest/architecture/dynamo.html#replication

2017년 10월 10일 화요일

Spring transaction propagation test [REQUIRES_NEW]

We will test the propagation of spring transaction.

First of all, you can declare [PersonService] that use the table named person.
That service will be using PersonNewService that can be changed the propagation of transaction by the user.
And then you can check the commit, rollback states.

@Service
@Transactional
@Slf4j
public class PersonServiceImpl implements PersonService {

    @Autowired
    private PersonRepository personRepository;

    @Autowired
    private PersonNewService personNewService;

    @Override
    @Transactional
    public void addPerson(String name) throws Exception {
        Person person = new Person();
        person.setName(name);
        personRepository.save(person);

        personNewService.addNewPerson("newName");
        
    }
}

@Service
@Slf4j
public class PersonNewServiceImpl implements PersonNewService {

    @Autowired
    private PersonRepository personRepository;

    @Autowired
    private PersonService personService;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addNewPerson(String newName) {

        Person newPerson = new Person();
        newPerson.setName(newName);
    }
}

I'm going to check the logic whether it's working fine.

When the API is called, you can see the result as below.

mysql> select * from person;
+----+---------+
| id | name    |
+----+---------+
| 18 | dfefe   |
| 19 | newName |
+----+---------+
2 rows in set (0.00 sec)


By using "TransactionSynchronizationManager.getCurrentTransactionName()",
you can check that these two methods are working in different transaction each.

2017-10-08 18:06:41.383  INFO 15092 --- [nio-8080-exec-1] o.b.t.p.service.impl.PersonServiceImpl   : addPerson : org.blog.test.person.service.impl.PersonServiceImpl.addPerson
2017-10-08 18:06:41.388  INFO 15092 --- [nio-8080-exec-1] o.b.t.p.s.impl.PersonNewServiceImpl      : addNewPerson : org.blog.test.person.service.impl.PersonNewServiceImpl.addNewPerson

When exception occured, I will check the rollback and commit state.
Exception occured by the method 'throw new RuntimeException();'.

1. After addNewPerson is called, In addPerson,Exception occured

기존의 transaction A가 실행되고, 새로운 transaction B가 실행되고 commit된 이후,
transaction A로 다시 돌아와서 해당 transaction A가 끝나기 전 Exception이 발생한 경우입니다.

transaction A was executed, and then new transaction B is executed and committed.
After that, exception occured before transaction A ended.

In API, exception occured, however, transaction B is already committed.
So the statement in transaction B is inserted regardless of exception.
However, rollback occured in transaction A, so the statement in transaction A is not inserted to DB.

mysql> select * from person;
+----+---------+
| id | name    |
+----+---------+
| 23 | newName |
+----+---------+
1 row in set (0.00 sec)


2. Before addNewPerson is called, In addPerson, Exception occured

Before transaction B, rollback occured in transaction A,
So there's no commit in transaction A,B both.

mysql> select * from person;
Empty set (0.00 sec)

3. After addNewPerson is called, in addNewPerson, exception occured

Exception occured in transaction B, and then rollback occured in transaction B.
Transaction A also not committed.

mysql> select * from person;
Empty set (0.00 sec)

4. After addNewPerson is called, exception occured in the method that is called from addNewPerson

External method is also proceed in transaction B. so all the transactions are rollbacked.

mysql> select * from person;
Empty set (0.00 sec)


2017년 10월 8일 일요일

Spring transaction propagation 테스트 [REQUIRES_NEW]

현재 spring에서 선언할 수 있는 transaction의 격리 수준을 테스트 해보도록 하겠습니다.

가장 먼저 아래와 같이 person이라는 table에 접근하는 서비스 [PersonService]를 선언하도록 합니다.
해당 서비스에서는 PersonNewService 라는 서비스를 접근하게 되고, 해당 서비스는 transaction이 격리 수준을 변화시켜
어떻게 commit, rollback 등이 진행되는지 알아보도록 하겠습니다.

@Service
@Transactional
@Slf4j
public class PersonServiceImpl implements PersonService {

    @Autowired
    private PersonRepository personRepository;

    @Autowired
    private PersonNewService personNewService;

    @Override
    @Transactional
    public void addPerson(String name) throws Exception {
        Person person = new Person();
        person.setName(name);
        personRepository.save(person);

        personNewService.addNewPerson("newName");
        
    }
}

@Service
@Slf4j
public class PersonNewServiceImpl implements PersonNewService {

    @Autowired
    private PersonRepository personRepository;

    @Autowired
    private PersonService personService;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addNewPerson(String newName) {

        Person newPerson = new Person();
        newPerson.setName(newName);
    }
}

가장 먼저, 정상 동작하는 지 알아보도록 하겠습니다.

해당 API를 호출하였을 때, 정상적으로 아래와 같이 값이 입력되는 것을 확인할 수 있습니다.

mysql> select * from person;
+----+---------+
| id | name    |
+----+---------+
| 18 | dfefe   |
| 19 | newName |
+----+---------+
2 rows in set (0.00 sec)

또한, "TransactionSynchronizationManager.getCurrentTransactionName()" 메소드를 통해,
현재 transaction의 name을 가져왔을 때 두 메소드 내에서 다른 이름의 transaction이 실행되고 있는 것을 확인할 수 있습니다.

2017-10-08 18:06:41.383  INFO 15092 --- [nio-8080-exec-1] o.b.t.p.service.impl.PersonServiceImpl   : addPerson : org.blog.test.person.service.impl.PersonServiceImpl.addPerson
2017-10-08 18:06:41.388  INFO 15092 --- [nio-8080-exec-1] o.b.t.p.s.impl.PersonNewServiceImpl      : addNewPerson : org.blog.test.person.service.impl.PersonNewServiceImpl.addNewPerson

그럼 Exception 발생시의 rollback 및 commit 상태에 대해 테스트를 진행해보도록 하겠습니다.
Exception은 "throw new RuntimeException();"을 통해 발생시키도록 합니다.

1. addNewPerson이 호출된 이후, addPerson에서 Exception 발생한 경우

기존의 transaction A가 실행되고, 새로운 transaction B가 실행되고 commit된 이후,
transaction A로 다시 돌아와서 해당 transaction A가 끝나기 전 Exception이 발생한 경우입니다.

전체 API에서는 exception이 발생하게 되고, 새로운 transaction B는 이미 commit 된 이후이기 때문에
exception 여부와 상관없이 transaction B에서 실행된 내역은 그대로 DB에 입력되게 됩니다.
하지만, 기존 transaction A는 rollback되기 때문에 addPerson에서 입력한 값은 commit되지 않는 것을 확인할 수 있습니다.

mysql> select * from person;
+----+---------+
| id | name    |
+----+---------+
| 23 | newName |
+----+---------+
1 row in set (0.00 sec)


2. addNewPerson이 호출된 이전, addPerson에서 Exception 발생한 경우

transaction B가 시작하기 전, transaction A가 rollback되기 때문에
transaction A,B 모두 commit 되지 않습니다.

mysql> select * from person;
Empty set (0.00 sec)

3. addNewPerson이 호출된 이후, addNewPerson에서 exception이 발생한 경우

transaction B에서 exception이 발생 후, transaction B가 rollback되고
transaction A도 반영되지 않습니다.

mysql> select * from person;
Empty set (0.00 sec)

4. addNewPerson이 호출된 이후, addNewPerson에서 호출한 외부 메서드에서 Exception이 발생한 경우

외부 호출 메서드 역시 transaction B에서 진행되며 해당 transaction에서 exception이 발생 후,
transaction B가 rollback되고 transaction A도 반영되지 않습니다.

mysql> select * from person;
Empty set (0.00 sec)

감사합니다~!!

2017년 9월 20일 수요일

Using spring data cassandra

You can use cassandra by using spring data cassandra.

First of all, you should set the access infomation to application.yml file matched your cassandra setting.

CREATE KEYSPACE myspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};

CREATE TABLE users (
    id UUID,
    name varchar,
    PRIMARY KEY (id)
);

spring:
  data:
    cassandra:
      contact-points: {server host}
      username: {username}
      password: {password}
      keyspace-name: myspace

And then, you can define the entity for your cassandra table.

package org.blog.test.user.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.cassandra.mapping.PrimaryKey;
import org.springframework.data.cassandra.mapping.Table;

import java.util.UUID;

@Table(value = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @PrimaryKey
    private UUID id;

    private String name;

}

After making repository, service layer, I set the command to add user when tomcat starting.

select * from users;

 id                                   | name
--------------------------------------+----------
 339bc454-91ae-48f7-bce7-f00e62a00d7f | testUser


You can download the example as below.

https://gitlab.com/shashaka/cassandra-project

[Cassandra] allowing remote access and security setting

allowing remote access

As default, cassandra is not allowing remote access from the other client outside.
To allow the access from the other client, we can change the setting in cassandra.yaml file.

Open the cassandra setting file.

sudo vi /etc/cassandra/cassandra.yaml

Change the setting related in rpc_address and broadcast_rpc_address.

rpc_address : 0.0.0.0
broadcast_rpc_address : 1.2.3.4

The default value of rpc_address is 'localhost',so we can change this value as 0.0.0.0 that means wild card.
broadcast_rpc_address is remarked as default, if you set the rpc_address as 0.0.0.0 , you should remove that remark.

Now cassandra is allowing the access from the client outside.
However, that client can control this cassandra without any authentication.
So you should set securty setting for your cassandra.


setting security

In cassandra setting file, change the value of authenticator.

authenticator: PasswordAuthenticator

The default value of authenticator is AllowAllAuthenticator, and we change the value to 'PasswordAuthenticator'.
As default, the administrator account is set by cassandra/cassandra.
For security, we will remove that account ,and create new super user.

Login cassandra as administrator.

cqlsh -u cassandra -p cassandra

Add new super user.

create user root with password 'password' superuser;

After log in by super user, and remove the account of cassandra.

cqlsh -u root -p password
drop user cassandra;

Check the setting is applied correctly.

list users;

reference : http://www.bloger.kr/54