|
111 | 111 | import java.util.Date; |
112 | 112 | import java.util.Enumeration; |
113 | 113 | import java.util.HashMap; |
| 114 | +import java.util.HashSet; |
| 115 | +import java.util.Set; |
114 | 116 | import java.util.List; |
115 | 117 | import java.util.Locale; |
116 | 118 | import java.util.Map; |
@@ -1037,6 +1039,261 @@ public Object execute(Object o, BindException errors) |
1037 | 1039 | } |
1038 | 1040 | } |
1039 | 1041 |
|
| 1042 | + @RequiresSiteAdmin |
| 1043 | + public static class RetrainAllAction extends MutatingApiAction<Object> |
| 1044 | + { |
| 1045 | + @Override |
| 1046 | + public Object execute(Object o, BindException errors) |
| 1047 | + { |
| 1048 | + var req = getViewContext().getRequest(); |
| 1049 | + String mode = req.getParameter("mode"); |
| 1050 | + if (mode == null || mode.isEmpty()) |
| 1051 | + mode = "reset"; |
| 1052 | + boolean incremental = mode.equalsIgnoreCase("incremental"); |
| 1053 | + |
| 1054 | + int maxRuns = 20; |
| 1055 | + String maxRunsParam = req.getParameter("maxRuns"); |
| 1056 | + if (maxRunsParam == null || maxRunsParam.isEmpty()) |
| 1057 | + maxRunsParam = req.getParameter("targetRuns"); // backwards compatibility |
| 1058 | + if (maxRunsParam != null && !maxRunsParam.isEmpty()) |
| 1059 | + { |
| 1060 | + try { maxRuns = Integer.parseInt(maxRunsParam); } |
| 1061 | + catch (NumberFormatException ignored) { } |
| 1062 | + } |
| 1063 | + if (maxRuns < 1) maxRuns = 1; |
| 1064 | + if (maxRuns > 100) maxRuns = 100; |
| 1065 | + |
| 1066 | + int minRuns = 5; |
| 1067 | + String minRunsParam = req.getParameter("minRuns"); |
| 1068 | + if (minRunsParam != null && !minRunsParam.isEmpty()) |
| 1069 | + { |
| 1070 | + try { minRuns = Integer.parseInt(minRunsParam); } |
| 1071 | + catch (NumberFormatException ignored) { } |
| 1072 | + } |
| 1073 | + if (minRuns < 1) minRuns = 1; |
| 1074 | + if (minRuns > maxRuns) minRuns = maxRuns; |
| 1075 | + |
| 1076 | + Container c = getContainer(); |
| 1077 | + String containerPath = c.getPath(); |
| 1078 | + int expectedDuration = containerPath.toLowerCase().contains("perf") ? 720 : 540; |
| 1079 | + |
| 1080 | + // Only look back 1.5x maxRuns days to avoid ancient data |
| 1081 | + int lookbackDays = (int) Math.ceil(maxRuns * 1.5); |
| 1082 | + java.sql.Timestamp cutoffDate = new java.sql.Timestamp( |
| 1083 | + System.currentTimeMillis() - (long) lookbackDays * 24 * 60 * 60 * 1000); |
| 1084 | + |
| 1085 | + DbScope scope = TestResultsSchema.getSchema().getScope(); |
| 1086 | + try (DbScope.Transaction transaction = scope.ensureTransaction()) |
| 1087 | + { |
| 1088 | + // Get unique user IDs from recent testruns for this container |
| 1089 | + SQLFragment allUsersSql = new SQLFragment(); |
| 1090 | + allUsersSql.append( |
| 1091 | + "SELECT DISTINCT userid FROM " + TestResultsSchema.getTableInfoTestRuns() + |
| 1092 | + " WHERE container = ? AND posttime >= ?"); |
| 1093 | + allUsersSql.add(c.getEntityId()); |
| 1094 | + allUsersSql.add(cutoffDate); |
| 1095 | + List<Integer> allUserIds = new ArrayList<>(); |
| 1096 | + new SqlSelector(scope, allUsersSql).forEach(rs -> |
| 1097 | + allUserIds.add(rs.getInt("userid"))); |
| 1098 | + |
| 1099 | + // Get current trainrun counts per user |
| 1100 | + Map<Integer, Integer> userTrainCounts = new HashMap<>(); |
| 1101 | + for (int userId : allUserIds) |
| 1102 | + userTrainCounts.put(userId, 0); |
| 1103 | + |
| 1104 | + if (incremental) |
| 1105 | + { |
| 1106 | + // In incremental mode, get existing counts |
| 1107 | + SQLFragment countsSql = new SQLFragment(); |
| 1108 | + countsSql.append( |
| 1109 | + "SELECT r.userid, COUNT(tr.runid) as traincount" + |
| 1110 | + " FROM " + TestResultsSchema.getTableInfoTrain() + " tr" + |
| 1111 | + " JOIN " + TestResultsSchema.getTableInfoTestRuns() + " r ON tr.runid = r.id" + |
| 1112 | + " WHERE r.container = ?" + |
| 1113 | + " GROUP BY r.userid"); |
| 1114 | + countsSql.add(c.getEntityId()); |
| 1115 | + new SqlSelector(scope, countsSql).forEach(rs -> |
| 1116 | + userTrainCounts.put(rs.getInt("userid"), rs.getInt("traincount"))); |
| 1117 | + } |
| 1118 | + else |
| 1119 | + { |
| 1120 | + // Reset mode: delete all trainruns for this container |
| 1121 | + SQLFragment deleteTrainSql = new SQLFragment(); |
| 1122 | + deleteTrainSql.append( |
| 1123 | + "DELETE FROM " + TestResultsSchema.getTableInfoTrain() + |
| 1124 | + " WHERE runid IN (SELECT id FROM " + TestResultsSchema.getTableInfoTestRuns() + |
| 1125 | + " WHERE container = ?)"); |
| 1126 | + deleteTrainSql.add(c.getEntityId()); |
| 1127 | + new SqlExecutor(scope).execute(deleteTrainSql); |
| 1128 | + |
| 1129 | + // Delete all userdata for this container |
| 1130 | + SQLFragment deleteUserDataSql = new SQLFragment(); |
| 1131 | + deleteUserDataSql.append( |
| 1132 | + "DELETE FROM " + TestResultsSchema.getTableInfoUserData() + |
| 1133 | + " WHERE container = ?"); |
| 1134 | + deleteUserDataSql.add(c.getEntityId()); |
| 1135 | + new SqlExecutor(scope).execute(deleteUserDataSql); |
| 1136 | + } |
| 1137 | + |
| 1138 | + int usersRetrained = 0; |
| 1139 | + int totalTrainRuns = 0; |
| 1140 | + final int minRunsRequired = minRuns; |
| 1141 | + |
| 1142 | + for (int userId : allUserIds) |
| 1143 | + { |
| 1144 | + // Get existing trainrun IDs for this user |
| 1145 | + SQLFragment existingRunsSql = new SQLFragment(); |
| 1146 | + existingRunsSql.append( |
| 1147 | + "SELECT tr.runid FROM " + TestResultsSchema.getTableInfoTrain() + " tr" + |
| 1148 | + " JOIN " + TestResultsSchema.getTableInfoTestRuns() + " r ON tr.runid = r.id" + |
| 1149 | + " WHERE r.userid = ? AND r.container = ?"); |
| 1150 | + existingRunsSql.add(userId); |
| 1151 | + existingRunsSql.add(c.getEntityId()); |
| 1152 | + Set<Integer> existingRunIds = new HashSet<>(); |
| 1153 | + new SqlSelector(scope, existingRunsSql).forEach(rs -> |
| 1154 | + existingRunIds.add(rs.getInt("runid"))); |
| 1155 | + |
| 1156 | + // Find all clean runs within lookback period (for both modes) |
| 1157 | + SQLFragment cleanRunsSql = new SQLFragment(); |
| 1158 | + cleanRunsSql.append( |
| 1159 | + "SELECT tr.id FROM " + TestResultsSchema.getTableInfoTestRuns() + " tr" + |
| 1160 | + " WHERE tr.userid = ? AND tr.container = ?" + |
| 1161 | + " AND tr.posttime >= ?" + |
| 1162 | + " AND tr.failedtests = 0 AND tr.leakedtests = 0" + |
| 1163 | + " AND tr.passedtests > 0 AND tr.flagged = false" + |
| 1164 | + " AND tr.duration >= ?" + |
| 1165 | + " AND NOT EXISTS (SELECT 1 FROM " + TestResultsSchema.getTableInfoHangs() + |
| 1166 | + " h WHERE h.testrunid = tr.id)" + |
| 1167 | + " ORDER BY tr.posttime DESC"); |
| 1168 | + cleanRunsSql.add(userId); |
| 1169 | + cleanRunsSql.add(c.getEntityId()); |
| 1170 | + cleanRunsSql.add(cutoffDate); |
| 1171 | + cleanRunsSql.add(expectedDuration); |
| 1172 | + |
| 1173 | + List<Integer> recentCleanRunIds = new ArrayList<>(); |
| 1174 | + new SqlSelector(scope, cleanRunsSql).forEach(rs -> |
| 1175 | + recentCleanRunIds.add(rs.getInt("id"))); |
| 1176 | + |
| 1177 | + // Determine final set of trainrun IDs |
| 1178 | + final List<Integer> finalRunIds = new ArrayList<>(); |
| 1179 | + final int maxRunsLimit = maxRuns; |
| 1180 | + if (incremental && !existingRunIds.isEmpty()) |
| 1181 | + { |
| 1182 | + // Incremental: combine existing + new recent clean runs, keep most recent maxRuns |
| 1183 | + // Get all existing trainruns with posttimes for sorting |
| 1184 | + SQLFragment allCandidatesSql = new SQLFragment(); |
| 1185 | + allCandidatesSql.append( |
| 1186 | + "SELECT id, posttime FROM " + TestResultsSchema.getTableInfoTestRuns() + |
| 1187 | + " WHERE userid = ? AND container = ?" + |
| 1188 | + " AND (id = ANY(?) OR id = ANY(?))" + |
| 1189 | + " ORDER BY posttime DESC"); |
| 1190 | + allCandidatesSql.add(userId); |
| 1191 | + allCandidatesSql.add(c.getEntityId()); |
| 1192 | + allCandidatesSql.add(existingRunIds.toArray(new Integer[0])); |
| 1193 | + allCandidatesSql.add(recentCleanRunIds.toArray(new Integer[0])); |
| 1194 | + |
| 1195 | + new SqlSelector(scope, allCandidatesSql).forEach(rs -> { |
| 1196 | + if (finalRunIds.size() < maxRunsLimit) |
| 1197 | + finalRunIds.add(rs.getInt("id")); |
| 1198 | + }); |
| 1199 | + } |
| 1200 | + else |
| 1201 | + { |
| 1202 | + // Reset mode: just use recent clean runs up to maxRuns |
| 1203 | + if (recentCleanRunIds.size() > maxRuns) |
| 1204 | + finalRunIds.addAll(recentCleanRunIds.subList(0, maxRuns)); |
| 1205 | + else |
| 1206 | + finalRunIds.addAll(recentCleanRunIds); |
| 1207 | + } |
| 1208 | + |
| 1209 | + // Skip if total runs is below minimum threshold |
| 1210 | + if (finalRunIds.size() < minRunsRequired) |
| 1211 | + continue; |
| 1212 | + |
| 1213 | + // Determine runs to add and remove |
| 1214 | + Set<Integer> finalRunSet = new HashSet<>(finalRunIds); |
| 1215 | + List<Integer> runsToAdd = new ArrayList<>(); |
| 1216 | + List<Integer> runsToRemove = new ArrayList<>(); |
| 1217 | + |
| 1218 | + for (int runId : finalRunIds) |
| 1219 | + { |
| 1220 | + if (!existingRunIds.contains(runId)) |
| 1221 | + runsToAdd.add(runId); |
| 1222 | + } |
| 1223 | + for (int runId : existingRunIds) |
| 1224 | + { |
| 1225 | + if (!finalRunSet.contains(runId)) |
| 1226 | + runsToRemove.add(runId); |
| 1227 | + } |
| 1228 | + |
| 1229 | + // Remove old trainruns |
| 1230 | + for (int runId : runsToRemove) |
| 1231 | + { |
| 1232 | + SQLFragment deleteTrainSql = new SQLFragment(); |
| 1233 | + deleteTrainSql.append( |
| 1234 | + "DELETE FROM " + TestResultsSchema.getTableInfoTrain() + " WHERE runid = ?"); |
| 1235 | + deleteTrainSql.add(runId); |
| 1236 | + new SqlExecutor(scope).execute(deleteTrainSql); |
| 1237 | + } |
| 1238 | + |
| 1239 | + // Insert new trainruns |
| 1240 | + for (int runId : runsToAdd) |
| 1241 | + { |
| 1242 | + SQLFragment insertTrainSql = new SQLFragment(); |
| 1243 | + insertTrainSql.append( |
| 1244 | + "INSERT INTO " + TestResultsSchema.getTableInfoTrain() + " (runid) VALUES (?)"); |
| 1245 | + insertTrainSql.add(runId); |
| 1246 | + new SqlExecutor(scope).execute(insertTrainSql); |
| 1247 | + } |
| 1248 | + |
| 1249 | + // Set active=true only if user has at least maxRuns |
| 1250 | + boolean isActive = finalRunIds.size() >= maxRuns; |
| 1251 | + |
| 1252 | + // Calculate stats and upsert into userdata |
| 1253 | + SQLFragment upsertStatsSql = new SQLFragment(); |
| 1254 | + upsertStatsSql.append( |
| 1255 | + "INSERT INTO " + TestResultsSchema.getTableInfoUserData() + |
| 1256 | + " (userid, container, meantestsrun, meanmemory, stddevtestsrun, stddevmemory, active)" + |
| 1257 | + " SELECT ?, ?, avg(passedtests), avg(averagemem)," + |
| 1258 | + " stddev_pop(passedtests), stddev_pop(averagemem), ?" + |
| 1259 | + " FROM " + TestResultsSchema.getTableInfoTestRuns() + |
| 1260 | + " WHERE id = ANY(?)" + |
| 1261 | + " ON CONFLICT(userid, container) DO UPDATE SET" + |
| 1262 | + " meantestsrun = excluded.meantestsrun," + |
| 1263 | + " meanmemory = excluded.meanmemory," + |
| 1264 | + " stddevtestsrun = excluded.stddevtestsrun," + |
| 1265 | + " stddevmemory = excluded.stddevmemory," + |
| 1266 | + " active = excluded.active"); |
| 1267 | + upsertStatsSql.add(userId); |
| 1268 | + upsertStatsSql.add(c.getEntityId()); |
| 1269 | + upsertStatsSql.add(isActive); |
| 1270 | + upsertStatsSql.add(finalRunIds.toArray(new Integer[0])); |
| 1271 | + new SqlExecutor(scope).execute(upsertStatsSql); |
| 1272 | + |
| 1273 | + usersRetrained++; |
| 1274 | + totalTrainRuns += runsToAdd.size(); |
| 1275 | + } |
| 1276 | + |
| 1277 | + transaction.commit(); |
| 1278 | + |
| 1279 | + ApiSimpleResponse response = new ApiSimpleResponse(); |
| 1280 | + response.put("success", true); |
| 1281 | + response.put("usersRetrained", usersRetrained); |
| 1282 | + response.put("totalTrainRuns", totalTrainRuns); |
| 1283 | + response.put("mode", mode); |
| 1284 | + return response; |
| 1285 | + } |
| 1286 | + catch (Exception e) |
| 1287 | + { |
| 1288 | + _log.error("Error in RetrainAllAction", e); |
| 1289 | + ApiSimpleResponse response = new ApiSimpleResponse(); |
| 1290 | + response.put("success", false); |
| 1291 | + response.put("error", e.getMessage()); |
| 1292 | + return response; |
| 1293 | + } |
| 1294 | + } |
| 1295 | + } |
| 1296 | + |
1040 | 1297 | /** |
1041 | 1298 | * action for posting test output as an xml file |
1042 | 1299 | */ |
|
0 commit comments