仓库源文站点原文


title: Portable OJ 一期工程技术方案 date: 2021-10-31 20:09:29 math: true mermaid: true

hide: true

架构设计

系统结构

采用三端分离的架构

web

server

judge

存储结构

所有模块的需要索引的信息和部分短字段均存储至 MySQL,非短字段或者非需要索引字段均存储至 Mongo,Redis 仅用于缓存且不采用持久化

模块设计

判断模块从属规则:基于哪个实体的数据进行逻辑判断

用户模块

用户属性

权限 名称 功能
CHANGE_ORGANIZATION 修改用户组织权 能够让一个受你管理的组织下的一个用户的组织转为一个新的受你管理的组织下
GRANT 分发权限 将自己拥有的权限同时授权给一个受你管理的组织下的一个用户,或者从他手里收回此权限
VIEW_HIDDEN_PROBLEM 查看隐藏题目权 拥有题库查看、题库提交、比赛链接隐藏题目的权利,同时也可以下载公开、隐藏题目的数据
CREATE_AND_EDIT_PROBLEM 创建编辑自己拥有的题目 可以创建题目,同时可以编辑自己拥有的题目
EDIT_NOT_OWNER_PROBLEM 编辑可访问的题目 可以随意编辑任何可以查看的题目
VIEW_PUBLIC_SOLUTION 查看公开的所有提交 可以查看公开的所有提交
VIEW_SOLUTION_MESSAGE 查看提交的运行信息 查看提交的运行信息
MANAGER_JUDGE 管理判题系统 管理判题系统

用户功能

问题模块

问题属性

状态 名称 描述
NORMAL 正常 标准的题目,仅此状态可以进行提交
UNTREATED 未处理 未进行生成 output 文件
TREATING 处理中 正在生成 output 文件
UNCHECK 未校验 未校验标准代码
CHECKING 校验中 正在校验标准代码
TREAT_FAILED 处理失败 处理失败,无法生成 output 文件
CHECK_FAILED 校验失败 校验失败,不满足期望的标准代码通过情况
graph LR
a([NORMAL]) -- 未处理 --> b([UNTREATED])
a([NORMAL]) -- 未校验 --> d([UNCHECK])
b([UNTREATED]) -- 未处理 --> b([UNTREATED])
b([UNTREATED]) -- 未校验 --> b([UNTREATED])
d([UNCHECK]) -- 未处理 --> b([UNTREATED])
d([UNCHECK]) -- 未校验 --> d([UNCHECK])
b([UNTREATED]) -- 执行处理 --> c([TREATING])
d([UNCHECK]) -- 执行处理 --> e([CHECKING])
c([TREATING]) -- 处理失败 --> f([TREAT_FAILED])
c([TREATING]) -- 处理成功 --> d([UNCHECK])
e([CHECKING]) -- 校验失败 --> g([CHECK_FAILED])
e([CHECKING]) -- 校验成功 --> a([NORMAL])
f([TREAT_FAILED]) -- 未处理 --> b([UNTREATED])
f([TREAT_FAILED]) -- 未校验 --> b([UNTREATED])
g([CHECK_FAILED]) -- 未处理 --> b([UNTREATED])
g([CHECK_FAILED]) -- 未校验 --> d([UNCHECK])
f([TREAT_FAILED]) -- 执行处理 --> c([TREATING])
g([CHECK_FAILED]) -- 执行处理 --> e([CHECKING])

问题功能

提交模块

提交属性

提交功能

判题模块

判题模块名词解释

Judge 机器属性

Judge 功能

存储设计

用户模块

MySQL

