Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework:spring-webflux'
implementation 'com.github.f4b6a3:uuid-creator:6.0.0'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,40 @@
@Service
public class CollaborationService {
private final SessionRegistry sessionRegistry;
private final RedisStreamRepository redisStreamRepository;
private final RedisStreamStore redisStreamStore;

public void handleConnect(WebSocketSession session) {
sessionRegistry.addSession(getMindmapId(session), session);
}

public void processMessage(WebSocketSession sender, BinaryMessage message) {
ByteBuffer buffer = message.getPayload();
byte[] payload = new byte[buffer.remaining()];
buffer.get(payload);
UUID roomId = getMindmapId(sender);

sessionRegistry.broadcast(getMindmapId(sender), sender, payload);
byte[] payload = toByteArray(message.getPayload());

sessionRegistry.broadcast(roomId, sender, payload);

if (YjsProtocolUtil.isUpdateFrame(payload)) {
try {
redisStreamStore.appendUpdate(roomId, payload);
} catch (Exception e) {
log.error("Error while appending update frame to redis. roomId={}", roomId, e);
}
}
}

public void handleDisconnect(WebSocketSession session) {
//TODO: Collaboration room 세션 수 0일때 스냅샷 트리거
sessionRegistry.removeSession(getMindmapId(session), session);
}

private UUID getMindmapId(WebSocketSession session) {
return (UUID) session.getAttributes().get(AttributeKeys.MINDMAP_ID);
}

private byte[] toByteArray(ByteBuffer buffer) {
ByteBuffer dup = buffer.duplicate();
byte[] bytes = new byte[dup.remaining()];
dup.get(bytes);
return bytes;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.yat2.episode.collaboration;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Map;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class RedisStreamStore {

private final RedisTemplate<String, byte[]> redisBinaryTemplate;

private static final String FIELD_UPDATE = "u";

private String streamKey(UUID roomId) {
return "collab:room:" + roomId;
}

public RecordId appendUpdate(UUID roomId, byte[] update) {
String key = streamKey(roomId);

StreamOperations<String, String, byte[]> ops = redisBinaryTemplate.opsForStream();

MapRecord<String, String, byte[]> record =
StreamRecords.newRecord().in(key).ofMap(Map.of(FIELD_UPDATE, update));

RecordId id = ops.add(record);

redisBinaryTemplate.expire(key, Duration.ofDays(2));

return id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.yat2.episode.collaboration;

import lombok.experimental.UtilityClass;

@UtilityClass
public final class YjsProtocolUtil {
public static final int MSG_SYNC = 0;

/* 추후 sync 라우팅을 위함 */
public static final int SYNC_STEP_1 = 0;
public static final int SYNC_STEP_2 = 1;
public static final int SYNC_UPDATE = 2;

public static boolean isUpdateFrame(byte[] payload) {
if (payload == null || payload.length < 3) {
return false;
}
return payload[0] == MSG_SYNC && payload[1] == SYNC_UPDATE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.yat2.episode.global.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Slf4j
@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, byte[]> redisBinaryTemplate(
RedisConnectionFactory connectionFactory
) {
RedisTemplate<String, byte[]> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());

template.setValueSerializer(RedisSerializer.byteArray());
template.setHashValueSerializer(RedisSerializer.byteArray());

template.afterPropertiesSet();
return template;
}
}
6 changes: 6 additions & 0 deletions backend/api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ spring:
resources:
add-mappings: false

data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_PASSWORD}

server:
port: 8080
forward-headers-strategy: framework
Expand Down
13 changes: 12 additions & 1 deletion backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,18 @@ services:
volumes:
- minio-data:/data
command: server /data --console-address ":9001"

redis:
image: redis:7-alpine
container_name: episode-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: [ "redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}" ]

volumes:
mysql-data:
minio-data:

redis-data:
1 change: 1 addition & 0 deletions infra/prod/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ services:
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
Expand Down
28 changes: 27 additions & 1 deletion infra/stage/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ services:
networks:
- web
healthcheck:
test: [ "CMD", "wget", "-qO-", "http://localhost:8080/actuator/health" ]
test: [ "CMD", "wget", "-q", "--spider", "http://localhost:8080/actuator/health" ]
interval: 30s
timeout: 5s
retries: 3
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy

caddy:
image: caddy:2.10.2-alpine
Expand Down Expand Up @@ -61,6 +66,26 @@ services:
volumes:
- minio-data:/data
command: server /data --console-address ":9001"
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ]
interval: 30s
timeout: 20s
retries: 3
networks:
- web

redis:
image: redis:7-alpine
container_name: episode-redis
restart: unless-stopped
volumes:
- redis-data:/data
command: [ "redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}" ]
healthcheck:
test: [ "CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping" ]
interval: 10s
timeout: 5s
retries: 5
networks:
- web

Expand All @@ -73,3 +98,4 @@ volumes:
caddy_config:
mysql-data:
minio-data:
redis-data: