写点什么

CI/CD 流水线创建方法:Monad、Arrow 还是 Dart ?

  • 2021-02-18
  • 本文字数:8674 字

    阅读完需:约 28 分钟

CI/CD 流水线创建方法:Monad、Arrow 还是 Dart ?

本文将用三种方法来创建 CI/CD 流水线。Monad 不能对流水线进行静态分析,Arrow 语法很难用,我称之为 Dart(不知道它是否已经有名字了)的一种轻量级的 Arrow 方法可以像 Arrow 一样进行静态分析,但语法比 Monad 更简单。


我需要构建一个用于创建 CI/CD 流水线的系统。它起初是为了构建一个 CI 系统,测试 Github 上的 OCaml 项目(针对多个版本的 OCaml 编译器和多个操作系统,测试每个提交)。下面是一个简单的流水线,获取某个 Git 分支最新的提交,构建,并执行测试用例。


【译者注】CI/CD:持续集成(Continuous Integration)和持续部署(Continuous Deployment)简称,指在开发过程中自动执行一系列脚本来减低开发引入 bug 的概率,在新代码从开发到部署的过程中,尽量减少人工的介入。



这里的配色标识是:绿色的方框是已经完成,橙色的是正在进行,灰色的意味着这一步还不能开始。


这里有一个稍微复杂点的例子,它还下载了一个 Docker 基础镜像,使用两个不同版本的 OCaml 编译器并行构建提交,然后测试得到的镜像。红框表示此步骤失败:



一个更复杂的例子是测试项目本身,然后搜索依赖它的其他项目,并根据新版本测试这些项目:



在这里,圆圈意味着在检查反向依赖项之前,我们应该等待测试通过。


我们可以用 YAML 或类似的方法来描述这些管道,但这将是非常有限的。相反,我决定使用一种特定于领域的嵌入式语言,这样我们就可以免费使用宿主语言的特性(例如字符串操作、变量、函数、导入、类型检查等)。


最明显的方法是使每个框成为正则函数。然后上面的第一个例子可以是(这里,使用 OCaml 语法):


let example1 commit =  let src = fetch commit in  let image = build src in  test image
复制代码


第二个可能是:


let example2 commit =  let src = fetch commit in  let base = docker_pull "ocaml/opam2" in  let build ocaml_version =    let dockerfile = make_dockerfile ~base ~ocaml_version in    let image = build ~dockerfile src ~label:ocaml_version in    test image  in  build "4.07";  build "4.08"
复制代码


第三个可能是这样的:


let example3 commit =  let src = fetch commit in  let image = build src in  test image;  let revdeps = get_revdeps src in  List.iter example1 revdeps
复制代码


不过,我们想在语言中添加一些附加功能:


  • 管道步骤应尽可能并行运行。上面的 example2 函数将一次完成一个构建。

  • 管道步骤应在其输入更改时重新计算。e、 当我们作出新的承诺时,我们需要重建。

  • 用户应该能够查看每个步骤的进度。

  • 用户应该能够为任何步骤触发重建。

  • 我们应该能够从代码中自动生成图表,这样我们就可以在运行管道之前看到它将做什么。

  • 一步的失败不应该使整个管道停止。


对于这篇博客文章来说,确切的附加功能并不重要,因此为了简单起见,我将重点放在同时运行步骤上。

Monad 方法


【译者注】Monad:函子,单子,来自 Haskell 编程语言,是函数式编程中,一种定义将函数(函子)组合起来的结构方式,它除了返回值以外,还需要一个上下文。常见的 Monad 有计算任务,分支任务,或者 I/O 操作。


如果没有额外的功能,我们有如下功能:


val fetch : commit -> sourceval build : source -> image
复制代码


您可以将其理解为“build 是一个获取源值并返回(Docker)镜像的函数”。


这些函数很容易组合在一起,形成一个更大的函数来获取提交并构建它:


let fab c =  let src = fetch c in  build src
复制代码



我们还可以将其缩短为 build(fetch c)或 fetch c |>build。OCaml 中的|>(pipe)运算符只调用其右侧的函数,而参数在其左侧。


为了将这些函数扩展为并发的,我们可以让它们返回承诺,例如,


val fetch : commit -> source promiseval build : source -> image promise
复制代码


但是现在我们无法使用 let(或|>)轻松组合它们,因为 fetch 的输出类型与 build 的输入不匹配。


但是,我们可以定义一个类似的操作,let(或>>=)来处理承诺。它立即返回对最终结果的承诺,并在第一个承诺实现后调用 let* 的主体。那么我们有:


