Track chat session lifecycles: added session start/end times, database schema, and persistence logic.
This commit is contained in:
parent
b1267068a5
commit
3462d82716
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user