Track chat session lifecycles: added session start/end times, database schema, and persistence logic.

This commit is contained in:
akastijn 2026-08-01 17:40:48 +02:00
parent b1267068a5
commit 3462d82716
4 changed files with 95 additions and 11 deletions

View File

@ -1,5 +1,10 @@
package com.alttd.altitudeweb.services.chat.event_publisher; package com.alttd.altitudeweb.services.chat.event_publisher;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSession;
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSessionMapper;
import com.alttd.altitudeweb.setup.Connection;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
@ -7,6 +12,8 @@ import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -27,19 +34,10 @@ public class EventPublisher {
emitterMap.computeIfAbsent(eventUser.uuid(), k -> new CopyOnWriteArrayList<>()).add(emitter); emitterMap.computeIfAbsent(eventUser.uuid(), k -> new CopyOnWriteArrayList<>()).add(emitter);
eventUserMap.put(eventUser.uuid(), eventUser); eventUserMap.put(eventUser.uuid(), eventUser);
Instant sessionStart = Instant.now();
log.info("User {} subscribed, active emitters: {}", eventUser.uuid(), countActive()); log.info("User {} subscribed, active emitters: {}", eventUser.uuid(), countActive());
Runnable cleanup = () -> { Runnable cleanup = () -> handleCleanup(eventUser, emitter, sessionStart);
List<SseEmitter> userEmitters = emitterMap.get(eventUser.uuid());
if (userEmitters != null) {
userEmitters.remove(emitter);
if (userEmitters.isEmpty()) {
emitterMap.remove(eventUser.uuid());
eventUserMap.remove(eventUser.uuid());
}
}
log.info("Cleaned up emitter for {}, active emitters: {}", eventUser.uuid(), countActive());
};
// covers: clean disconnect (tab closed normally), server-side timeout, write failure // covers: clean disconnect (tab closed normally), server-side timeout, write failure
emitter.onCompletion(cleanup); emitter.onCompletion(cleanup);
@ -100,4 +98,39 @@ public class EventPublisher {
sendToUser(emitter.uuid(), eventName, json); sendToUser(emitter.uuid(), eventName, json);
}); });
} }
private void handleCleanup(EventUser eventUser, SseEmitter emitter, Instant sessionStart) {
List<SseEmitter> userEmitters = emitterMap.get(eventUser.uuid());
if (userEmitters != null) {
userEmitters.remove(emitter);
if (userEmitters.isEmpty()) {
emitterMap.remove(eventUser.uuid());
eventUserMap.remove(eventUser.uuid());
}
}
Instant sessionEnd = Instant.now();
Duration between = Duration.between(sessionStart, sessionEnd);
log.info("Cleaned up emitter for {}, active emitters: {}, session duration: {}",
eventUser.uuid(), countActive(), between
);
ChatSession chatSession = ChatSession.builder()
.uuid(eventUser.uuid())
.session_start(sessionStart)
.session_end(sessionEnd)
.build();
saveSession(chatSession);
}
private void saveSession(ChatSession chatSession) {
Connection.getConnection(Databases.DEFAULT)
.runQuery(sqlSession -> {
log.debug("Saving chat session");
try {
sqlSession.getMapper(ChatSessionMapper.class).storeSession(chatSession);
log.debug("Saved chat session");
} catch (Exception e) {
log.error("Failed to save chat session", e);
}
});
}
} }

View File

@ -0,0 +1,18 @@
package com.alttd.altitudeweb.database.web_db.chat_session;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.UUID;
@Getter
@Builder
public class ChatSession {
private final UUID uuid;
private final Instant session_start;
private final Instant session_end;
}

View File

@ -0,0 +1,13 @@
package com.alttd.altitudeweb.database.web_db.chat_session;
import org.apache.ibatis.annotations.Insert;
public interface ChatSessionMapper {
@Insert("""
INSERT INTO chat_session (uuid, session_start, session_end)
VALUES (#{uuid}, #{session_start}, #{session_end})
""")
void storeSession(ChatSession chatSession);
}

View File

@ -4,6 +4,7 @@ import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.web_db.KeyPairMapper; import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper; import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
import com.alttd.altitudeweb.database.web_db.SettingsMapper; import com.alttd.altitudeweb.database.web_db.SettingsMapper;
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSessionMapper;
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper; import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper; import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper;
import com.alttd.altitudeweb.database.web_db.forms.StaffApplicationMapper; import com.alttd.altitudeweb.database.web_db.forms.StaffApplicationMapper;
@ -28,6 +29,7 @@ public class InitializeWebDb {
configuration.addMapper(DiscordAppealMapper.class); configuration.addMapper(DiscordAppealMapper.class);
configuration.addMapper(StaffApplicationMapper.class); configuration.addMapper(StaffApplicationMapper.class);
configuration.addMapper(EmailVerificationMapper.class); configuration.addMapper(EmailVerificationMapper.class);
configuration.addMapper(ChatSessionMapper.class);
}).join() }).join()
.runQuery(sqlSession -> { .runQuery(sqlSession -> {
createSettingsTable(sqlSession); createSettingsTable(sqlSession);
@ -38,6 +40,7 @@ public class InitializeWebDb {
createdDiscordAppealTable(sqlSession); createdDiscordAppealTable(sqlSession);
createStaffApplicationsTable(sqlSession); createStaffApplicationsTable(sqlSession);
createUserEmailsTable(sqlSession); createUserEmailsTable(sqlSession);
createChatSessionTable(sqlSession);
}); });
log.debug("Initialized WebDb"); log.debug("Initialized WebDb");
} }
@ -208,4 +211,21 @@ public class InitializeWebDb {
} }
} }
private static void createChatSessionTable(@NotNull SqlSession sqlSession) {
String query = """
CREATE TABLE IF NOT EXISTS chat_session (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
uuid UUID NOT NULL,
session_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
session_end TIMESTAMP NULL,
FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE
);
""";
try (Statement statement = sqlSession.getConnection().createStatement()) {
statement.execute(query);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
} }