let fab c =  let* src = fetch c in  build src
复制代码


换句话说,通过在周围撒上几个星号字符,我们可以将简单的旧管道变成一个新的并发管道!使用 let* 编写 promise returning 函数的时间规则与使用 let 编写常规函数的时间规则完全相同,因此使用 promise 编写程序与编写常规程序一样简单。


仅仅使用 let *不会在我们的管道中添加任何并发(它只允许它与其他代码并发执行)。但是我们可以为此定义额外的函数,比如 all 一次计算一个列表中的每个承诺,或者使用 and 运算符指示两个事物应该并行运行:


除了处理承诺外,我们还可以为可能返回错误的函数(只有在第一个值成功时才调用 let 的主体)或为实时更新(每次输入更改时都调用主体)或为所有这些东西一起定义 let*。这是单子的基本概念。


这其实很管用。在 2016 年,我用这种方法做了 DataKitCI,它最初用于 Docker-for-Mac 上的 CI 系统。之后,Madhavapeddy 用它创建了 opam-repo-ci,这是 opam-repository 上的 CI 系统,OCaml 上主要的软件仓库。这将检查每个新的 PR 以查看它添加或修改了哪些包,针对多个 OCaml 编译器版本和 Linux 发行版(Debian、Ubuntu、Alpine、CentOS、Fedora 和 OpenSUSE)测试每个包,然后根据更改的包查找所有包的所有版本,并测试这些版本。


使用 monad 的主要问题是我们不能对管道进行静态分析。考虑上面的 example2 函数。在查询 GitHub 以获得测试提交之前,我们无法运行该函数,因此不知道它将做什么。一旦我们有了 commit,我们就可以调用 example2commit,但是在 fetch 和 docker_pull 操作完成之前,我们无法计算 let* 的主体来找出管道接下来将做什么。


换言之,我们只能绘制图表,显示已经执行或正在执行的管道位,并且必须使用和 * 手动指示并发的机会。

Arrow 方法


Arrow 使管道的静态分析成为可能。而不是我们的一元函数:


val fetch : commit -> source promiseval build : source -> image promise
复制代码


我们可以定义箭头类型:


type ('a, 'b) arrow
val fetch : (commit, source) arrowval build : (source, image) arrow
复制代码


a('a,'b)arrow 是一个接受 a 类型输入并生成 b 类型结果的管道。如果我们定义类型('a,'b)arrow='a->'b promise,则这与一元版本相同。但是,我们可以将箭头类型抽象化,并对其进行扩展,以存储我们需要的任何静态信息。例如,我们可以标记箭头:


type ('a, 'b) arrow = {  f : 'a -> 'b promise;  label : string;}
复制代码


这里,箭头是一个记录。f 是旧的一元函数,label 是“静态分析”。


用户看不到 arrow 类型的内部,必须使用 arrow 实现提供的函数来构建管道。有三个基本功能可用:


val arr : ('a -> 'b) -> ('a, 'b) arrowval ( >>> ) : ('a, 'b) arrow -> ('b, 'c) arrow -> ('a, 'c) arrowval first : ('a, 'b) arrow -> (('a * 'c), ('b * 'c)) arrow
复制代码


arr 接受纯函数并给出等价的箭头。对于我们的承诺示例,这意味着箭头返回已经实现的承诺。>>>把两个箭头连在一起。首先从“a”到“b”取一个箭头,改为成对使用。该对的第一个元素将由给定的箭头处理,第二个组件将原封不动地返回。


我们可以让这些操作自动创建带有适当 f 和 label 字段的新箭头。例如,在 a>>>b 中,结果 label 字段可以是字符串{a.label}>>{b.label}。这意味着我们可以显示管道,而不必先运行它,如果需要的话,我们可以很容易地用更结构化的内容替换 label。


我们的第一个例子是:


let example1 commit =  let src = fetch commit in  let image = build src in  test image
复制代码


to


let example1 =  fetch >>> build >>> test
复制代码


虽然我们不得不放弃变量名,但这似乎很令人愉快。但事情开始变得复杂,有了更大的例子。例如 2,我们需要定义几个标准组合:


(** Process the second component of a tuple, leaving the first unchanged. *)let second f =  let swap (x, y) = (y, x) in  arr swap >>> first f >>> arr swap
(** [f *** g] processes the first component of a pair with [f] and the second with [g]. *)let ( *** ) f g = first f >>> second g
(** [f &&& g] processes a single value with [f] and [g] in parallel and returns a pair with the results. *)let ( &&& ) f g = arr (fun x -> (x, x)) >>> (f *** g)
复制代码


