Browse Source

clusters

master
gsd 6 months ago
parent
commit
96a54e30f5
  1. 154
      src/main/java/app/annotations/impl/ClusterMethodAspect.java
  2. 15
      src/main/java/app/annotations/interfaces/ClusterMethod.java
  3. 3
      src/main/java/app/controllers/other/DockerController.java
  4. 13
      src/main/java/app/controllers/other/ExternalVIPController.java
  5. 13
      src/main/java/app/controllers/other/PulseController.java
  6. 12
      src/main/java/app/controllers/server/ServerUpdaterController.java
  7. 7
      src/main/java/app/controllers/user/ProfileController.java
  8. 3
      src/main/java/app/entities/Stats.java
  9. 2
      src/main/java/app/entities/VipGiveMethod.java
  10. 10
      src/main/java/app/services/StatsService.java
  11. 8
      src/main/java/app/services/db/ReportService.java
  12. 2
      src/main/java/app/updates/BanCountUpdater.java
  13. 11
      src/main/java/app/updates/BaseUpdater.java
  14. 2
      src/main/java/app/updates/CountriesUpdater.java
  15. 4
      src/main/java/app/updates/OnlineUpdater.java
  16. 4
      src/main/java/app/updates/PlayersUpdater.java
  17. 2
      src/main/java/app/updates/PreviewUpdater.java
  18. 4
      src/main/java/app/updates/SocialUpdater.java
  19. 10
      src/main/java/app/updates/UniqueUpdater.java
  20. 6
      src/main/java/app/updates/VipCountUpdater.java

154
src/main/java/app/annotations/impl/ClusterMethodAspect.java

@ -0,0 +1,154 @@
package app.annotations.impl;
import app.updates.BaseUpdater;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
@Aspect
@Configuration
public class ClusterMethodAspect extends BaseUpdater {
@Autowired
private HttpServletRequest httpServletRequest;
private final RestTemplate restTemplate;
private final String[] clusters;
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
public ClusterMethodAspect(Environment environment) {
this.restTemplate = new RestTemplate();
this.clusters = environment.getProperty("CLUSTERS","").isEmpty() ? new String[0] : environment.getProperty("CLUSTERS","").split(",");
if (this.clusters.length > 0) {
for (int i = 0; i < this.clusters.length; i++) {
this.logger.info("[Clusters init] found {} host", this.clusters[i]);
}
} else {
this.logger.warn("[Clusters init] not found clusters");
}
}
@After("@annotation(app.annotations.interfaces.ClusterMethod)")
public void after() {
try {
this.sendData(httpServletRequest);
} catch (Exception e) {
logger.error("Cannot create data to send to other clusters", e);
}
}
private void sendData(HttpServletRequest request) {
if (clusters.length > 0) {
ExecutorService executor = Executors.newFixedThreadPool(clusters.length);
Set<Callable<Supplier>> callables = new HashSet<>(clusters.length);
ClusterRequest clusterRequest = new ClusterRequest(request);
for(int i = 0; i < clusters.length; i++) {
int finalI = i;
callables.add(() -> {
try {
restTemplate.exchange("http://" + clusters[finalI] + clusterRequest.getUrl(), clusterRequest.getHttpMethod(), clusterRequest.getHttpEntity(clusters[finalI]), byte[].class);
} catch (Exception e) {
logger.error("Cannot send info to cluster {}", clusters[finalI], e);
}
return null;
});
}
try {
executor.invokeAll(callables);
} catch (InterruptedException ie) {}
finally {
executor.shutdown();
}
}
}
@PostConstruct
private void createClustersChecker() {
for(int i = 0; i < clusters.length; i++) {
int finalI = i;
this.logger.info("Create cluster pulse check on {}", clusters[finalI]);
CreateTaskUpdater(() -> checkCluster(clusters[finalI]), 60 * 1000, getClass().getName());
}
}
private boolean checkCluster(String address) {
try {
restTemplate.getForEntity("http://" + address + "/api/pulse/db", Long.class).getBody();
return true;
} catch (Exception err) {
this.logger.error("Cannot pulse {}", address);
return false;
}
}
class ClusterRequest {
private final String url;
private final HttpHeaders httpHeaders;
private Object body;
private final HttpMethod httpMethod;
public ClusterRequest(HttpServletRequest httpServletRequest) {
StringBuilder url = new StringBuilder();
url.append(httpServletRequest.getRequestURI());
if (httpServletRequest.getQueryString() != null) {
url.append("?");
url.append(httpServletRequest.getQueryString());
}
this.url = url.toString();
httpHeaders = new HttpHeaders();
httpServletRequest.getHeaderNames().asIterator().forEachRemaining(
(headerName) -> httpHeaders.set(headerName, httpServletRequest.getHeader(headerName))
);
try (ObjectInputStream is = new ObjectInputStream(httpServletRequest.getInputStream())) {
body = is.readObject();
} catch (Exception e) {}
httpMethod = HttpMethod.valueOf(httpServletRequest.getMethod());
}
public String getUrl() {
return url;
}
public Object getBody() {
return body;
}
public HttpMethod getHttpMethod() {
return httpMethod;
}
public HttpEntity getHttpEntity(String cluster) {
httpHeaders.set("F13-TO-CLUSTER", cluster);
return new HttpEntity(body, httpHeaders);
}
}
}

