Skip to content

Commit a2256cd

Browse files
committed
SEBSP-210 add live caching
1 parent 1cf3cdf commit a2256cd

File tree

10 files changed

+394
-35
lines changed

10 files changed

+394
-35
lines changed

src/main/java/ch/ethz/seb/sps/server/ServiceUpdateTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private void init() {
5151
fixedDelayString = "${sps.webservice.distributed.update:15000}",
5252
initialDelay = 5000,
5353
scheduler = SYSTEM_SCHEDULER)
54-
private void examSessionUpdateTask() {
54+
private void sessionUpdateTask() {
5555
this.serviceInfo.updateMaster();
5656
}
5757

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2025 ETH Zürich, IT Services
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
package ch.ethz.seb.sps.server.datalayer.dao;
10+
11+
import java.util.Collection;
12+
import java.util.List;
13+
14+
import ch.ethz.seb.sps.server.datalayer.batis.model.ScreenshotDataLiveCacheRecord;
15+
import ch.ethz.seb.sps.utils.Result;
16+
17+
public interface ScreenshotDataLiveCacheDAO {
18+
19+
Result<ScreenshotDataLiveCacheRecord> createCacheEntry(String sessionUUID);
20+
21+
Result<String> deleteCacheEntry(String sessionUUID);
22+
23+
Result<List<String>> deleteAll(List<String> sessionUUIDs);
24+
25+
Result<Collection<ScreenshotDataLiveCacheRecord>> getAll();
26+
}
27+

src/main/java/ch/ethz/seb/sps/server/datalayer/dao/SessionDAO.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,10 @@ Result<Session> createNew(
135135
* @param termination_time The timestamp on which to close the session (unix timestamp im milliseconds)
136136
* @return Result refer to the close timestamp or to an error when happened.*/
137137
Result<Long> closeAt(String sessionUUID, Long termination_time);
138+
139+
/** Reduces the given list of sessionUUIDs to a list of session UUIDs of all closed sessions include in the given set
140+
*
141+
* @param sessionUUIDs the session UUIDs
142+
* @return a list of session UUIDs of all closed sessions include in the given set*/
143+
Result<List<String>> getAllClosedSessionsIn(Set<String> sessionUUIDs);
138144
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2025 ETH Zürich, IT Services
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
package ch.ethz.seb.sps.server.datalayer.dao.impl;
10+
11+
import java.util.Collection;
12+
import java.util.List;
13+
14+
import ch.ethz.seb.sps.server.datalayer.batis.model.ScreenshotDataLiveCacheRecord;
15+
import ch.ethz.seb.sps.server.datalayer.dao.ScreenshotDataLiveCacheDAO;
16+
import ch.ethz.seb.sps.utils.Result;
17+
import org.springframework.stereotype.Service;
18+
19+
@Service
20+
public class ScreenshotDataLiveCacheDAOBatis implements ScreenshotDataLiveCacheDAO {
21+
22+
@Override
23+
public Result<ScreenshotDataLiveCacheRecord> createCacheEntry(String sessionUUID) {
24+
return null;
25+
// return Result.tryCatch(() -> {
26+
// new ScreenshotDataLiveCacheRecord( null, sessionUUID, -1L );
27+
// });
28+
}
29+
30+
@Override
31+
public Result<String> deleteCacheEntry(String sessionUUID) {
32+
return null;
33+
}
34+
35+
@Override
36+
public Result<List<String>> deleteAll(List<String> sessionUUIDs) {
37+
return null;
38+
}
39+
40+
@Override
41+
public Result<Collection<ScreenshotDataLiveCacheRecord>> getAll() {
42+
return null;
43+
}
44+
}

src/main/java/ch/ethz/seb/sps/server/datalayer/dao/impl/SessionDAOBatis.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,25 @@ public Result<Long> closeAt(String sessionUUID, Long termination_time) {
632632
});
633633
}
634634