Then, example2 changes from:


let example2 commit =  let src = fetch commit in  let base = docker_pull "ocaml/opam2" in  let build ocaml_version =    let dockerfile = make_dockerfile ~base ~ocaml_version in    let image = build ~dockerfile src ~label:ocaml_version in    test image  in  build "4.07";  build "4.08"
复制代码


to:


let example2 =  let build ocaml_version =    first (arr (fun base -> make_dockerfile ~base ~ocaml_version))    >>> build_with_dockerfile ~label:ocaml_version    >>> test  in  arr (fun c -> ((), c))  >>> (docker_pull "ocaml/opam2" *** fetch)  >>> (build "4.07" &&& build "4.08")  >>> arr (fun ((), ()) -> ())
复制代码


我们已经丢失了大多数变量名,而不得不使用元组,记住我们的值在哪里。这里有两个值并不是很糟糕,但是随着更多的值被添加并且我们开始嵌套元组,它变得非常困难。我们还失去了在 build~dockerfile src 中使用可选标记参数的能力,而是需要使用一个新操作,该操作接受 dockerfile 和源的元组。


假设现在运行测试需要从源代码获取测试用例。在原始代码中,我们只需使用:src 将测试图像更改为测试图像。在 arrow 版本中,我们需要在生成步骤之前复制源代码,使用带 dockerfile 的 first build_ 运行生成,并确保参数是新测试使用的正确方法。

Dart 方法


我开始怀疑是否有一种更简单的方法来实现与箭头相同的静态分析,但是没有无点语法,而且似乎有。考虑示例 1 的一元版本。我们有:


val build : source -> image promiseval test : image -> results promise
let example1 commit = let* src = fetch commit in let* image = build src in test image
复制代码


如果你不知道蒙娜兹的事,你还有别的办法。您可以定义 build 和 test,将 promises 作为输入,而不是使用 let* 等待获取完成,然后使用源调用 build:


val build : source promise -> image promiseval test : image promise -> results promise
复制代码


毕竟,fetching 给了你一个源代码承诺,你想要一个图像承诺,所以这看起来很自然。我们甚至可以以承诺为例。然后看起来是这样的:


let example1 commit =  let src = fetch commit in  let image = build src in  test image
复制代码


很好,因为它和我们刚开始的简单版本是一样的。问题是效率低下:


  • 我们用承诺的方式调用 example1(我们还不知道它是什么)。

  • 我们不必等待找出要测试的提交,而是调用 fetch,获取某个源的承诺。

  • 不需要等待获取源代码,我们就调用 build,获取图像的承诺。

  • 不用等待构建,我们调用 test,得到结果的承诺。


我们立即返回测试结果的最终承诺,但我们还没有做任何真正的工作。相反,我们建立了一长串的承诺,浪费了记忆。


但是,在这种情况下,我们希望执行静态分析。i、 我们想在内存中建立一些表示流水线的数据结构……这正是我们对 monad 的“低效”使用所产生的结果!


为了使其有用,我们需要基本操作(比如 fetch)来为静态分析提供一些信息(比如标签)。OCaml 的 let 语法没有为标签提供明显的位置,但是我能够定义一个运算符(let**),该运算符返回一个接受 label 参数的函数。它可用于生成如下基本操作:


let fetch commit =  "fetch" |>  let** commit = commit in  (* (standard monadic implementation of fetch goes here) *)
复制代码


因此,fetch 接受一个提交的承诺,对它执行一个单字节绑定以等待实际的提交,然后像以前一样继续,但它将绑定标记为一个 fetch 操作。如果 fetch 包含多个参数,则可以使用 and* 并行等待所有参数。


理论上,let**In fetch 的主体可以包含进一步的绑定。如果那样的话,我们在一开始就无法分析整个管道。但是,只要原语在开始时等待所有输入,并且不在内部进行任何绑定,我们就可以静态地发现整个管道。


我们可以选择是否将这些绑定操作公开给应用程序代码。如果 let*(或 let**)被公开,那么应用程序就可以使用 monad 的所有表达能力,但是在某些承诺解决之前,我们将无法显示整个管道。如果我们隐藏它们,那么应用程序只能生成静态管道。


到目前为止,我的方法是使用 let* 作为逃生舱口,这样就可以建造任何所需的管道,但我后来用更专业的操作来代替它的任何用途。例如,我添加了:


