jam2in / arcus-java-client

Arcus Java client
Apache License 2.0
0 stars 0 forks source link

Map 자료구조 직렬화, 역직렬화 버그 #43

Open vcho1958 opened 1 year ago

vcho1958 commented 1 year ago
jhpark816 commented 1 year ago

문제 발생하는 코드를 예로 들어 설명해 주면 나을 것 같습니다. 위에 설명이 있으니, 예제 코드만 넣어주면 될 것으로 보입니다. 필요하다면, 코멘트 추가해 주시고요.

참고로, collection에서 element 값들은 모두 동일 유형(타입)의 객체이어야 합니다.

vcho1958 commented 1 year ago

말씀하신대로 Map 자료형의 요소들마다 다른 유형을 넣어서 발생한 버그인 것 같습니다. 세션의 경우 각 필드별로 변경요소가 많을 수 있어서 서로 다른 자료형을 하나의 Map콜렉션에 집어넣을 필요성이 있었습니다.

다음은 문제가 된 데이터가 생성되는 과정입니다.

            this.delta.put(ArcusSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
            this.delta.put(ArcusSessionMapper.MAX_INACTIVE_INTERVAL_KEY, cached.getMaxInactiveInterval().getSeconds());
            this.delta.put(ArcusSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());

이런 자료가 arcus에 추가가 되고

public class User implements UserDetails, Serializable {
    private static final long serialVersionUID = -1;
    @Id
    @GeneratedValue
    private Long id;
    private String password;
    private String username;
    @OneToMany(mappedBy = "user")

    private List<UserRole> roles = new ArrayList<>();
    @JsonFormat(pattern = "yyyy-MM-dd kk:mm:ss")
    private LocalDateTime createdAt;
    @JsonFormat(pattern = "yyyy-MM-dd kk:mm:ss")
    private LocalDateTime lastAccessedAt;
    private boolean locked = false;

    @Transient
    private Duration maxInactiveInterval = Duration.ofSeconds(3600);

    @Transient
    private Instant lastAccessedTime = Instant.now();

    static public User of(String username, String encodedPassword) {
        return User.builder()
            .username(username)
            .password(encodedPassword)
            .createdAt(LocalDateTime.now())
            .lastAccessedAt(LocalDateTime.now())
            .locked(false)
            .build();
    }

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<Role> roles = new ArrayList<>();
        this.roles.forEach(role -> roles.add(role.getRole()));
        return roles;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        Duration maxAccessInterval = Duration.ofSeconds(7776000); //90일
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return !locked;
    }

    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return isAccountNonExpired() && isAccountNonLocked() && isCredentialsNonExpired();
    }
}

이런 자료구조가

    @PostMapping("/auth/login")
    public boolean login(@RequestBody LoginDto loginDto, HttpSession session){
        log.info(loginDto.toString());
        UserDetails user = authService.login(loginDto);
        session.setAttribute("principal", user);
        return true;
    }

setAttribute가 끝난 후 해당 콜렉션에 삽입됩니다.

Arcus에 저장될 때는

            this.delta.forEach((dkey, value)->{
                Boolean response = null;
                try {

                    response = ArcusSessionRepository.this.arcusClientPool.asyncMopInsert(key, dkey, value, collectionAttributes).get();
                    if(!response)
                        ArcusSessionRepository.this.arcusClientPool.asyncMopUpdate(key,dkey,value).get();

                    ArcusSessionRepository.this.arcusClientPool.asyncSetAttr(key, collectionAttributes).get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                    throw new RuntimeException(e);
                }

            });
            this.delta.clear();

이런 코드에 의해 저장이 됐습니다.