635+
@Override
636+
@Transactional(readOnly = true)
637+
public Result<List<String>> getAllClosedSessionsIn(Set<String> sessionUUIDs) {
638+
return Result.tryCatch(() -> {
639+
if (sessionUUIDs == null || sessionUUIDs.isEmpty()) {
640+
return Collections.emptyList();
641+
}
642+
643+
return sessionRecordMapper.selectByExample()
644+
.where(ExamRecordDynamicSqlSupport.terminationTime, SqlBuilder.isNotNull())
645+
.and( ExamRecordDynamicSqlSupport.uuid, SqlBuilder.isIn(sessionUUIDs))
646+
.build()
647+
.execute()
648+
.stream()
649+
.map(SessionRecord::getUuid)
650+
.toList();
651+
});
652+
}
653+
635654
@Override
636655
@Transactional(readOnly = true)
637656
public boolean isActive(String modelId) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) 2025 ETH Zürich, IT Services
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
package ch.ethz.seb.sps.server.servicelayer;
10+
11+
import java.util.Collection;
12+
13+
import ch.ethz.seb.sps.server.ServiceInitEvent;
14+
import ch.ethz.seb.sps.server.servicelayer.impl.ScreenshotQueueData;
15+
import ch.ethz.seb.sps.utils.Result;
16+
import org.springframework.context.event.EventListener;
17+
18+
public interface LiveProctoringCacheService {
19+
20+
@EventListener(ServiceInitEvent.class)
21+
void init();
22+
23+
/** Get the PK id of the last screenshot_data row for a given live session.
24+
*
25+
* @param sessionUUID The live session UUID
26+
* @return PK id of the last screenshot_data row if available or -1 if there is no screenshot yet or null if there is no slot for the given sessionUUID*/
27+
Long getLatestSSDataId(String sessionUUID);
28+
29+
/** Creates a new cache slot for given session. Usually called when session is created
30+
*
31+
* @param sessionUUID Session UUID to create a live cache slot for
32+
* @return Result refer to given sessionUUID or to an error when happened */
33+
Result<String> createCacheSlot(String sessionUUID);
34+
35+
/** Deletes a cache slot for a given session. Usually called when session is closed
36+
*
37+
* @param sessionUUID Session UUID to create a live cache slot for
38+
* @return Result refer to given sessionUUID or to an error when happened */
39+
Result<String> deleteCacheSlot(String sessionUUID);
40+
41+
/** Called by the batch store services to update latest cache entries on storage
42+
* @param batch The batch with the latest screenshot_data ids */
43+
void updateCacheStore(Collection<ScreenshotQueueData> batch);
44+
45+
/** Goes through all cache slots and deletes the one that has a closed session */
46+
void cleanup();
47+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright (c) 2025 ETH Zürich, IT Services
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
package ch.ethz.seb.sps.server.servicelayer.impl;
10+
11+
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
12+
13+
import java.time.Duration;
14+
import java.util.Collection;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
import java.util.stream.Collectors;
19+
20+
import ch.ethz.seb.sps.server.ServiceConfig;
21+
import ch.ethz.seb.sps.server.ServiceInit;
22+
import ch.ethz.seb.sps.server.datalayer.batis.custommappers.ScreenshotMapper;
23+
import ch.ethz.seb.sps.server.datalayer.batis.mapper.ScreenshotDataLiveCacheRecordDynamicSqlSupport;
24+
import ch.ethz.seb.sps.server.datalayer.batis.mapper.ScreenshotDataLiveCacheRecordMapper;
25+
import ch.ethz.seb.sps.server.datalayer.batis.mapper.ScreenshotDataRecordMapper;
26+
import ch.ethz.seb.sps.server.datalayer.batis.model.ScreenshotDataLiveCacheRecord;
27+
import ch.ethz.seb.sps.server.datalayer.dao.ScreenshotDataLiveCacheDAO;
28+
import ch.ethz.seb.sps.server.datalayer.dao.SessionDAO;
29+
import ch.ethz.seb.sps.server.servicelayer.LiveProctoringCacheService;
30+
import ch.ethz.seb.sps.utils.Result;
31+
import org.apache.ibatis.binding.MapperRegistry;
32+
import org.apache.ibatis.session.ExecutorType;
33+
import org.apache.ibatis.session.SqlSessionFactory;
34+
import org.joda.time.DateTime;
35+
import org.joda.time.DateTimeZone;
36+
import org.mybatis.dynamic.sql.update.UpdateDSL;
37+
import org.mybatis.spring.SqlSessionTemplate;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
40+
import org.springframework.beans.factory.annotation.Qualifier;
41+
import org.springframework.beans.factory.annotation.Value;
42+
import org.springframework.context.annotation.Lazy;
43+
import org.springframework.scheduling.TaskScheduler;
44+
import org.springframework.stereotype.Component;
45+
import org.springframework.transaction.PlatformTransactionManager;
46+
import org.springframework.transaction.TransactionDefinition;
47+
import org.springframework.transaction.TransactionException;
48+
import org.springframework.transaction.support.TransactionTemplate;
49+
50+
@Lazy
51+
@Component
52+
public class LiveProctoringCacheServiceImpl implements LiveProctoringCacheService {
53+
54+
private static final Logger log = LoggerFactory.getLogger(LiveProctoringCacheServiceImpl.class);
55+
public static final Logger INIT_LOGGER = LoggerFactory.getLogger("SERVICE_INIT");
56+
57+
private final TaskScheduler taskScheduler;
58+
private final long batchInterval;
59+
60+
private final SqlSessionFactory sqlSessionFactory;
61+
private final TransactionTemplate transactionTemplate;
62+
private final ScreenshotDataLiveCacheDAO screenshotDataLiveCacheDAO;
63+
private final SessionDAO sessionDAO;
64+
65+
private SqlSessionTemplate sqlSessionTemplate;
66+
private ScreenshotDataLiveCacheRecordMapper screenshotDataLiveCacheRecordMapper;
67+
68+
69+
private final Map<String, Long> cache = new ConcurrentHashMap<>();
70+
71+
public LiveProctoringCacheServiceImpl(
72+
final SqlSessionFactory sqlSessionFactory,
73+
final PlatformTransactionManager transactionManager,
74+
final ScreenshotDataLiveCacheDAO screenshotDataLiveCacheDAO,
75+
final SessionDAO sessionDAO,
76+
@Qualifier(value = ServiceConfig.SCREENSHOT_STORE_API_EXECUTOR) final TaskScheduler taskScheduler,
77+
@Value("${sps.data.store.batch.interval:1000}") final long batchInterval) {
78+
79+
this.sqlSessionFactory = sqlSessionFactory;
80+
this.transactionTemplate = new TransactionTemplate(transactionManager);
81+
this.screenshotDataLiveCacheDAO = screenshotDataLiveCacheDAO;
82+
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
83+
this.sessionDAO = sessionDAO;
84+
this.taskScheduler = taskScheduler;
85+
this.batchInterval = batchInterval;
86+
}
87+
88+
89+
@Override
90+
public void init() {
91+
92+
INIT_LOGGER.info("---->");
93+
INIT_LOGGER.info("----> Initialize Live Proctoring Cache Service");
94+
INIT_LOGGER.info("---->");
95+
96+
try {
97+
this.sqlSessionTemplate = new SqlSessionTemplate(this.sqlSessionFactory, ExecutorType.BATCH);
98+
99+
final MapperRegistry mapperRegistry = this.sqlSessionTemplate.getConfiguration().getMapperRegistry();
100+
final Collection<Class<?>> mappers = mapperRegistry.getMappers();
101+
if (!mappers.contains(ScreenshotMapper.class)) {
102+
mapperRegistry.addMapper(ScreenshotMapper.class);
103+
}
104+
if (!mappers.contains(ScreenshotDataRecordMapper.class)) {
105+
mapperRegistry.addMapper(ScreenshotDataRecordMapper.class);
106+
}
107+
this.screenshotDataLiveCacheRecordMapper = this.sqlSessionTemplate.getMapper(ScreenshotDataLiveCacheRecordMapper.class);
108+
109+
this.taskScheduler.scheduleWithFixedDelay(
110+
this::updateCache,
111+
DateTime.now(DateTimeZone.UTC).toDate().toInstant(),
112+
Duration.ofMillis(this.batchInterval));
113+
114+
115+
} catch (Exception e) {
116+
ServiceInit.INIT_LOGGER.error("----> Live Proctoring Cache Service : failed to initialized", e);
117+
throw e;
118+
}
119+
}
120+
121+
@Override
122+
public Long getLatestSSDataId(final String sessionUUID) {
123+
return cache.get(sessionUUID);
124+
}
125+
126+
@Override
127+
public Result<String> createCacheSlot(final String sessionUUID) {
128+
return screenshotDataLiveCacheDAO.createCacheEntry(sessionUUID);
129+
}
130+
131+
@Override
132+
public Result<String> deleteCacheSlot(final String sessionUUID) {
133+
return screenshotDataLiveCacheDAO.deleteCacheEntry(sessionUUID);
134+
}
135+
136+
@Override
137+
public void updateCacheStore(final Collection<ScreenshotQueueData> batch) {
138+
try {
139+
140+
this.transactionTemplate.executeWithoutResult(status -> {
141+
142+
batch.forEach(data -> {
143+
if (data.record.getId() != null) {
144+
UpdateDSL.updateWithMapper(
145+
screenshotDataLiveCacheRecordMapper::update,
146+
ScreenshotDataLiveCacheRecordDynamicSqlSupport.screenshotDataLiveCacheRecord)
147+
.set(ScreenshotDataLiveCacheRecordDynamicSqlSupport.idLatestSsd).equalTo(data.record.getId())
148+
.where(ScreenshotDataLiveCacheRecordDynamicSqlSupport.sessionUuid, isEqualTo(data.record.getSessionUuid()))
149+
.build()
150+
.execute();
151+
}
152+
});
153+
this.sqlSessionTemplate.flushStatements();
154+
155+
});
156+
157+
} catch (final TransactionException te) {
158+
log.error("Failed to batch update screenshot data live cache store. Transaction has failed. Cause: {}", te.getMessage());
159+
}
160+
}
161+
162+
@Override
163+
public void cleanup() {
164+
try {
165+
166+
List<String> closedSession = sessionDAO
167+
.getAllClosedSessionsIn(cache.keySet())
168+
.getOrThrow();
169+
170+
screenshotDataLiveCacheDAO
171+
.deleteAll(closedSession)
172+
.getOrThrow();
173+
174+
} catch (Exception e) {
175+
log.error("Failed to cleanup cache: ", e);
176+
}
177+
}
178+
179+
private void updateCache() {
180+
try {
181+
182+
Map<String, Long> newValues = screenshotDataLiveCacheDAO
183+
.getAll()
184+
.getOrThrow()
185+
.stream()
186+
.collect(Collectors.toMap(
187+
ScreenshotDataLiveCacheRecord::getSessionUuid,
188+
ScreenshotDataLiveCacheRecord::getIdLatestSsd
189+
));
190+
191+
cache.putAll(newValues);
192+
193+
} catch (Exception e) {
194+
log.error("Failed to update cache: ", e);
195+
}
196+
}
197+
}

src/main/java/ch/ethz/seb/sps/server/servicelayer/impl/ScreenshotQueueData.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import ch.ethz.seb.sps.domain.model.service.Session.ImageFormat;
1414
import ch.ethz.seb.sps.server.datalayer.batis.model.ScreenshotDataRecord;
1515

16-
final class ScreenshotQueueData {
16+
public final class ScreenshotQueueData {
1717

1818
final ScreenshotDataRecord record;
1919
final ByteArrayInputStream screenshotIn;

0 commit comments

Comments
 (0)