val list_map : ('a t -> 'b t) -> 'a list t -> 'b list t
复制代码


这将处理运行时才知道的列表中的每个项。然而,我们仍然可以静态地知道我们将应用于每个项的管道,即使我们不知道项本身是什么。list_map 本来可以使用 let* 实现,但这样我们就无法静态地看到管道。


下面是另外两个使用 dart 方法的示例:


let example2 commit =  let src = fetch commit in  let base = docker_pull "ocaml/opam2" in  let build ocaml_version =    let dockerfile =      let+ base = base in      make_dockerfile ~base ~ocaml_version in    let image = build ~dockerfile src ~label:ocaml_version in    test image  in  all [    build "4.07";    build "4.08"  ]
复制代码


与原来相比,我们有一个 all 来合并结果,并且在计算 dockerfile 时有一个额外的 let+base=base。let+ 只是 map 的另一种语法,在这里使用,因为我选择不更改 make_dockerfile 的签名。或者,我们可以让你的 dockerfile 接受一个基本图像的承诺,并在里面做地图。因为 map 需要一个纯实体(make_dockerfile 只生成一个字符串;没有承诺或错误),所以它不需要在图表上有自己的框,并且我们不会因为允许使用它而丢失任何东西。


let example3 commit =  let src = fetch commit in  let image = build src in  let ok = test image in  let revdeps = get_revdeps src in  gate revdeps ~on:ok |>  list_iter ~pp:Fmt.string example1
复制代码


这显示了另一个自定义操作:gate revdeps~on:ok 是一个承诺,只有在 revdeps 和 ok 都解决后才能解决。这将阻止它在库自己的测试通过之前测试库的 revdeps,即使如果我们希望它可以并行地这样做。而对于 monad,我们必须在需要的地方显式地启用并发(使用和 *),而对于 dart,我们必须在不需要的地方显式地禁用并发(使用 gate)。


我还添加了一个 list-iter 便利函数,并为它提供了一个漂亮的 printer 参数,这样一旦知道列表输入,我们就可以在图表中标记案例。


最后,虽然我说过不能在原语中使用 let*,但仍然可以使用其他一些 monad(它不会生成图)。实际上,在实际系统中,我对原语使用了一个单独的 let>操作符。这就要求主体使用底层 promise 库提供的非图生成承诺,因此不能在原语的主体中使用 let*(或 let>)。

和 Arrow 进行比较


给定一个“dart”,您可以通过定义例如。


type ('a, 'b) arrow = 'a promise -> 'b promise
复制代码


那么 arr 就是 map,f>>>g 就是有趣的 x->g(fx)。第一个也可以很容易地定义,假设你有某种函数来并行地做两件事(比如上面的和我们的)。


因此,dart API(即使有 let*hidden)仍然足以表示任何可以使用箭头 API 表示的管道。


Haskell 箭头教程 使用一个箭头是有状态函数的示例。例如,有一个 total 箭头,它返回它的输入和以前调用它的每个输入的总和。e、 g. 用输入 1 2 3 调用三次,产生输出 1 3 6。对输入序列运行管道将返回输出序列。


本教程使用 total 定义 mean1 函数,如下所示:


mean1 = (total &&& (arr (const 1) >>> total)) >>> arr (uncurry (/))
复制代码


因此,此管道复制每个输入编号,将第二个编号替换为 1,将两个流相加,然后用其比率替换每对。每次将另一个数字放入管道时,都会得到迄今为止输入的所有值的平均值。


使用 dart 样式的等效代码是(OCaml 使用 /。对于浮点除法):


let mean values =  let t = total values in  let n = total (const 1.0) in  map (uncurry (/.)) (pair t n)
复制代码


这对我来说更容易理解。通过定义标准运算符 let+(对于 map)和 +(对于 pair),我们可以稍微简化代码:


let (let+) x f = map f xlet (and+) = pair
let mean values = let+ t = total values and+ n = total (const 1.0) in t /. n
复制代码


无论如何,这不是一个很好的箭头示例,因为我们不使用一个状态函数的输出作为另一个状态函数的输入,所以这实际上只是一个简单的 applicative.


不过,我们可以很容易地用另一个有状态函数扩展示例管道,也许可以添加一些平滑处理。这看起来像箭头符号中的 mean1>>>平滑,省道符号中的值|>平均值|>平滑(或平滑(平均值))。