이후 다시 불러올 땐

    public ArcusSession findById(String sessionId) {
        String key = getSessionKey(sessionId);
        Map<String, Object> entries = null;
        try {
             entries = toDeserializedMap(this.arcusClientPool.asyncMopGet(key,false,false).get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        if (entries == null || entries.isEmpty()) {
            return null;
        }
        MapSession session = new ArcusSessionMapper(sessionId).apply(entries);
        if (session.isExpired()) {
            deleteById(sessionId);
            return null;
        }
        return new ArcusSession(session, false);
    }

이런 코드로 불러와서

    public MapSession apply(Map<String, Object> map) {
        Assert.notEmpty(map, "map must not be empty");
        MapSession session = new MapSession(this.sessionId);
        Long creationTime = (Long) map.get(CREATION_TIME_KEY);
        if (creationTime == null) {
            handleMissingKey(CREATION_TIME_KEY);
        }
        session.setCreationTime(Instant.ofEpochMilli(creationTime));
        Long lastAccessedTime = (Long) map.get(LAST_ACCESSED_TIME_KEY);
        if (lastAccessedTime == null) {
            handleMissingKey(LAST_ACCESSED_TIME_KEY);
        }
        session.setLastAccessedTime(Instant.ofEpochMilli(lastAccessedTime));
        Long maxInactiveInterval = (int) map.get(MAX_INACTIVE_INTERVAL_KEY);
        if (maxInactiveInterval == null) {
            handleMissingKey(MAX_INACTIVE_INTERVAL_KEY);
        }
        session.setMaxInactiveInterval(Duration.ofSeconds(maxInactiveInterval));
        map.forEach((name, value) -> {
            if (name.startsWith(ATTRIBUTE_PREFIX)) {
                session.setAttribute(name.substring(ATTRIBUTE_PREFIX.length()), value);
            }
        });
        return session;
    }

apply함수에 의해 세션 인스턴스로 매핑되는 과정 중 (int) map.get(MAX_INACTIVE_INTERVAL_KEY);에 실패했다는 메세지가 발생합니다.

vcho1958 commented 1 year ago

명칭은 잘 기억이 안나지만 잭슨의 deserializer처럼 유사한 역할을 하는 클래스가 있었던 것 같은데 문서에서 찾지 못했습니다. Transcoder였던것 같은데 확장 방법에 대한 매뉴얼이 있을까요?

필요시 JPA의 어노테이션처럼 대상이되는 클래스에 추가하면 리플렉션을 이용해 컴파일시점에 해당 deserializer를 자동 구현하게하는 어노테이션을 추가하겠습니다.

jhpark816 commented 1 year ago

@vcho1958 arcus-java-client API 기준으로 상황을 설명해 주어야 이해하기 편할 것 같습니다.

vcho1958 commented 1 year ago
arcusClientPool.asyncMopInsert(key, dkey, value, collectionAttributes);

를 통해 값들을 넣었을 때 모든 필드에 대한 타입이 가장 먼저 들어가는 값(A)의 타입으로 강제 지정됩니다. arcusClientPool.asyncMopGet(key,false,false) 이후 조회하여 원래 클래스로 형변환을 시도하면 A를 B로 형변환에 실패했다는 메세지가 출력됩니다.

arcusClientPool.asyncMopCreate(key, ElementValueType.OTHERS, collectionAttributes) 생성 후 시도하면 조회시 모든 타입에서 caught ioexception decoding 2(자료형의 크기) bytes of data 에러로그가 출력되고 null이 반환됩니다.

uhm0311 commented 1 year ago

쉽게 말하자면 Map<String, Object> 타입을 arcus에 저장할 수 없다는 의미입니다. Arcus의 Map은 정해진 하나의 타입만 넣을 수 있고, 이 이슈에서는 여러 타입(원시 타입 포함)의 객체들을 넣고 싶은 경우이기 때문입니다.

jhpark816 commented 1 year ago

현재 저장하는 객체의 타입을 cache item의 flags 정보로 기록해 두게 되는 데, 1개의 flags 정보만 cache item header에 둘 수 있습니다. 즉, 1개 객체 타입만 지정 가능합니다.

list, set, b+tree 경우에는 동일 타입의 Object 사용하는 것이 일반적인 데, map 경우는 서로 다른 타입의 Object 사용하는 경우가 많을 수 있겠네요.

1개 타입의 object 만 사용하도록 java client map API를 변경할 수 있나요? String 타입으로 고정하는 것이 하나의 방안인데, 응용에서 String이 아닌 동일 객체 타입의 객체만을 사용한다면 기존대로 Object 타입으로 하면 되구요. 아니면, 가이드 문서에 주의사항 코멘트를 남기는 것이 좋은가요?

uhm0311 commented 1 year ago

@vcho1958

이슈 주제와 관련 없는 이야기지만, 앞으로는 https://github.com/naver/arcus-java-client 레포지토리에 이슈를 등록해주세요.

vcho1958 commented 1 year ago

넵 주의사항 코멘트를 남기는 편이 좋을 것 같습니다. 이외에도 collection attribute를 insert할 때 지정하여 자동생성이 된 map이 아닌, create를 통해 타입을 OTHERS로 지정하면 위에서 말씀드린 것처럼 decode시에 오류가 발생합니다.

uhm0311 commented 1 year ago

@jhpark816

아니면, 가이드 문서에 주의사항 코멘트를 남기는 것이 좋은가요?

1.13.4 릴리즈 전에 문서에 포함할까요?

jhpark816 commented 1 year ago

@uhm0311 기존 interface에서 달라지는 부분이 아니므로, 1.13.4 릴리즈에선 그대로 두시죠. 하지만, 나중에 논의해야 할 주제라고 생각합니다.