Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bazel/java.MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ maven.install(
artifacts = [
"ch.qos.logback:logback-classic:%s" % LOGBACK_VERSION,
"ch.qos.logback:logback-core:%s" % LOGBACK_VERSION,
"com.zaxxer:HikariCP:5.1.0",
"com.fasterxml.jackson.core:jackson-annotations",
"com.fasterxml.jackson.core:jackson-core",
"com.fasterxml.jackson.core:jackson-databind",
Expand Down Expand Up @@ -41,7 +42,12 @@ maven.install(
"org.eclipse.jetty:jetty-server:%s" % JETTY_VERSION,
"org.eclipse.jetty.websocket:websocket-jetty-server:%s" % JETTY_VERSION,
"org.jspecify:jspecify:1.0.0",
"org.postgresql:postgresql:42.7.4",
"org.slf4j:slf4j-api:2.0.17",
"software.amazon.awssdk:auth:2.29.44",
"software.amazon.awssdk:regions:2.29.44",
"software.amazon.awssdk:sqs:2.29.44",
"software.amazon.awssdk:url-connection-client:2.29.44",
],
boms = [
"io.netty:netty-bom:4.2.9.Final",
Expand Down
14 changes: 14 additions & 0 deletions jvm/src/main/java/com/muchq/chess_indexer/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.muchq.chess_indexer;

import io.micronaut.runtime.Micronaut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
private static final Logger LOG = LoggerFactory.getLogger(App.class);

public static void main(String[] args) {
LOG.info("Starting chess indexer API");
Micronaut.run(App.class, args);
}
}
77 changes: 77 additions & 0 deletions jvm/src/main/java/com/muchq/chess_indexer/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
load("@rules_java//java:java_binary.bzl", "java_binary")
load("@rules_java//java:defs.bzl", "java_library")
load("//bazel/rules:oci.bzl", "linux_oci_java")

java_library(
name = "chess_indexer_lib",
srcs = glob(["**/*.java"], exclude = [
"App.java",
"worker/WorkerApp.java",
]),
visibility = ["//visibility:public"],
deps = [
"//jvm/src/main/java/com/muchq/chess_com_api",
"//jvm/src/main/java/com/muchq/http_client/core",
"//jvm/src/main/java/com/muchq/http_client/jdk11",
"//jvm/src/main/java/com/muchq/json",
"@maven//:com_fasterxml_jackson_core_jackson_annotations",
"@maven//:com_fasterxml_jackson_core_jackson_databind",
"@maven//:com_zaxxer_HikariCP",
"@maven//:io_micronaut_micronaut_context",
"@maven//:io_micronaut_micronaut_core",
"@maven//:io_micronaut_micronaut_http",
"@maven//:io_micronaut_micronaut_runtime",
"@maven//:jakarta_inject_jakarta_inject_api",
"@maven//:org_postgresql_postgresql",
"@maven//:org_slf4j_slf4j_api",
"@maven//:software_amazon_awssdk_auth",
"@maven//:software_amazon_awssdk_regions",
"@maven//:software_amazon_awssdk_sqs",
"@maven//:software_amazon_awssdk_url_connection_client",
],
)

java_binary(
name = "chess_indexer_api",
srcs = ["App.java"],
main_class = "com.muchq.chess_indexer.App",
plugins = [
"//bazel/rules:micronaut_type_element_visitor_processor",
"//bazel/rules:micronaut_aggregating_type_element_visitor_processor",
"//bazel/rules:micronaut_bean_definition_inject_processor",
"//bazel/rules:micronaut_package_element_visitor_processor",
],
resources = ["//jvm/src/main/resources:micronaut_config"],
visibility = ["//visibility:public"],
runtime_deps = [
"@maven//:ch_qos_logback_logback_classic",
],
deps = [
":chess_indexer_lib",
"@maven//:io_micronaut_micronaut_http_server_netty",
],
)

java_binary(
name = "chess_indexer_worker",
srcs = ["worker/WorkerApp.java"],
main_class = "com.muchq.chess_indexer.worker.WorkerApp",
plugins = [
"//bazel/rules:micronaut_type_element_visitor_processor",
"//bazel/rules:micronaut_aggregating_type_element_visitor_processor",
"//bazel/rules:micronaut_bean_definition_inject_processor",
"//bazel/rules:micronaut_package_element_visitor_processor",
],
resources = ["//jvm/src/main/resources:micronaut_config"],
visibility = ["//visibility:public"],
runtime_deps = [
"@maven//:ch_qos_logback_logback_classic",
],
deps = [
":chess_indexer_lib",
"@maven//:io_micronaut_micronaut_http_server_netty",
],
)

linux_oci_java(bin_name = "chess_indexer_api")
linux_oci_java(bin_name = "chess_indexer_worker")
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.muchq.chess_indexer.api;

import com.muchq.chess_indexer.db.IndexRequestDao;
import com.muchq.chess_indexer.ingest.IndexRequestService;
import com.muchq.chess_indexer.model.IndexRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.Post;
import jakarta.inject.Inject;
import java.time.LocalDate;
import java.util.Optional;
import java.util.UUID;

@Controller("/index-requests")
public class IndexRequestController {

private final IndexRequestService indexRequestService;
private final IndexRequestDao indexRequestDao;

@Inject
public IndexRequestController(IndexRequestService indexRequestService, IndexRequestDao indexRequestDao) {
this.indexRequestService = indexRequestService;
this.indexRequestDao = indexRequestDao;
}

@Post
public HttpResponse<IndexRequestResponse> create(@Body IndexRequestCreateRequest request) {
try {
String platform = normalizePlatform(request.platform());
LocalDate startDate = parseDate(request.startDate());
LocalDate endDate = parseDate(request.endDate());
if (endDate.isBefore(startDate)) {
return HttpResponse.badRequest();
}

IndexRequest created = indexRequestService.submit(platform, request.username(), startDate, endDate);
return HttpResponse.created(IndexRequestResponse.from(created));
} catch (IllegalArgumentException e) {
return HttpResponse.badRequest();
}
}

@Get("/{id}")
public HttpResponse<IndexRequestResponse> get(@PathVariable String id) {
Optional<IndexRequest> request = indexRequestDao.findById(UUID.fromString(id));
return request.map(value -> HttpResponse.ok(IndexRequestResponse.from(value)))
.orElseGet(HttpResponse::notFound);
}

private String normalizePlatform(String platform) {
if (platform == null) {
throw new IllegalArgumentException("platform is required");
}
String normalized = platform.trim().toLowerCase();
if (!normalized.equals("chess.com")) {
throw new IllegalArgumentException("Only chess.com is supported right now");
}
return normalized;
}

private LocalDate parseDate(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("date is required");
}
return LocalDate.parse(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.muchq.chess_indexer.api;

public record IndexRequestCreateRequest(
String platform,
String username,
String startDate,
String endDate
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.muchq.chess_indexer.api;

import com.muchq.chess_indexer.model.IndexRequest;

public record IndexRequestResponse(
String id,
String platform,
String username,
String startDate,
String endDate,
String status,
int gamesIndexed,
String errorMessage
) {
public static IndexRequestResponse from(IndexRequest request) {
return new IndexRequestResponse(
request.id().toString(),
request.platform(),
request.username(),
request.startDate().toString(),
request.endDate().toString(),
request.status().name(),
request.gamesIndexed(),
request.errorMessage()
);
}
}
34 changes: 34 additions & 0 deletions jvm/src/main/java/com/muchq/chess_indexer/api/QueryController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.muchq.chess_indexer.api;

import com.muchq.chess_indexer.model.GameSummary;
import com.muchq.chess_indexer.query.QueryService;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import jakarta.inject.Inject;
import java.util.List;

@Controller("/queries")
public class QueryController {

private final QueryService queryService;

@Inject
public QueryController(QueryService queryService) {
this.queryService = queryService;
}

@Post
public HttpResponse<QueryResponse> query(@Body QueryRequest request) {
if (request == null || request.query() == null || request.query().isBlank()) {
return HttpResponse.badRequest();
}
try {
List<GameSummary> games = queryService.run(request.query());
return HttpResponse.ok(QueryResponse.of(games));
} catch (IllegalArgumentException e) {
return HttpResponse.badRequest();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.muchq.chess_indexer.api;

public record QueryRequest(String query) {}
10 changes: 10 additions & 0 deletions jvm/src/main/java/com/muchq/chess_indexer/api/QueryResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.muchq.chess_indexer.api;

import com.muchq.chess_indexer.model.GameSummary;
import java.util.List;

public record QueryResponse(int count, List<GameSummary> games) {
public static QueryResponse of(List<GameSummary> games) {
return new QueryResponse(games.size(), games);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.muchq.chess_indexer.config;

public record IndexerConfig(
String dbUrl,
String dbUser,
String dbPassword,
String sqsQueueUrl,
String awsRegion,
int workerPollSeconds,
int workerBatchSize,
int apiQueryLimit
) {

public static IndexerConfig fromEnv() {
String dbUrl = getenvOrThrow("INDEXER_DB_URL");
String dbUser = getenvOrDefault("INDEXER_DB_USER", "postgres");
String dbPassword = getenvOrDefault("INDEXER_DB_PASSWORD", "");
String sqsQueueUrl = getenvOrDefault("INDEXER_SQS_QUEUE_URL", "");
String awsRegion = getenvOrDefault("AWS_REGION", "us-east-1");

int workerPollSeconds = parseInt(getenvOrDefault("INDEXER_WORKER_POLL_SECONDS", "10"));
int workerBatchSize = parseInt(getenvOrDefault("INDEXER_WORKER_BATCH_SIZE", "5"));
int apiQueryLimit = parseInt(getenvOrDefault("INDEXER_API_QUERY_LIMIT", "100"));

return new IndexerConfig(
dbUrl,
dbUser,
dbPassword,
sqsQueueUrl,
awsRegion,
workerPollSeconds,
workerBatchSize,
apiQueryLimit);
}

private static String getenvOrDefault(String key, String defaultValue) {
String value = System.getenv(key);
return value == null ? defaultValue : value;
}

private static String getenvOrThrow(String key) {
String value = System.getenv(key);
if (value == null || value.isBlank()) {
throw new IllegalStateException("Missing required env var: " + key);
}
return value;
}

private static int parseInt(String raw) {
try {
return Integer.parseInt(raw);
} catch (NumberFormatException e) {
throw new IllegalStateException("Failed to parse int env var: " + raw, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.muchq.chess_indexer.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.muchq.chess_com_api.ChessClient;
import com.muchq.http_client.core.HttpClient;
import com.muchq.http_client.jdk11.Jdk11HttpClient;
import com.muchq.json.JsonUtils;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import io.micronaut.context.annotation.Context;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Singleton;
import java.time.Clock;
import javax.sql.DataSource;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;

@Factory
public class IndexerModule {

@Context
public IndexerConfig indexerConfig() {
return IndexerConfig.fromEnv();
}

@Context
public Clock clock() {
return Clock.systemUTC();
}

@Context
public ObjectMapper objectMapper() {
return JsonUtils.mapper();
}

@Context
public HttpClient httpClient() {
return new Jdk11HttpClient(java.net.http.HttpClient.newHttpClient());
}

@Context
public ChessClient chessClient(HttpClient httpClient, ObjectMapper objectMapper) {
return new ChessClient(httpClient, objectMapper);
}

@Singleton
public DataSource dataSource(IndexerConfig config) {
HikariConfig hikari = new HikariConfig();
hikari.setJdbcUrl(config.dbUrl());
hikari.setUsername(config.dbUser());
hikari.setPassword(config.dbPassword());
hikari.setMaximumPoolSize(10);
hikari.setMinimumIdle(1);
hikari.setPoolName("chess-indexer");
return new HikariDataSource(hikari);
}

@Singleton
public SqsClient sqsClient(IndexerConfig config) {
return SqsClient.builder()
.region(Region.of(config.awsRegion()))
.credentialsProvider(DefaultCredentialsProvider.create())
.httpClientBuilder(UrlConnectionHttpClient.builder())
.build();
}
}
Loading
Loading