CREATE TABLE account
(
    id       BIGINT      AUTO_INCREMENT NOT NULL COMMENT '用户 ID',
    data_id  VARCHAR(32)                    NULL COMMENT '用户数据主键',
    handle   VARCHAR(64)                NOT NULL COMMENT '用户名',
    password VARCHAR(64)                NOT NULL COMMENT '用户密码',
    type     VARCHAR(64)                NOT NULL COMMENT '用户类型',
    PRIMARY KEY (`id`),
    UNIQUE KEY uk_handle(`handle`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mp4 COLLATION=utf8mb4_general_ci COMMENT '用户基本信息表';

Mongo

public class NormalUserData {

    /**
     * 数据库主键
     */
    @Id
    private String _id;

    /**
     * 所属组织
     */
    private OrganizationType organization;

    /**
     * 总提交数量
     */
    private Integer submission;

    /**
     * 总通过数量
     */
    private Integer accept;

    /**
     * 权限列表
     */
    private Set<PermissionType> permissionTypeSet;

    /**
     * 邮箱
     */
    private String email;
}

题目模块

MySQL

CREATE TABLE problem
(
    id                  BIGINT          AUTO_INCREMENT  NOT NULL COMMENT '题目 ID',
    data_id             VARCHAR(32)                         NULL COMMENT '题目数据主键',
    title               VARCHAR(256)                    NOT NULL COMMENT '题目的标题',
    status_type         VARCHAR(64)                     NOT NULL COMMENT '题目状态',
    access_type         VARCHAR(64)                     NOT NULL COMMENT '访问权限',
    submission_count    INT             DEFAULT 0       NOT NULL COMMENT '总提交数',
    accept_count        INT             DEFAULT 0       NOT NULL COMMENT '通过的提交数',
    owner               BIGINT                          NOT NULL COMMENT '题目拥有者',
    PRIMARY KEY (`id`),
    UNIQUE KEY uk_title(`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mp4 COLLATION=utf8mb4_general_ci COMMENT '题目基本信息表';

Mongo

public class ProblemData {

    @Id
    private String _id;

    /**
     * 首次关联至的比赛 ID
     */
    private Long contestId;

    /**
     * 默认的耗时限制,单位(ms)
     */
    private Integer defaultTimeLimit;

    /**
     * 默认的内存限制,单位(mb)
     */
    private Integer defaultMemoryLimit;

    /**
     * 部分语言的特殊时间限制
     */
    private Map<LanguageType, Integer> specialTimeLimit;

    /**
     * 部分语言的特殊内存限制
     */
    private Map<LanguageType, Integer> specialMemoryLimit;

    /**
     * 允许使用的语言类型
     */
    private List<LanguageType> supportLanguage;

    /**
     * 题目描述
     */
    private String description;

    /**
     * 输入描述
     */
    private String input;

    /**
     * 输出描述
     */
    private String output;

    /**
     * 输入输出样例
     */
    private List<Example> example;

    /**
     * 题目类型
     */
    private ProblemType type;

    /**
     * judge 模式
     */
    private JudgeCodeType judgeCodeType;

    /**
     * DIY judge code
     */
    private String judgeCode;

    /**
     * 测试样例的名称,允许给不同的样例准备不同的名称,输入为 XXX.in,输出为 XXX.out,交互题只有输入
     */
    private List<String> testName;

    /**
     * 是否允许下载样例
     */
    private Boolean shareTest;

    /**
     * 标准代码,必定为通过
     */
    private StdCode stdCode;

    /**
     * 测试代码(并不一定是通过,可能是故意错误的,但是一定有一份是通过的)
     */
    private List<StdCode> testCodeList;

    /**
     * 题目版本号
     */
    private Integer version;

    /**
     * 题目最后更新时间
     */
    private Date gmtModifyTime;

    @Data
    @Builder
    public static class Example {

        /**
         * 样例输入(原始文件格式 \n 表示换行)
         */
        private String in;

        /**
         * 样例输出(原始文件格式 \n 表示换行)
         */
        private String out;
    }

    @Data
    @Builder
    public static class StdCode {

        /**
         * 文件名
         */
        private String name;

        /**
         * 代码
         */
        private String code;

        /**
         * 期望结果
         */
        private SolutionStatusType expectResultType;

        /**
         * 所使用的语言
         */
        private LanguageType languageType;

        /**
         * 最终的测试 ID
         */
        private Long solutionId;

        public void reset() {
            this.solutionId = null;
        }
    }
}

提交模块

MySQL

CREATE TABLE solution
(
    id                  BIGINT          AUTO_INCREMENT  NOT NULL COMMENT '题目 ID',
    data_id             VARCHAR(32)                         NULL COMMENT '题目数据主键',
    submit_time         date                            NOT NULL COMMENT '题目的标题',
    user_id             BIGINT                              NULL COMMENT '提交的用户 ID',
    problem_id          BIGINT                          NOT NULL COMMENT '提交至的问题 ID',
    contest_id          BIGINT                              NULL COMMENT '提交至的比赛 ID',
    language_type       VARCHAR(64)                     NOT NULL COMMENT '提交的语言类型',
    status              VARCHAR(64)                     NOT NULL COMMENT '当前状态',
    solution_type       VARCHAR(64)                     NOT NULL COMMENT '提交的类型。公开的提交,在比赛中提交,系统测试程序',
    time_cost           INT                                 NULL COMMENT '耗时',
    memory_cost         INT                                 NULL COMMENT '内存消耗',           
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mp4 COLLATION=utf8mb4_general_ci COMMENT '提交基本信息';

Mongo

public class SolutionData {

    /**
     * Mongo ID
     */
    @Id
    private String _id;

    /**
     * 代码内容
     */
    private String code;

    /**
     * 编译信息
     */
    private String compileMsg;

    /**
     * 运行中 judge 反馈信息
     */
    private Map<String, String> runningMsg;

    /**
     * 运行的版本号
     */
    private Integer runOnVersion;
}

TCP 数据格式

Judge 和 Service 通过超长时间的 TCP 连接,并通过应答的方式,由 Judge 向 Service 发送请求,Service 应答。Judge 通过向 Service 发送注册指令在 Service 上完成注册,然后可以获取到一个唯一表示当前 Judge 机的编码,之后 Judge 可以使用多个新增 TCP 连接的方式实现建立多条 TCP 连接并绑定至同一个 Judge 上

通用格式

Request

BEGIN
${METHOD}
${DATA}
END

Response

${CODE}
${DATA}

格式说明

其中 DATA 数据使用分块传输编码的方式进行编码

每一个块都以一个该块包含的字节数(以十进制整数表示)开始,跟随一个 LF 换行符,然后是数据本身,最后以一个 LF 结束,且结束的 LF 不在字节数长度内。最后一个块必定是一个字节数为 0 的块,消息最后以 LF 结尾。即类似如下内容

${len1}
${data1}
${len2}
${data2}
0

将所有块的数据拼接后可以得到完整的数据内容,数据有三种格式,为前后端约定,分别为 单数据 简单数据复杂数据,同时规定在后续开发中,尽量避免使用复杂数据结构

单数据

${value}

直接显示值,此方式适用于返回值仅有一个值的情况,或者是仅有一个数组/集合的情况,仅用于返回值

简单数据

${key} ${value}

为一个 key 和一个 value,表示 key 的值是 value,中间使用空格分割,最后以 LF 作为结束符号。此时,value 部分长度不应该超过 200 个字符

复杂数据

${key} ${len}
${value}

首先是一个 keylen,表示这个 key 的值长度为 len,然后是一个 LF 换行符,再紧接着的是整个值的信息,最后以 LF 作为结束符号。通常用在简单数据中存在换行、空格的数据情况下使用,仅用于请求值

Response 返回时还会携带状态码,若状态码为非 0 则为错误

API 接口说明

标题即为请求调用的方法

Register

向目标 Service 注册本 Judge 机

请求参数

参数 描述 使用的数据类型
serverCode 目标服务器给出的随机服务串,用来认定 Judge 是来自可信任的人启动并连接至此的 简单数据
maxThreadCore 最大任务线程池大小 简单数据
maxSocketCore 最大通信线程池大小 简单数据
maxWorkCore 最大任务线程池大小 简单数据

返回信息

参数 描述 使用的数据类型
judgeCode 表示当前 Judge 的口令 单数据

Append

通知服务器新增加一个 TCP 连接,并与现有的 TCP 所指向的 Judge 绑定

请求参数

参数 描述 使用的数据类型
judgeCode 表示当前 Judge 的口令 简单数据

返回信息

无返回信息

Heartbeat

心跳数据包

请求参数

参数 描述 使用的数据类型
threadAccumulation 堆积的小任务数据量 简单数据
socketAccumulation 堆积的通信数据量 简单数据
workAccumulation 堆积的任务数据量 简单数据

返回信息

参数 描述 使用的数据类型
judgeTask 需要进行 judge 的编号,可重复出现,表示多个任务 简单数据
testTask 需要生成 test 数据的 problem 编号,可重复出现,表示多个任务 简单数据
threadCore 更新最大线程池数量,为更新至的值,可以为空 简单数据
socketCore 更新最大连接池数量,为更新至的值,可以为空 简单数据
workCore 更新最大任务线程池数量,为更新至的值,可以为空 简单数据
cleanProblem 需要删除的题目数据 简单数据

SolutionInfo

获取 Judge 任务的信息

请求参数

参数 描述 使用的数据类型
id 提交的 ID 简单数据

返回信息

参数 描述 使用的数据类型
problemId 提交对应的题目 ID 简单数据
language 使用的语言 简单数据
judgeName 使用的 judge 模式 简单数据
testNum 测试数据量 简单数据
timeLimit 时间限制 简单数据
memoryLimit 内存限制 简单数据

SolutionTest

获取评测 Solution 的下一组测试名称

请求参数

参数 描述 使用的数据类型
id 对应提交 ID 简单数据

返回信息

参数 描述 使用的数据类型
name 下一组测试数据名称 单数据

SolutionTestReport

提交评测 Solution 的信息

请求参数

参数 描述 使用的数据类型
value 对应运行的结果 简单数据
testName 运行的测试名称 简单数据
msg 对应运行产生的信息 复杂数据
id 对应提交 ID 简单数据
timeCost 对应提交的耗时 简单数据
memoryCost 对应提交的内存消耗 简单数据

返回信息

参数 描述 使用的数据类型
continue 是否结束评测 单数据

SolutionCompileMsgReport

提交评测 Solution 的编译信息

请求参数

参数 描述 使用的数据类型
id 对应提交 ID 简单数据
value 对应 code 编译是否成功的信息 简单数据
judge 对应 judge 编译是否成功的信息 简单数据
data 编译信息 复杂数据

返回信息

无返回信息

SolutionCode

提交的代码

请求参数

参数 描述 使用的数据类型
id 提交的 ID 简单数据

返回信息

参数 描述 使用的数据类型
code 提交的代码 单数据

StandardJudge

获取默认的 Judge 列表

请求参数

无请求参数

返回信息

参数 描述 使用的数据类型
judgeNameList 默认的 Judge 名称列表 单数据

StandardJudgeCode

获取对应的默认 Judge 代码

请求参数

参数 描述 使用的数据类型
name 默认的 Judge 名称 简单数据

返回信息

参数 描述 使用的数据类型
code 默认的 Judge 代码 单数据

ProblemJudgeCode

获取对应题目的自定义 Judge 代码

请求参数

参数 描述 使用的数据类型
id 题目 ID 简单数据

返回信息

参数 描述 使用的数据类型
code 对应题目的 DIY Judge 代码 单数据

ProblemDataIn

获取对应题目的输入文件

请求参数

参数 描述 使用的数据类型
id 题目 ID 简单数据
name 测试数据名称 简单数据

返回信息

参数 描述 使用的数据类型
test 对应题目的测试输入数据 单数据

ProblemDataOut

获取对应题目的输出文件

请求参数

参数 描述 使用的数据类型
id 题目 ID 简单数据
name 测试数据名称 简单数据

返回信息

参数 描述 使用的数据类型
test 对应题目的测试输出数据 单数据

TestInfo

获取 Test 任务的信息

请求参数

参数 描述 使用的数据类型
id 题目的 ID 简单数据

返回信息

参数 描述 使用的数据类型
language 使用的语言 简单数据
testNum 测试数据量 简单数据
timeLimit 时间限制 简单数据
memoryLimit 内存限制 简单数据

TestStdCode

获取生成测试的用的标准代码

请求参数

参数 描述 使用的数据类型
id 题目的 ID 简单数据

返回信息

参数 描述 使用的数据类型
code 标准代码 单数据

TestTest

获取评测 Test 的下一组测试名称

请求参数

参数 描述 使用的数据类型
id problem ID 简单数据

返回信息

参数 描述 使用的数据类型
name 下一组测试数据名称 单数据

TestReportOutput

上传输出文件

请求参数

参数 描述 使用的数据类型
id 题目的 ID 简单数据
flag 是否成功 简单数据
name 数据的名称 简单数据
pos 输出的偏移量 简单数据
value 输出的片段内容 复杂数据

返回信息

TestResultReport

通知服务器 stdCode 编译出现编译错误

请求参数

参数 描述 使用的数据类型
id 题目的 ID 简单数据

返回信息

TestReportOver

通知服务器,本题目的 test 生成已经成功完成

请求参数

参数 描述 使用的数据类型
id 题目的 ID 简单数据

返回信息