注意:Haskell 还有一个 Arrows 语法扩展,它允许 Haskell 代码编写为:


mean2 = proc value -> do    t <- total -< value    n <- total -< 1    returnA -< t / n
复制代码


这更像是飞镖符号。

更多示例


我在 ocurrent/ocurrent 上建立了一个使用这些思想的稍微扩展版本的库。子目录 lib_term 是与这篇博客文章相关的部分,在 TERM 中描述了各种组合词。


其他目录处理更具体的细节,例如与 Lwt promise 库的集成,提供管理 web UI 或 Cap’n Proto RPC 接口,以及带有用于使用 Git、GitHub、Docker 和 Slack 的原语的插件。


OCaml Docker 基础镜像构建


ocurrent/docker-base-images 包含一个管道,用于为各种 Linux 发行版、CPU 架构、OCaml 编译器版本和配置选项构建 OCaml 的 Docker 基本映像。例如,要在 Debian 10 上测试 OCAML4.09,可以执行以下操作:


$ docker run --rm -it ocurrent/opam:debian-10-ocaml-4.09
:~$ ocamlopt --version4.09.0
:~$ opam depext -i utop[...]
:~$ utop----+-------------------------------------------------------------+------------------ | Welcome to utop version 2.4.2 (using OCaml version 4.09.0)! | +-------------------------------------------------------------+
Type #utop_help for help about using utop.
-( 11:50:06 )-< command 0 >-------------------------------------------{ counter: 0 }-utop #
复制代码


以下是管道的外观(单击可查看完整尺寸)



它每周提取 opam 存储库的最新 Git 提交,然后为每个发行版构建包含该内容的基本映像和 opam 包管理器,然后为每个受支持的编译器变体构建一个映像。许多图片是建立在多个架构(amd64、arm32、arm64 和 ppc64)上的,并被推到 Docker Hub 的一个暂存区。然后,管道将所有散列组合起来,将一个多架构清单推送到 Docker Hub。还有一些别名(例如,debian 表示 debian-10-ocaml-4.09)。最后,如果有任何问题,则管道会将错误发送到松弛通道。


您可能想知道,我们是否真的需要一个管道来实现这一点,而不是从 cron 作业运行一个简单的脚本。但是拥有一个管道可以让我们在运行它之前看到管道将要做什么,观察管道的进度,单独重新启动失败的作业,等等,几乎与我们编写的代码相同。


如果你想看完成的流水线,可以阅读 pipeline.ml。


OCaml CI


ocurrent/ocaml-ci 是一个用于测试 OCaml 项目的(实验性的)GitHub 应用程序。管道获取应用程序的安装列表,获取每个安装的已配置存储库,获取每个存储库的分支和 PRs,然后针对多个 Linux 发行版和 OCaml 编译器版本测试每个存储库的头部。如果项目使用 ocamlformat,它还会检查提交的格式是否与 ocamlformat 的格式完全相同。



结果作为提交状态被推回到 GitHub,并记录在 web 和 tty ui 的本地索引中。这里有很多红色,主要是因为如果一个项目不支持特定版本的 OCaml,那么构建会被标记为失败,并在管道中显示为红色,尽管在生成 GitHub 状态报告时会过滤掉这些失败。我们可能需要一个新的颜色跳过阶段。

结论


编写 CI/CD 管道很方便,就好像它们是一次连续运行这些步骤并始终成功的单点脚本一样,然后只要稍作更改,管道就会在输入更改时运行这些步骤,同时提供日志记录、错误报告、取消和重建支持。


使用 monad 可以很容易地将任何程序转换为具有这些特性的程序,但是,与常规程序一样,在运行某些数据之前,我们不知道该程序将如何处理这些数据。特别是,我们只能自动生成显示已经开始的步骤的图表。


传统的静态分析方法是使用箭头。这比单元格稍微有限,因为流水线的结构不能根据输入数据而改变,尽管我们可以增加有限的灵活性,例如可选的步骤或两个分支之间的选择。但是,使用箭头符号编写管道是很困难的,因为我们必须使用无点样式(没有变量)编程。


通过以一种不寻常的方式使用 monad(这里称为“dart”),我们可以获得静态分析的相同好处。我们的函数不是接受纯值并返回包装值的函数,而是接受并返回包装值。这导致语法看起来与普通编程相同,但允许静态分析(代价是无法直接操作包装的值)。


如果我们隐藏(或不使用)monad 的 let*(bind)函数,那么我们创建的管道总是可以静态地确定的。如果我们使用绑定,那么管道中会有随着管道运行而扩展到更多管道阶段的孔。