15
src/main/java/app/annotations/interfaces/ClusterMethod.java

@ -0,0 +1,15 @@
package app.annotations.interfaces;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* штука для дублирования данных на другой аналогичный сервис
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClusterMethod {
}

3
src/main/java/app/controllers/other/DockerController.java

@ -2,6 +2,7 @@ package app.controllers.other;
import app.annotations.enums.AuthMethod;
import app.annotations.interfaces.CheckWebAccess;
import app.annotations.interfaces.ClusterMethod;
import app.entities.Stats;
import app.entities.server.DockerStats;
import jakarta.servlet.http.HttpServletRequest;
@ -37,6 +38,7 @@ public class DockerController {
@PostMapping("/{srv}")
@CheckWebAccess(auth_method = AuthMethod.SECRET_KEY)
@ClusterMethod
public ResponseEntity setStats(HttpServletRequest httpServletRequest, @PathVariable String srv, @RequestBody DockerStats dockerStats) {
if (!stats.getServers().containsKey(srv)) return new ResponseEntity<>(HttpStatus.NOT_FOUND);
stats.getServers().get(srv).setDockerStats(dockerStats);
@ -45,6 +47,7 @@ public class DockerController {
@DeleteMapping("/{srv}")
@CheckWebAccess(auth_method = AuthMethod.SECRET_KEY)
@ClusterMethod
public ResponseEntity setStats(HttpServletRequest httpServletRequest, @PathVariable String srv) {
if (!stats.getServers().containsKey(srv)) return new ResponseEntity<>(HttpStatus.NOT_FOUND);
DockerStats clear = new DockerStats();

13
src/main/java/app/controllers/other/ExternalVIPController.java

@ -4,6 +4,7 @@ import app.annotations.enums.AuthMethod;
import app.annotations.enums.CollectStages;
import app.annotations.interfaces.CheckPermitionFlag;
import app.annotations.interfaces.CheckWebAccess;
import app.annotations.interfaces.ClusterMethod;
import app.annotations.interfaces.CollectStatistic;
import app.entities.VipGiveMethod;
import app.entities.VipPrice;
@ -49,16 +50,23 @@ public class ExternalVIPController {
@PostMapping
@CollectStatistic(stage = CollectStages.COMBINED)
@CheckWebAccess(auth_method = AuthMethod.SECRET_KEY)
@ClusterMethod
public ResponseEntity<Integer> addVIP(HttpServletRequest request,
@RequestParam String steam,
@RequestParam int amount,
@RequestParam String service,
@RequestParam(required = false, defaultValue = "") String extra,
@RequestParam(required = false, defaultValue = "") String unique) {
@RequestParam(required = false, defaultValue = "") String unique,
@RequestHeader(value = "F13-TO-CLUSTER", defaultValue = "") String cluster) {
if (!unique.isEmpty()) {
if (unique_set.contains(unique)) return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE);
else unique_set.add(unique);
else {
unique_set.add(unique);
if (!cluster.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE);
}
}
}
int result = vipService.addVIP(
@ -84,6 +92,7 @@ public class ExternalVIPController {
@CheckWebAccess
@CollectStatistic
@CheckPermitionFlag(flag = "z")
@ClusterMethod
public ResponseEntity<HashMap> disable(HttpServletRequest request, @RequestParam(value = "type", required = false) String type) {
if (vipService.getServices().containsKey(type)) vipService.getServices().put(type, !vipService.getServices().get(type));
return new ResponseEntity<>(vipService.getServices(), HttpStatus.OK);

13
src/main/java/app/controllers/other/PulseController.java

@ -1,5 +1,6 @@
package app.controllers.other;
import app.services.StatsService;
import app.services.db.DBService;
import app.updates.OnlineUpdater;
import org.springframework.beans.factory.annotation.Autowired;
@ -9,6 +10,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
/**
* контролллер для проверки жива ли эта хуйня и че там по бд
*/
@ -18,12 +21,15 @@ public class PulseController {
DBService dbService;
OnlineUpdater onlineUpdater;
StatsService statsService;
@Autowired
public PulseController(DBService dbService,
OnlineUpdater onlineUpdater) {
OnlineUpdater onlineUpdater,
StatsService statsService) {
this.dbService = dbService;
this.onlineUpdater = onlineUpdater;
this.statsService = statsService;
}
@GetMapping("/db")
@ -39,4 +45,9 @@ public class PulseController {
public ResponseEntity<Long> getDiff() {
return new ResponseEntity<>(onlineUpdater.getDifferentReplica(), HttpStatus.OK);
}
@GetMapping("/services")
public ResponseEntity<HashMap<String, Long>> getServices() {
return new ResponseEntity<>(statsService.getServices(), HttpStatus.OK);
}
}

12
src/main/java/app/controllers/server/ServerUpdaterController.java

@ -2,6 +2,7 @@ package app.controllers.server;
import app.annotations.enums.AuthMethod;
import app.annotations.interfaces.CheckWebAccess;
import app.annotations.interfaces.ClusterMethod;
import app.entities.Stats;
import app.entities.report.ReportBody;
import app.entities.report.ReportType;
@ -45,6 +46,7 @@ public class ServerUpdaterController {
@PostMapping(value = "/{srv}")
@CheckWebAccess(auth_method = AuthMethod.SECRET_KEY)
@ClusterMethod
public ResponseEntity updateServer(HttpServletRequest request, @PathVariable String srv, @RequestBody ServerRequestBody serverRequestBody) {
if (!stats.getServers().containsKey(srv)) return new ResponseEntity<>(HttpStatus.NOT_FOUND);
stats.getServers().get(srv).RefreshServerFromRequest(serverRequestBody);
@ -55,6 +57,7 @@ public class ServerUpdaterController {
@DeleteMapping(value = "/{srv}")
@CheckWebAccess(auth_method = AuthMethod.SECRET_KEY)
@ClusterMethod
public ResponseEntity downServer(HttpServletRequest request, @PathVariable String srv) {
if (!stats.getServers().containsKey(srv)) return new ResponseEntity<>(HttpStatus.NOT_FOUND);
stats.getServers().get(srv).RefreshServerFromRequest(null);
@ -71,10 +74,15 @@ public class ServerUpdaterController {
*/
@PostMapping(value = "/{srv}/report")
@CheckWebAccess(auth_method = AuthMethod.SECRET_KEY)
@ClusterMethod
public ResponseEntity sendReport(HttpServletRequest request,
@RequestBody ReportBody body, @PathVariable String srv) {
@RequestBody ReportBody body, @PathVariable String srv,
@RequestHeader(value = "F13-TO-CLUSTER", defaultValue = "") String cluster) {
return ResponseEntity.status(HttpStatus.OK).body(
reportService.createReport(ReportType.IN_GAME, profileService.GetProfile(body.getAuthor_steam64(), "permition,steam_data"), profileService.GetProfile(body.getReported_steam64(), "permition"), body.getReason()));
reportService.createReport(ReportType.IN_GAME,
profileService.GetProfile(body.getAuthor_steam64(), "permition,steam_data"),
profileService.GetProfile(body.getReported_steam64(), "permition"),
body.getReason(), cluster));
}
@PostMapping(value = "/{srv}/report/{report_id}")

7
src/main/java/app/controllers/user/ProfileController.java

@ -91,10 +91,12 @@ public class ProfileController {
@BurstUpdatePlayers
@WaitAfterNext(order = "report")
@CollectStatistic(stage = CollectStages.COMBINED)
@ClusterMethod
public ResponseEntity<Long> ReportUser(HttpServletRequest request,
@CookieValue(value = "steam64", defaultValue = "") String steam64,
@RequestParam(value = "steam64", defaultValue = "") String reported_steam64,
@RequestParam(value = "text", defaultValue = "") String text) {
@RequestParam(value = "text", defaultValue = "") String text,
@RequestHeader(value = "F13-TO-CLUSTER", defaultValue = "") String cluster) {
if (profileService.GetProfile(steam64, "ban").getBan() != null)
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build();
@ -102,7 +104,8 @@ public class ProfileController {
ReportType.WEBSITE,
profileService.GetProfile(steam64, "permition,steam_data"),
profileService.GetProfile(reported_steam64, "permition"),
text
text,
cluster
), HttpStatus.OK);
}

3
src/main/java/app/entities/Stats.java

@ -2,6 +2,7 @@ package app.entities;
import app.entities.server.Server;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@ -28,6 +29,8 @@ public class Stats {
Statistic statistic = new Statistic();
String builddate = "";
HashMap<String, HashMap> donate = new HashMap<>();
@JsonIgnore
HashMap<String, Long> updates_internal = new HashMap<>();
public Stats(){
try {

2
src/main/java/app/entities/VipGiveMethod.java

@ -7,7 +7,7 @@ public enum VipGiveMethod {
MANUAL("Админ", "Admin"),
AFTERTIME("Убрана"),
TOTAL("Итого"),
DONATIONALERTS("Донейшон Алертс", "DonationAlerts", true, true, true),
DONATIONALERTS("Донейшон Алертс", "DonationAlerts", false, true, true),
PROMOCODE("Промокод", "PromoCode");
private final String human_name;

10
src/main/java/app/services/StatsService.java

@ -12,6 +12,8 @@ import app.utils.CryptedCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@Service
@ -78,4 +80,12 @@ public class StatsService {
if (!stats.getServers().containsKey(server_name)) return "Invalid server name";
return stats.getServers().get(server_name).ExecuteRCON(command);
}
public void updateService(String name) {
stats.getUpdates_internal().put(name, Instant.now().getEpochSecond());
}
public HashMap<String, Long> getServices() {
return stats.getUpdates_internal();
}
}

8
src/main/java/app/services/db/ReportService.java

@ -108,7 +108,7 @@ public class ReportService {
return jdbcTemplate_ro.query("SELECT * FROM user_reports WHERE r_steam2 LIKE ?", new Object[]{steamID.steam2}, (rs,n) -> new Report(rs));
}
public long createReport(ReportType reportType, PlayerProfile current_user, PlayerProfile reported_user, String text) {
public long createReport(ReportType reportType, PlayerProfile current_user, PlayerProfile reported_user, String text, String cluster) {
switch (reportType) {
case WEBSITE -> {
if (reported_user.getPlay_on() == null) return 0L;
@ -116,6 +116,12 @@ public class ReportService {
(Instant.now().getEpochSecond() - userKD.get(current_user.getSteamids().steam64)) < kd) {
return (Instant.now().getEpochSecond() - userKD.get(current_user.getSteamids().steam64)) - kd;
}
if (!cluster.isEmpty()) {
userKD.merge(current_user.getSteamids().steam64, Instant.now().getEpochSecond(), (x,y) -> y);
return userKD.get(current_user.getSteamids().steam64);
}
long report_id = addReport(current_user, reported_user, text, reportType);
long res = createReportToDiscord(current_user, reported_user, text, report_id);
if (res == 0) return 0;

2
src/main/java/app/updates/BanCountUpdater.java

@ -35,7 +35,7 @@ public class BanCountUpdater extends BaseUpdater{
public void SetUpdater(){
if(update) {
logger.warn("Updater enabled");
CreateTaskUpdater(this::UpdateBanCount, 5 * 60 * 1000);
CreateTaskUpdater(this::UpdateBanCount, 5 * 60 * 1000, getClass().getName());
}
}

11
src/main/java/app/updates/BaseUpdater.java

@ -1,23 +1,30 @@
package app.updates;
import app.services.StatsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
@Service
public abstract class BaseUpdater {
@Autowired
private StatsService statsService;
private final Logger logger = LoggerFactory.getLogger(BaseUpdater.class);
public void CreateTaskUpdater(Supplier function, int timeout) {
logger.warn("Create task: {}, update every {} seconds", function.toString(), timeout / 1000);
public void CreateTaskUpdater(Supplier function, int timeout, String name) {
logger.warn("Create task: {}, update every {} seconds", name, timeout / 1000);
Executors.newFixedThreadPool(1).submit(() -> {
while (true) {
try {
//System.out.printf("Call: %s\n", function.toString());
function.get();
statsService.updateService(name);
} catch (Exception err) {
err.printStackTrace();
} finally {

2
src/main/java/app/updates/CountriesUpdater.java

@ -44,7 +44,7 @@ public class CountriesUpdater extends BaseUpdater{
public void UpdateCountries(){
if (update) {
logger.warn("Updater enabled");
CreateTaskUpdater(this::UpdateCountriesStatistic, 30 * 60 * 1000);
CreateTaskUpdater(this::UpdateCountriesStatistic, 30 * 60 * 1000, getClass().getName());
}
}

4
src/main/java/app/updates/OnlineUpdater.java

@ -45,9 +45,9 @@ public class OnlineUpdater extends BaseUpdater {
logger.warn("Per server online updater enabled");
stats.getServers().forEach((server_name, server) -> {
logger.info("{} created online updater", server_name);
CreateTaskUpdater(() -> AppendOnlineStats(server_name), sleep_time);
CreateTaskUpdater(() -> AppendOnlineStats(server_name), sleep_time, getClass().getName() + "_" + server_name);
});
CreateTaskUpdater(this::updateCurrentDayPeak, sleep_time);
CreateTaskUpdater(this::updateCurrentDayPeak, sleep_time, getClass().getName() + ".updateCurrentDayPeak");
}
public boolean AppendOnlineStats(String server_id) {

4
src/main/java/app/updates/PlayersUpdater.java

@ -42,14 +42,14 @@ public class PlayersUpdater extends BaseUpdater{
serversHandler.pushServer(server_name, server);
}
return null;
}, 15000);
}, 15000, getClass().getName() + "_refresh_check_" + server_name);
} else if (System.getenv("A2S") != null && System.getenv("A2S").equals("true")) {
logger.warn("Create a2s updater: {} server", server_name);
CreateTaskUpdater(() -> {
logger.info("Update players from: {} server", server_name);
server.RefreshServerA2SData();
return null;
}, 60000);
}, 60000, getClass().getName() + "_refresh_a2s_" + server_name);
} else {
logger.warn("{} server not be update", server_name);
}

2
src/main/java/app/updates/PreviewUpdater.java

@ -40,7 +40,7 @@ public class PreviewUpdater extends BaseUpdater{
CreateTaskUpdater(() -> {
loadPreview(stats.getServers().get(server_name));
return null;
}, 30000);
}, 30000, getClass().getName()+"_"+server_name);
});
}
}

4
src/main/java/app/updates/SocialUpdater.java

@ -39,11 +39,11 @@ public class SocialUpdater extends BaseUpdater{
public void SetUpdater(){
if(!discord_url.isEmpty()) {
logger.warn("Discord count updater enabled");
CreateTaskUpdater(this::UpdateDiscordCount, 5 * 60 * 1000);
CreateTaskUpdater(this::UpdateDiscordCount, 5 * 60 * 1000, getClass().getName() + "_discord");
}
if(!vk_url.isEmpty()){
logger.warn("VK count updater enabled");
CreateTaskUpdater(this::UpdateVKCount, 5 * 60 * 1000);
CreateTaskUpdater(this::UpdateVKCount, 5 * 60 * 1000, getClass().getName() + "_vk");
}
}

10
src/main/java/app/updates/UniqueUpdater.java

@ -46,16 +46,16 @@ public class UniqueUpdater extends BaseUpdater{
if(global_update) {
logger.warn("Global updater enabled");
Thread.sleep(2000);
CreateTaskUpdater(this::UpdateServerUniqueTotal, 24 * 60 * 60 * 1000);
CreateTaskUpdater(this::UpdateServerUniqueYear, 24 * 60 * 60 * 1000);
CreateTaskUpdater(this::UpdateServerUniqueMonth, 24 * 60 * 60 * 1000);
CreateTaskUpdater(this::UpdateServerUniqueDay, 60 * 60 * 1000);
CreateTaskUpdater(this::UpdateServerUniqueTotal, 24 * 60 * 60 * 1000, getClass().getName() + "_total");
CreateTaskUpdater(this::UpdateServerUniqueYear, 24 * 60 * 60 * 1000, getClass().getName() + "_year");
CreateTaskUpdater(this::UpdateServerUniqueMonth, 24 * 60 * 60 * 1000, getClass().getName() + "_month");
CreateTaskUpdater(this::UpdateServerUniqueDay, 60 * 60 * 1000, getClass().getName() + "_day");
}
///////////////////////////////////////////////////////////////////////////////////////
if(server_update) {
logger.warn("Per server updater enabled");
stats.getServers().forEach((server_name, server) -> {
CreateTaskUpdater(() -> getServerUnique(server_name, server.getDb()), 5 * 60 * 1000);
CreateTaskUpdater(() -> getServerUnique(server_name, server.getDb()), 5 * 60 * 1000, getClass().getName() + "_" + server_name);
});
}
}

6
src/main/java/app/updates/VipCountUpdater.java

@ -47,9 +47,9 @@ public class VipCountUpdater extends BaseUpdater{
public void SetUpdater(){
if (update) {
logger.warn("Updater enabled");
CreateTaskUpdater(this::UpdateVIPCount, timeout);
CreateTaskUpdater(this::UpdateFreeVIPCount, timeout);
CreateTaskUpdater(this::CheckEndedVIPs, 60 * 1000);
CreateTaskUpdater(this::UpdateVIPCount, timeout, getClass().getName() + "_count");
CreateTaskUpdater(this::UpdateFreeVIPCount, timeout, getClass().getName() + "_freecount");
CreateTaskUpdater(this::CheckEndedVIPs, 60 * 1000, getClass().getName() + "_ended");
}
}

Loading…
Cancel
Save