340 lines
9.1 KiB
Go
340 lines
9.1 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"sync"
|
|
|
|
"datarush/internal/results/domain"
|
|
pb "datarush/pkg/api/results"
|
|
submissionPb "datarush/pkg/api/submission"
|
|
taskPb "datarush/pkg/api/task"
|
|
userPb "datarush/pkg/api/user"
|
|
|
|
"github.com/google/uuid"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
type IResultService interface {
|
|
CreateResult(ctx context.Context, result *domain.Result) (*domain.Result, error)
|
|
GetResult(ctx context.Context, id uuid.UUID) (*domain.Result, error)
|
|
UpdateResult(ctx context.Context, result *domain.Result) (*domain.Result, error)
|
|
DeleteResult(ctx context.Context, id uuid.UUID) error
|
|
ListResults(
|
|
ctx context.Context,
|
|
pageSize int32,
|
|
pageToken int32,
|
|
competitionID *uuid.UUID,
|
|
userID *uuid.UUID,
|
|
status *domain.ResultStatus,
|
|
minScore *float64,
|
|
maxScore *float64,
|
|
) (results []domain.Result, totalCount int32, nextPageToken int32, err error)
|
|
GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error)
|
|
GetLeaderboard(
|
|
ctx context.Context,
|
|
competitionID uuid.UUID,
|
|
limit int32,
|
|
offset int32,
|
|
) ([]domain.Result, int32, error)
|
|
UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error)
|
|
RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error
|
|
}
|
|
|
|
type ResultsHandler struct {
|
|
pb.UnimplementedResultsServiceServer
|
|
service IResultService
|
|
userServiceAddr string
|
|
submissionServiceAddr string
|
|
taskServiceAddr string
|
|
userClient userPb.UserServiceClient
|
|
submissionClient submissionPb.SubmissionServiceClient
|
|
taskClient taskPb.TaskServiceClient
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewResultsHandler(
|
|
s IResultService,
|
|
userServiceAddr, submissionServiceAddr, taskServiceAddr string,
|
|
) *ResultsHandler {
|
|
return &ResultsHandler{
|
|
service: s,
|
|
userServiceAddr: userServiceAddr,
|
|
submissionServiceAddr: submissionServiceAddr,
|
|
taskServiceAddr: taskServiceAddr,
|
|
}
|
|
}
|
|
|
|
func (h *ResultsHandler) getUserClient(ctx context.Context) (userPb.UserServiceClient, error) {
|
|
h.mu.RLock()
|
|
if h.userClient != nil {
|
|
client := h.userClient
|
|
h.mu.RUnlock()
|
|
return client, nil
|
|
}
|
|
h.mu.RUnlock()
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.userClient != nil {
|
|
return h.userClient, nil
|
|
}
|
|
|
|
conn, err := grpc.DialContext(ctx, h.userServiceAddr,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithBlock())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h.userClient = userPb.NewUserServiceClient(conn)
|
|
return h.userClient, nil
|
|
}
|
|
|
|
func (h *ResultsHandler) getSubmissionClient(ctx context.Context) (submissionPb.SubmissionServiceClient, error) {
|
|
h.mu.RLock()
|
|
if h.submissionClient != nil {
|
|
client := h.submissionClient
|
|
h.mu.RUnlock()
|
|
return client, nil
|
|
}
|
|
h.mu.RUnlock()
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.submissionClient != nil {
|
|
return h.submissionClient, nil
|
|
}
|
|
|
|
conn, err := grpc.DialContext(ctx, h.submissionServiceAddr,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithBlock())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h.submissionClient = submissionPb.NewSubmissionServiceClient(conn)
|
|
return h.submissionClient, nil
|
|
}
|
|
|
|
func (h *ResultsHandler) getTaskClient(ctx context.Context) (taskPb.TaskServiceClient, error) {
|
|
h.mu.RLock()
|
|
if h.taskClient != nil {
|
|
client := h.taskClient
|
|
h.mu.RUnlock()
|
|
return client, nil
|
|
}
|
|
h.mu.RUnlock()
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.taskClient != nil {
|
|
return h.taskClient, nil
|
|
}
|
|
|
|
conn, err := grpc.DialContext(ctx, h.taskServiceAddr,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithBlock())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h.taskClient = taskPb.NewTaskServiceClient(conn)
|
|
return h.taskClient, nil
|
|
}
|
|
|
|
func (h *ResultsHandler) fetchUsername(ctx context.Context, userID string) string {
|
|
client, err := h.getUserClient(ctx)
|
|
if err != nil {
|
|
log.Printf("failed to get user client: %v", err)
|
|
return ""
|
|
}
|
|
|
|
user, err := client.GetProfile(ctx, &userPb.GetProfileRequest{UserId: userID})
|
|
if err != nil {
|
|
log.Printf("failed to get user profile for user %s: %v", userID, err)
|
|
return ""
|
|
}
|
|
|
|
return user.GetUsername()
|
|
}
|
|
|
|
func (h *ResultsHandler) fetchTaskStatuses(ctx context.Context, competitionID, userID string) []*pb.TaskStatus {
|
|
client, err := h.getSubmissionClient(ctx)
|
|
if err != nil {
|
|
log.Printf("failed to get submission client: %v", err)
|
|
return []*pb.TaskStatus{}
|
|
}
|
|
|
|
resp, err := client.ListSubmissions(ctx, &submissionPb.ListSubmissionsRequest{
|
|
CompetitionId: competitionID,
|
|
UserId: userID,
|
|
PageSize: 1000,
|
|
PageToken: 0,
|
|
})
|
|
if err != nil {
|
|
log.Printf("failed to list submissions for user %s in competition %s: %v", userID, competitionID, err)
|
|
return []*pb.TaskStatus{}
|
|
}
|
|
|
|
taskMap := make(map[string]*pb.TaskStatus)
|
|
for _, submission := range resp.GetSubmissions() {
|
|
taskID := submission.GetTaskId()
|
|
|
|
existing, exists := taskMap[taskID]
|
|
if !exists {
|
|
taskMap[taskID] = &pb.TaskStatus{
|
|
TaskId: taskID,
|
|
TaskTitle: "",
|
|
EarnedPoints: submission.GetEarnedPoints(),
|
|
MaxPoints: 0,
|
|
Position: nil,
|
|
}
|
|
} else {
|
|
if submission.GetEarnedPoints() > existing.EarnedPoints {
|
|
existing.EarnedPoints = submission.GetEarnedPoints()
|
|
}
|
|
}
|
|
}
|
|
|
|
taskClient, err := h.getTaskClient(ctx)
|
|
if err != nil {
|
|
log.Printf("failed to get task client: %v", err)
|
|
} else {
|
|
for taskID, status := range taskMap {
|
|
task, err := taskClient.GetTask(ctx, &taskPb.GetTaskRequest{TaskId: taskID})
|
|
if err != nil {
|
|
log.Printf("failed to get task %s: %v", taskID, err)
|
|
continue
|
|
}
|
|
status.TaskTitle = task.GetTitle()
|
|
status.MaxPoints = task.GetMaxPoints()
|
|
}
|
|
}
|
|
|
|
taskStatuses := make([]*pb.TaskStatus, 0, len(taskMap))
|
|
for _, status := range taskMap {
|
|
taskStatuses = append(taskStatuses, status)
|
|
}
|
|
|
|
return taskStatuses
|
|
}
|
|
|
|
func (h *ResultsHandler) enrichUserResult(
|
|
ctx context.Context,
|
|
result *domain.Result,
|
|
competitionID string,
|
|
) *pb.UserResult {
|
|
userID := result.UserID.String()
|
|
username := h.fetchUsername(ctx, userID)
|
|
taskStatuses := h.fetchTaskStatuses(ctx, competitionID, userID)
|
|
|
|
pbResult := &pb.UserResult{
|
|
UserId: userID,
|
|
Username: username,
|
|
TotalScore: int32(result.Score),
|
|
OverallPosition: 0,
|
|
TaskStatuses: taskStatuses,
|
|
}
|
|
|
|
if result.Rank != nil {
|
|
pbResult.OverallPosition = int32(*result.Rank)
|
|
}
|
|
|
|
return pbResult
|
|
}
|
|
|
|
func (h *ResultsHandler) GetCompetitionResults(
|
|
ctx context.Context,
|
|
req *pb.GetCompetitionResultsRequest,
|
|
) (*pb.GetCompetitionResultsResponse, error) {
|
|
competitionID, err := uuid.Parse(req.GetCompetitionId())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
|
|
}
|
|
|
|
results, total, err := h.service.GetLeaderboard(ctx, competitionID, req.GetPageSize(), req.GetPageToken())
|
|
if err != nil {
|
|
log.Printf("failed to get competition results: %v", err)
|
|
return nil, status.Errorf(codes.Internal, "failed to get competition results: %v", err)
|
|
}
|
|
|
|
pbResults := make([]*pb.UserResult, len(results))
|
|
var wg sync.WaitGroup
|
|
|
|
for i := range results {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
pbResults[idx] = h.enrichUserResult(ctx, &results[idx], req.GetCompetitionId())
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
var nextPageToken int32
|
|
if int(req.GetPageToken()+req.GetPageSize()) < int(total) {
|
|
nextPageToken = req.GetPageToken() + 1
|
|
}
|
|
|
|
return &pb.GetCompetitionResultsResponse{
|
|
Results: pbResults,
|
|
TotalCount: total,
|
|
NextPageToken: nextPageToken,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ResultsHandler) GetUserCompetitionResults(
|
|
ctx context.Context,
|
|
req *pb.GetUserCompetitionResultsRequest,
|
|
) (*pb.GetUserCompetitionResultsResponse, error) {
|
|
competitionID, err := uuid.Parse(req.GetCompetitionId())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
|
|
}
|
|
|
|
userID, err := uuid.Parse(req.GetUserId())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid user id format: %v", err)
|
|
}
|
|
|
|
result, err := h.service.GetByCompetitionAndUser(ctx, competitionID, userID)
|
|
if err != nil {
|
|
if err == domain.ErrResultNotFound {
|
|
return nil, status.Errorf(codes.NotFound, "result not found for user in competition")
|
|
}
|
|
log.Printf("failed to get user competition results: %v", err)
|
|
return nil, status.Errorf(codes.Internal, "failed to get user competition results: %v", err)
|
|
}
|
|
|
|
pbResult := h.enrichUserResult(ctx, result, req.GetCompetitionId())
|
|
|
|
return &pb.GetUserCompetitionResultsResponse{
|
|
Result: pbResult,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ResultsHandler) RecalculateResults(
|
|
ctx context.Context,
|
|
req *pb.RecalculateResultsRequest,
|
|
) (*emptypb.Empty, error) {
|
|
competitionID, err := uuid.Parse(req.GetCompetitionId())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
|
|
}
|
|
|
|
if err := h.service.RecalculateRanks(ctx, competitionID); err != nil {
|
|
log.Printf("failed to recalculate results: %v", err)
|
|
return nil, status.Errorf(codes.Internal, "failed to recalculate results: %v", err)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|