基本步骤可以通过使用单个“标签绑定”创建,其中标签为原子组件提供静态分析。


我以前从未见过使用过这种模式(或者在 arrow 文档中提到过),它似乎提供了与 arrow 完全相同的好处,而且难度要小得多。如果这个名字是真的,告诉我!


这项工作由 OCaml 实验室资助。


原文链接:


https://roscidus.com/blog/blog/2019/11/14/cicd-pipelines/

2021-02-18 10:245278

评论

发布
暂无评论
发现更多内容

公安警务报警系统,二维码一键定位报警

t13823115967

二维码定位报警系统开发 微警务 二维码定位

盘点2020 | YourBatman 2020年感悟关键词:科比、裁员、管理层、活着

YourBatman

裁员 盘点2020 科比 管理层 活着

鸟枪换炮,利用python3对球员做大数据降维(因子分析得分),为C罗找到合格僚机

刘悦的技术博客

Python 数据分析 特征选择 降维

IPFS挖矿系统开发详情案例

系统开发咨询1357O98O718

IPFS云算力挖矿系统开发 IPFS算力挖矿软件系统开发

NoahTenet诺亚信条软件系统APP开发

系统开发

IPFS云算力挖矿系统开发详解案例及源码

系统开发咨询1357O98O718

云算力挖矿系统开发详解 云算力APP系统软件开发 云算力模式系统开发源码 云算力软件系统开发定制

CKLC挖矿矿机系统开发案例介绍

系统开发咨询1357O98O718

CKLC挖矿矿机系统软件开发 CKLC挖矿矿机系统开发 CKLC挖矿矿机APP系统开发

开设赌场的CTO | 法庭上的CTO(23)

赵新龙

CTO 法庭上的CTO

犯”集资诈骗罪“、二审判6年的CTO | 法庭上的CTO(21)

赵新龙

CTO 法庭上的CTO

母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

叫练

多线程与高并发 Wait lock 线程互斥 await

为了搞清楚类加载,竟然手撸JVM!

小傅哥

JVM 小傅哥 类加载 生命周期 加载机制

5G与4G的差别及应用

anyRTC开发者

人工智能 android AI 5G WebRTC

时空大数据与智能技术的时代共舞,百度地图给2020的答案

脑极体

Java多线程编程核心技术

田维常

多线程

分享一个普通程序员的“沪漂”六年的历程以及感想

程序员老猫

回忆录 经历 年终总结 沪漂 上海买房

被砍伤的技术VP | 法庭上的CTO(24)

赵新龙

CTO 法庭上的CTO

生产环境全链路压测建设历程 23:FAQ 3、4 适配改造,目标压力

数列科技杨德华

全链路压测 七日更

侵犯著作权、判刑两年半的 CTO |法庭上的CTO(22)

赵新龙

CTO 法庭上的CTO

总结2020:5个月出版两本书,日更公众号是一种怎样的体验?

冰河

程序员 程序人生 年终总结

Spring cloud Gateway(二) 一个Http请求的流程解析

Java 网关

互联网大厂有哪些分库分表的思路和技巧?

冰河

分布式数据库 分库分表 分布式存储 数据一致性 数据同步

散布消极言论被开除的总监 | 法庭上的CTO(25)

赵新龙

CTO 法庭上的CTO

智慧社区综合应用平台搭建,社区管理解决方案

t13823115967

智慧社区管理平台开发 智慧平安社区平台建设

AAAI 2021论文:利用深度元学习对城市销量进行预测(附论文下载)

京东科技开发者

数据库 大数据 时序预测

盘点2020 | 2021,Begin Again !

catcoolion

大前端 盘点2020

10次面试,2份offer —— 大龄程序员 2020 求职记录

escray

面试 架构师训练营第 1 期

IPFS挖矿矿机系统开发方案丨IPFS挖矿矿机源码案例

系统开发咨询1357O98O718

IPFS云算力挖矿系统开发 IPFS算力挖矿系统开发搭建

SpringCloudGateway(一) 概览

Java SpringcloudGateway

MySQL为Null会导致5个问题,个个致命!

王磊

MySQL MySQL使用

阿里面试:Mybatis中方法和SQL是怎么关联起来的呢?

田维常

mybatis

架构师训练营第一周作业

Mark

CI/CD 流水线创建方法:Monad、Arrow 还是 Dart ?_架构_Thomas Leonard_InfoQ精选文章