From b56b95f7b7f4ff7280a34a3f8a3b0501fc500c91 Mon Sep 17 00:00:00 2001 From: yushan Date: Wed, 1 Jun 2022 15:50:07 +0800 Subject: [PATCH] fix: move website docs to new repo --- .github/workflows/gh-pages.yml | 32 -- .gitmodules | 3 - README.md | 18 +- docs/about/index.md | 3 - docs/archetypes/default.md | 6 - docs/config.toml | 85 --- docs/content/_index.md | 75 --- docs/content/docs/advance/_index.md | 5 - docs/content/docs/advance/configuration.md | 80 --- docs/content/docs/design/_index.md | 5 - docs/content/docs/design/design.v1.md | 77 --- docs/content/docs/example/_index.md | 5 - .../content/docs/example/terminal_cli/gui.png | Bin 38554 -> 0 bytes .../docs/example/terminal_cli/index.md | 22 - docs/content/docs/quick_start/_index.md | 5 - docs/content/docs/quick_start/prepare.md | 44 -- docs/content/docs/quick_start/quick_start.md | 46 -- docs/content/posts/_index.md | 7 - docs/content/posts/bugs.md | 13 - docs/content/posts/worker_pool.md | 532 ------------------ ...s_50fc8c04e12a2f59027287995557ceff.content | 1 - ...scss_50fc8c04e12a2f59027287995557ceff.json | 1 - docs/themes/hugo-book | 1 - 23 files changed, 7 insertions(+), 1059 deletions(-) delete mode 100644 .github/workflows/gh-pages.yml delete mode 100644 .gitmodules delete mode 100644 docs/about/index.md delete mode 100644 docs/archetypes/default.md delete mode 100644 docs/config.toml delete mode 100644 docs/content/_index.md delete mode 100644 docs/content/docs/advance/_index.md delete mode 100644 docs/content/docs/advance/configuration.md delete mode 100644 docs/content/docs/design/_index.md delete mode 100644 docs/content/docs/design/design.v1.md delete mode 100644 docs/content/docs/example/_index.md delete mode 100644 docs/content/docs/example/terminal_cli/gui.png delete mode 100644 docs/content/docs/example/terminal_cli/index.md delete mode 100644 docs/content/docs/quick_start/_index.md delete mode 100644 docs/content/docs/quick_start/prepare.md delete mode 100644 docs/content/docs/quick_start/quick_start.md delete mode 100644 docs/content/posts/_index.md delete mode 100644 docs/content/posts/bugs.md delete mode 100644 docs/content/posts/worker_pool.md delete mode 100644 docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.content delete mode 100644 docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.json delete mode 160000 docs/themes/hugo-book diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml deleted file mode 100644 index 64c650c..0000000 --- a/.github/workflows/gh-pages.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: github pages - -on: - push: - pull_request: - branches: - - main - -jobs: - deploy: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - with: - submodules: true # Fetch Hugo themes (true OR recursive) - fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - - - name: Setup Hugo - uses: peaceiris/actions-hugo@v2 - with: - hugo-version: 'latest' - extended: true - - - name: Build - run: hugo --gc --minify --buildFuture --source ./docs - - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/main' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/public diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 47029a0..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/themes/hugo-book"] - path = docs/themes/hugo-book - url = https://github.com/alex-shpak/hugo-book diff --git a/README.md b/README.md index d9b2def..e7ce2d9 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ # goim -> Instant Messaging Server written by golang. +![GoIM](https://go-goim.github.io/images/logo.png) -[![goliangci-lint](https://github.com/yusank/goim/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/yusank/goim/actions/workflows/golangci-lint.yml) -[![github pages](https://github.com/yusank/goim/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/yusank/goim/actions/workflows/gh-pages.yml) -[![Semgrep](https://github.com/yusank/goim/actions/workflows/semgrep.yml/badge.svg)](https://github.com/yusank/goim/actions/workflows/semgrep.yml) +Instant Messaging Server written by golang. -> 选型参考:[https://zhuanlan.zhihu.com/p/31377253](https://zhuanlan.zhihu.com/p/31377253) - -预期能力: +[![golangci-lint](https://github.com/go-goim/goim/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/go-goim/goim/actions/workflows/golangci-lint.yml) +[![CodeQL](https://github.com/go-goim/goim/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/go-goim/goim/actions/workflows/codeql-analysis.yml) +[![Semgrep](https://github.com/go-goim/goim/actions/workflows/semgrep.yml/badge.svg)](https://github.com/go-goim/goim/actions/workflows/semgrep.yml) -![design](./static/images/goim.png) - -目前消息流转: +> 选型参考:[https://zhuanlan.zhihu.com/p/31377253](https://zhuanlan.zhihu.com/p/31377253) -![msg](./static/images/send_rec_msg.png) \ No newline at end of file +详细文档请在官网查看:[https://go-goim.github.io](https://go-goim.github.io/) \ No newline at end of file diff --git a/docs/about/index.md b/docs/about/index.md deleted file mode 100644 index a9407bb..0000000 --- a/docs/about/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# about - -GoIM is instant messaging system written by Go. diff --git a/docs/archetypes/default.md b/docs/archetypes/default.md deleted file mode 100644 index 00e77bd..0000000 --- a/docs/archetypes/default.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "{{ replace .Name "-" " " | title }}" -date: {{ .Date }} -draft: true ---- - diff --git a/docs/config.toml b/docs/config.toml deleted file mode 100644 index d204b35..0000000 --- a/docs/config.toml +++ /dev/null @@ -1,85 +0,0 @@ -baseURL = 'http://yusank.github.io/goim/' -title = 'GoIM' -theme = "hugo-book" -# (Optional) Set this to true if you use capital letters in file names -disablePathToLower = true -# (Optional) Set this to true to enable 'Last Modified by' date and git author -# information on 'doc' type pages. -enableGitInfo = true - -# Needed for mermaid/katex shortcodes -[markup] -[markup.goldmark.renderer] - unsafe = true - -[markup.tableOfContents] - startLevel = 1 - endLevel = 4 - - -# (Optional) Theme is intended for documentation use, therefore it doesn't render taxonomy. -# You can remove related files with config below - -[languages] -[languages.en] - languageName = '中文' - contentDir = 'content' - weight = 1 - -[params] - # (Optional, default light) Sets color theme: light, dark or auto. - # Theme 'auto' switches between dark and light modes based on browser/os preferences - BookTheme = 'auto' - - # (Optional, default true) Controls table of contents visibility on right side of pages. - # Start and end levels can be controlled with markup.tableOfContents setting. - # You can also specify this parameter per page in front matter. - BookToC = true - -# BookMenuBundle = '/menu' - - # (Optional, default docs) Specify section of content to render as menu - # You can also set value to "*" to render all sections to menu - BookSection = 'docs' - - # Set source repository location. - # Used for 'Last Modified' and 'Edit this page' links. - BookRepo = 'https://github.com/yusank/goim' - - # Specifies commit portion of the link to the page's last modified commit hash for 'doc' page - # type. - # Required if 'BookRepo' param is set. - # Value used to construct a URL consisting of BookRepo/BookCommitPath/ - # Github uses 'commit', Bitbucket uses 'commits' - # BookCommitPath = 'commit' - - # Enable 'Edit this page' links for 'doc' page type. - # Disabled by default. Uncomment to enable. Requires 'BookRepo' param. - # Path must point to the site directory. - BookEditPath = 'edit/main/docs' - - # (Optional, default January 2, 2006) Configure the date format used on the pages - # - In git information - # - In blog posts - BookDateFormat = 'Jan 2, 2006' - - # (Optional, default true) Enables search function with flexsearch, - # Index is built on fly, therefore it might slowdown your website. - # Configuration for indexing can be adjusted in i18n folder per language. - BookSearch = true - - # (Optional, default true) Enables comments template on pages - # By default partials/docs/comments.html includes Disqus template - # See https://gohugo.io/content-management/comments/#configure-disqus - # Can be overwritten by same param in page frontmatter - BookComments = true - - # /!\ This is an experimental feature, might be removed or changed at any time - # (Optional, experimental, default false) Enables portable links and link checks in markdown pages. - # Portable links meant to work with text editors and let you write markdown without {{< relref >}} shortcode - # Theme will print warning if page referenced in markdown does not exists. - BookPortableLinks = true - - # /!\ This is an experimental feature, might be removed or changed at any time - # (Optional, experimental, default false) Enables service worker that caches visited pages and resources for offline use. - BookServiceWorker = true diff --git a/docs/content/_index.md b/docs/content/_index.md deleted file mode 100644 index a74731e..0000000 --- a/docs/content/_index.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Introduction -type: docs ---- - -> Instant Messaging system written by Go. - -## How to run - -```shell -# run push server -make run Srv=push -# run gateway server -make run Srv=gateway -# run msg server -make run Srv=msg -``` - -## Design of GoIM - -### 整体能力规划 - -![design](https://raw.githubusercontent.com/yusank/goim/main/static/images/goim.png) - -#### 客户端如何查找和连接长连接服务 - -客户端如何连接长连接服务,目前我有两个方案各有优缺点,但是还没确定。 - -#### 反向代理方案 - -客户端统一入口在 gateway 上,gateway 支持反向代理能力,客户端发起长连接请求时,代理到后端的服务(这里准备使用一致性哈希来确定转发到哪台机器上) - -优点: - -- 入口统一,且可以在 gateway 上完成鉴权等操作 -- 后端服务无需暴露 ip,且可任意扩缩容比较安全 - -缺点: - -- gateway 需要承受长连接带来的压力,需要更多的 gateway 来承受大量在线用户的情况 - -#### httpdns 方案 - -客户端先通过暴露的域名,去访问 httpdns 服务获取真正后端服务的 ip,然后通过 ip 直接进行长连接 - -优点: - -- 客户端与长连接服务器直连,减少代理层的压力 - -缺点: - -- 要求暴露后端服务 ip,安全性降低且比较浪费 ip 资源 - -##### 纯 httpdns - -![ws](https://raw.githubusercontent.com/yusank/goim/main/static/images/conn_ws_dns.png) - -##### 结合 gateway - -![gateway](https://raw.githubusercontent.com/yusank/goim/main/static/images/conn_ws_gateway.png) - -#### 结论 - -最终决定,使用基于 gateway 作为第一入口,再返回长链接服务的方案. -原因如下: - -1. 可以在 gateway 这一层做初步的校验和分配长链接服务的策略(比如按最小连接数,id 哈希等) -2. 反向代理会使系统更复杂且上层反向代理会有比较大的压力,项目初期不想搞太复杂 -3. 对于客户端来说 gateway 就是一切了,之后要加的用户体系都是通过 gateway 暴露出来,入口可以比较收拢. - -### 消息的流转 - -IM 数据将在 HBASE 上存储,关系型数据存在 MySQL - -![msg](https://raw.githubusercontent.com/yusank/goim/main/static/images/send_rec_msg.png) diff --git a/docs/content/docs/advance/_index.md b/docs/content/docs/advance/_index.md deleted file mode 100644 index ea0e46f..0000000 --- a/docs/content/docs/advance/_index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -weight: 2 -bookFlatSection: true -title: "高级特性" ---- \ No newline at end of file diff --git a/docs/content/docs/advance/configuration.md b/docs/content/docs/advance/configuration.md deleted file mode 100644 index 99ffa40..0000000 --- a/docs/content/docs/advance/configuration.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -weight: 1 ---- - -# Configuration - -配置为两份文件分别为 service config 和 registry config - -- service config 关注服务启停以及声明周期中需要的各类配置 -- registry config 关注服务注册相关配置 - -## server config definition - -```proto -// Service 为一个服务的全部配置 -message Service { - string name = 1; - string version = 2; - optional Server http = 3; - optional Server grpc = 4; - Log log = 5; - map metadata = 6; - Redis redis = 7; - MQ mq = 8; -} - -message Server { - string scheme = 1; - string addr = 2; - int32 port = 3; -} - - -enum Level { - DEBUG = 0; - INFO = 1; - WARING = 2; - ERROR = 3; - FATAL = 4; -} - -message Log { - optional string log_path = 1; - repeated Level level = 2; -} - -message Redis { - string addr = 1; - string password = 2; - int32 max_conns = 3; - int32 min_idle_conns = 4; - google.protobuf.Duration dial_timeout = 5; - google.protobuf.Duration idle_timeout = 6; -} - -message MQ { - repeated string addr = 1; - int32 max_retry = 2; -} -``` - -## registry config definition - -```proto -message RegistryInfo { - repeated string addr = 1; - string scheme = 2; - google.protobuf.Duration dial_timeout_sec = 3; - google.protobuf.Duration dial_keep_alive_time_sec = 4; - google.protobuf.Duration dial_keep_alive_timeout_sec = 5; -} - -message Registry { - string name = 1; - oneof reg { - RegistryInfo consul = 2; - RegistryInfo etcd = 3; - } -} -``` diff --git a/docs/content/docs/design/_index.md b/docs/content/docs/design/_index.md deleted file mode 100644 index 641573b..0000000 --- a/docs/content/docs/design/_index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -weight: 2 -bookFlatSection: true -title: "设计" ---- \ No newline at end of file diff --git a/docs/content/docs/design/design.v1.md b/docs/content/docs/design/design.v1.md deleted file mode 100644 index 850a347..0000000 --- a/docs/content/docs/design/design.v1.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -weight: 1 ---- - -# Design of version1 - -> 关于下个大版本的设计稿 - -## 准备工作 - -- 提升现有的系统稳定性 -- 确保系统日志已接入,可回溯问题原因 -- 抽象长链接部分,确保后期可替换可测试 -- 完善测试流程和测试用例 - -## 目标 - -- 支持用户关系,设计用户信息与用户关系 -- 完善消息协议 -- 支持消息持久化 -- 支持 Prometheus 和 open tracing -- **支持写扩散、读扩散** - -## 非目标 - -- admin 能力可以添加但是不重要 - -## 核心流程 - -1. 提升稳定性 -2. 设计用户相关表设计 -3. 设计消息持久化数据结构 -4. 添加用户管理相关能力,增删改查用户关系等 -5. 完善消息协议,补充必要信息 -6. 支持写扩散,读扩散并进行压测 - -### 提升稳定性 - -- 添加更完善的测试场景和测试用例,一次全量测试用例跑完后,能确保功能可用的状态。 -- 提供一键部署能力,最好是通过 docker-compose 的方式部署各个组件和 Service - -### 用户体系 - -- 用户登录状态 -- 用户鉴权 -- 用户关系的变更 - -### 消息持久化 - -- 通过单独一个组件对所有消息进行持久化到 HBASE -- mq 中的消息信息需要更详细 -- 提供查询历史消息的能力 - -### 用户管理 - -> optional - -- 提供用户管理能力,方便测试 debug - -### 消息协议的完善 - -- 根据进度补充信息 - -### 读写扩散 - -- 列出详细的对比、使用场景 -- 根据需求实现读写扩散 - -### 监控 - -> optional - -- 添加 Prometheus, open tracing,方便监控和追踪问题 - -## 其他 - -- 应把 connection 抽象出来,并支持 tcp 方式的长链接 diff --git a/docs/content/docs/example/_index.md b/docs/content/docs/example/_index.md deleted file mode 100644 index 7e925ba..0000000 --- a/docs/content/docs/example/_index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -weight: 3 -bookFlatSection: true -title: "使用示例" ---- \ No newline at end of file diff --git a/docs/content/docs/example/terminal_cli/gui.png b/docs/content/docs/example/terminal_cli/gui.png deleted file mode 100644 index 6fcdc23d3f283506610b04967ffb3880f315a2b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38554 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 关于部署 rocketmq:docker 部署 rocketmq 过程中遇到过一些问题,如果你有疑问可以参考这篇文章 [Docker 部署 RocketMQ](https://yusank.space/posts/rocketmq-deploy/) - -### config - -msg service 为例,`apps/msg/config/config.yaml`: - - name: goim.msg.service - version: v0.0.0 - grpc: - scheme: grpc - port: 18063 - log: - level: - - INFO - - DEBUG - metadata: - grpcSrv: yes - redis: - addr: 127.0.0.1:6379 - mq: - addr: - - 127.0.0.1:9876 - -`apps/msg/config/registry.yaml` : - - consul: - addr: - - 127.0.0.1:8500 - scheme: http - -根据自己的环境去修改各个组件的地址和端口。 diff --git a/docs/content/docs/quick_start/quick_start.md b/docs/content/docs/quick_start/quick_start.md deleted file mode 100644 index 89f5324..0000000 --- a/docs/content/docs/quick_start/quick_start.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -weight: 1 ---- - -# Quick Start - -## run - -```shell -# run msg service -$ make run Srv=msg -# run gateway service -$ make run Srv=gateway -# run push service -$ make run Srv=push -``` - -## other make command - -```shell -make help - -Usage: - make - -Development - vet Run go vet against code. - lint Run go lint against code. - test Run test against code. - -Generate - protoc Run protoc command to generate pb code. - -Build - build build provided server - build-all build all apps - -Docker - docker-build build docker image - -Run - run run provided server - -General - help Display this help. -``` diff --git a/docs/content/posts/_index.md b/docs/content/posts/_index.md deleted file mode 100644 index abfb8c4..0000000 --- a/docs/content/posts/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -menu: - after: - name: blog - weight: 5 -title: Blog ---- diff --git a/docs/content/posts/bugs.md b/docs/content/posts/bugs.md deleted file mode 100644 index 7c11cb6..0000000 --- a/docs/content/posts/bugs.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -weight: 1 -bookFlatSection: true -title: "解决bug" -date: 2022-03-31 -tags: - - "bugs" -categories: - - "Development" - - "bugs" ---- - -> 记录开过过程中遇到的问题以及解决过程。 diff --git a/docs/content/posts/worker_pool.md b/docs/content/posts/worker_pool.md deleted file mode 100644 index 7b29619..0000000 --- a/docs/content/posts/worker_pool.md +++ /dev/null @@ -1,532 +0,0 @@ ---- -weight: 2 -bookFlatSection: true -title: "实现异步并发 worker 队列" -date: 2022-04-01 -tags: - - "go" - - "golang" - - "worker" - - "pool" -categories: - - "Development" - - "golang" ---- - -> 记录实现一个异步并发 worker 队列的过程。 - -在开发 broadcast 功能的时候,碰到一个比较棘手的问题,需要并发执行多个 worker 来讲 broadcast 消息推送到所有在线用户,同时我希望能控制并发数量。 - - - -## 前言 - -以往遇到类似的问题我都会借助 `sync.WaitGroup` 加 `channel` 的方式去做,实现方式也比较简单。大致思路如下: - -```go -type LimitedWaitGroup struct { - wg *sync.WaitGroup - ch chan int -} - -func NewLimitedWaitGroup(size int) *LimitedWaitGroup { - return &LimitedWaitGroup{ - wg : new(sync.WaitGroup), - ch : make(chan int, size) - } -} - -func (w *LimitedWaitGroup) Add(f func()) { - // wait if channel is full - w.ch <- 1 - w.wg.Add(1) - go func() { - defer w.done() - f() - }() -} - -func (w *LimitedWaitGroup) done() { - <-w.ch - w.wg.Done() -} - -func (w *LimitedWaitGroup) Wait() { - w.wg.Wait() -} -``` - -这样能解决我大部分的简单需求,但是现在我想要的能力用这个简单的 `LimitedWaitGroup` 无法完全满足,所以重新设计了一个 `worker pool` 的概念来满足我现在以及以后类似的需求。 - -## 设计 - -### 需求整理 - -首先将目前想到的需求以及其优先级列出来: - -**高优先级:** - -1. worker pool 支持设置 size,防止 worker 无限增多 -2. 任务并发执行且能指定并发数 -3. 当 worker 达到上线时,新的任务在一定范围内支持排队等待(即 `limited queue`) -4. 支持捕获任务错误 -5. 排队中的任务应该按顺序调度执行 - -**低优先级:** - -1. 任务支持实时状态更新 -2. 任务可以外部等待完成(类似 `waitGroup.Done()` ) -3. 当空闲 worker 小于指定并发数时,支持占用空闲 worker 部分运行(如当前剩余 3 个 worker 可用,但是新的任务需要 5 个并发,则尝试先占用这 3 个worker,并在运行过程中继续监听 pool 空闲出来的 worker 并尝试去占用) - -{{< hint info >}} -**小结** -列出完需求及其优先级后,经过考虑决定,高优先级除了`第五条`, 低优先级除了`第三条`, 其他需求都在目前版本里实现。 - -原因如下: - -- 首先说低优先级第三条,这块的部分调度执行 worker,目前没有想好比较优雅的实现方式,所以暂时没有实现(但是下个版本会实现) -- 高优先级的第五条也是跟调度有点关系,如果队列里靠前的任务需要大量的 worker,那很容易造成阻塞,后面的 task 一直没办法执行,即便需要很少的 worker。所以等部分调度执行开发完再把任务按需执行打开。 - -{{< /hint >}} - -### Task Definition - -`task` 表示一次任务,包含了任务执行的方法,并发数,所属的 `workerSet`以及执行状态等。 - -```go -type TaskFunc func() error - -type task struct { - tf TaskFunc // task function - concurrence int // concurrence of task - ws *workerSet // assign value after task distribute to worker. - status TaskStatus // store task status. -} - -// TaskStatus is the status of task. -type TaskStatus int -``` - -### Worker Definition - -`worker` 作为最小调度单元,仅包含 `workerSet` 和 `error` . - -```go -type worker struct { - ws *workerSet - err error -} -``` - -### TaskResult Definition - -`TaskResult` 是一个对外暴露的 `interface`, 用于外部调用者获取和管理任务执行状态信息。 - -```go -// TaskResult is a manager of submitted task. -type TaskResult interface { - // get error if task failed. - Err() error - // wait for task done. - Wait() - // get task status. - Status() TaskStatus - // kill task. - Kill() -} -``` - -{{< hint warning >}} -**`task` 和 `TaskStatus` 分别实现 `TaskResult` 的接口,从而外部统一拿到 `TaskResult`** - -> 之所以 `TaskStatus` 也需要实现 `TaskResult` 是因为部分情况下,不需要创建 `task` 直接返回错误状态即可。如: -> 提交的任务的并发数过高(超过 pool 的 size),当前 queue 已满不能再处理任何其他任务了,这种情况直接返回对应的状态码。 -{{< /hint >}} - -### WorkerSet Definition - -`workerSet` 为一组 `worker`的集合,作用是调度 `worker` 并维护起所属 `task` 的整个生命过程. - -```go -// workerSet represent a group of task handle workers -type workerSet struct { - task *task - runningWorker atomic.Int32 - workers []*worker - ctx context.Context - cancel context.CancelFunc - wg *sync.WaitGroup -} -``` - -### WorkerPool Definition - -`Pool` 是一个可指定 size 的 worker pool. 可并发运行多个 task 并且支持额外的任务排队能力。 - -```go -// Pool is a buffered worker pool -type Pool struct { - // TODO: taskQueue should be a linked list, so that we can get the task from the head of the list and put it back to the head. - // If we use a channel as taskQueue, we can't get the task from the head of the list and put it back to the head. - // But make sure that before change it to linked list, we should have the ability run the task in min(taskQueue length, concurrence) goroutines. - taskQueue chan *task - enqueuedTaskCount atomic.Int32 // count of unhandled tasks - bufferSize int // size of taskQueue buffer, means can count of bufferSize task can wait to be handled - maxWorker int // count of how many worker run in concurrence - workerSets []*workerSet - lock *sync.Mutex - stopFlag atomic.Bool -} -``` - -## 实现 - -上面已经确定需要的能力和基础的数据结构了,下面一个个去实现各个模块的能力。 - -### Worker Implement - -`worker` 能力相对纯粹,看看 worker 是如何工作的: - -```go -func (w *worker) run() { - defer w.ws.done() - - var ec = make(chan error, 1) - defer close(ec) - go func() { - ec <- w.ws.task.tf() - }() - - select { - case e := <-ec: - w.err = e - case <-w.ws.ctx.Done(): - } -} -``` - -### WorkerSet Implement - -`workerSet` 调度 worker,记录 worker 运行状态等。 - -{{< details "点击展开" "...">}} - -```go - -func newWorkerSet(ctx context.Context, t *task) *workerSet { - // 初始化参数 - // ... 省略代码 - return ws -} - -func (ws *workerSet) run() { - ws.task.updateStatus(TaskStatusRunning) - for _, w := range ws.workers { - ws.addOne() - go w.run() - } -} - -func (ws *workerSet) stopAll() { - ws.cancel() - ws.task.updateStatus(TaskStatusKilled) -} - -// err returns the first error that occurred in the workerSet. -func (ws *workerSet) err() error { - // ...省略代码 - return nil -} - -func (ws *workerSet) getRunningWorker() int { - return int(ws.runningWorker.Load()) -} - -// done called when worker stop. -func (ws *workerSet) done() { - ws.addRunningWorker(-1) - ws.wg.Done() - if ws.getRunningWorker() == 0 { - ws.task.updateStatus(TaskStatusDone) - } -} - -// addOne called when worker start running. -func (ws *workerSet) addOne() { - ws.addRunningWorker(1) - ws.wg.Add(1) -} - -func (ws *workerSet) wait() { - ws.wg.Wait() -} -``` - -{{< /details >}} - -### Task Implement - -`task` 主要是记录 task 的状态,并通过 workerSet 控制其下的 worker. - -{{< details "点击展开" "...">}} - -```go -func newTask(tf TaskFunc, concurrence int) *task { - return &task{ - tf: tf, - concurrence: concurrence, - } -} - -// Err returns the first error that occurred in the workerSet. -func (t *task) Err() error { - // check t.ws if nil return nil. - if t.ws == nil { - return nil - } - - return t.ws.err() -} - -// Wait for task done. -// Please make sure task is done or running before call this function. -func (t *task) Wait() { - // check t.ws if nil. - if t.ws == nil { - return - } - - t.ws.wait() -} - -// Status returns task status. -func (t *task) Status() TaskStatus { - return t.status -} - -// Kill task. -func (t *task) Kill() { - // check t.ws if nil. - if t.ws == nil { - return - } - - t.ws.stopAll() -} - -func (t *task) assignWorkerSet(ws *workerSet) { - t.ws = ws -} - -func (t *task) updateStatus(status TaskStatus) { - t.status = status -} -``` - -{{< /details >}} - -### TaskStatus Implement - -`TaskStatus` 虽然实现了 `TaskResult` 接口,但是不能控制任何 task,其有效的方法只有 `Status()` 和 `Err()` - -```go -func (t TaskStatus) Error() string { - return t.String() -} - -func (t TaskStatus) Err() error { - switch t { - case TaskStatusError, TaskStatusQueueFull, TaskStatusTooManyWorker, TaskStatusPoolClosed, TaskStatusKilled: - return t - } - - return nil -} - -func (t TaskStatus) Wait() { - // do nothing. -} - -func (t TaskStatus) Status() TaskStatus { - return t -} - -func (t TaskStatus) Kill() { - // do nothing. -} -``` - -### Pool Implement - -`Pool` 是总的入口,任务会提交到 `Pool`, 并由 `Pool` 创建 task 并调度到 `workerSet` 上,同时定时清理已完成的 `workerSet`, -确保空闲 `worker` 能被合理使用。 - -{{< details "点击展开" "...">}} - -```go -func NewPool(workerSize, queueSize int) *Pool { - // ... 初始化各个参数 - - // check p.enqueue to find out why make this channel size with p.bufferSize+1. - // - p.taskQueue = make(chan *task, p.bufferSize+1) - // 启动单独 goroutine 维护队列 - go p.consumeQueue() - return p -} - -func (p *Pool) Submit(ctx context.Context, tf TaskFunc, concurrence int) TaskResult { - if p.stopFlag.Load() { - return TaskStatusPoolClosed - } - - if concurrence > p.maxWorker { - return TaskStatusTooManyWorker - } - - // check if there has any worker place left - p.lock.Lock() - defer p.lock.Unlock() - - t := newTask(tf, concurrence) - if p.tryRunTask(ctx, t) { - return t - } - - if p.enqueueTask(t, true) { - return t - } - - return TaskStatusQueueFull -} - -func (p *Pool) Stop() { - // 关闭队列和正在运行的 workerSet -} - -// tryRunTask try to put task into workerSet and run it.Return false if capacity not enough. -// Make sure get p.Lock before call this func -func (p *Pool) tryRunTask(ctx context.Context, t *task) bool { - if p.curRunningWorkerNum()+t.concurrence <= p.maxWorker { - ws := newWorkerSet(ctx, t) - p.workerSets = append(p.workerSets, ws) - // run 为异步方法 - ws.run() - return true - } - - return false -} - -// curRunningWorkerNum get current running worker num -// make sure lock mutex before call this func -func (p *Pool) curRunningWorkerNum() int { - // ...省略代码 - return cnt -} - -// enqueueTask put task to queue. -// p.enqueuedTaskCount increase 1 if is new task -func (p *Pool) enqueueTask(t *task, isNewTask bool) bool { - // ... 省略代码 - return true -} - -func (p *Pool) consumeQueue() { - var ticker = time.NewTicker(time.Second) - for { - select { - case t, ok := <-p.taskQueue: - if !ok { - // channel closed - return - } - if p.tryRunTask(context.Background(), t) { - p.enqueuedTaskCount.Sub(1) - goto unlock - } - // if enqueueTask return false, means channel is closed. - if !p.enqueueTask(t, false) { - // channel is closed - goto unlock - } - - unlock: - p.lock.Unlock() - case <-ticker.C: - log.Printf("current running worker num: %d", p.curRunningWorkerNum()) - } - } - - // never reach here -} -``` - -{{< /details >}} - -## 使用 - -到这里相关开发基本结束了,有一些 `TODO` 项后面后补充完善,下面通过 test case 来看一下如何使用这个 worker pool: - - -```go - -func TestPool_SubmitOrEnqueue(t *testing.T) { - p := NewPool(5, 1) - var ( - cnt int - concurrence = 5 - ) - - tf := func() error { - time.Sleep(time.Second) - log.Println("hello world") - cnt++ - return nil - } - - got := p.Submit(context.Background(), tf, concurrence) - if got.Status() != TaskStatusRunning { - t.Errorf("SubmitOrEnqueue() = %v, want %v", got.Status(), TaskStatusRunning) - return - } - got.Wait() - if cnt != concurrence { - t.Errorf("cnt = %v, want %v", cnt, concurrence) - } - if got := p.Submit(context.Background(), tf, concurrence); got.Status() != TaskStatusRunning { - t.Errorf("SubmitOrEnqueue() = %v, want %v", got, TaskStatusRunning) - return - } - if got := p.Submit(context.Background(), tf, concurrence); got.Status() != TaskStatusEnqueue { - t.Errorf("SubmitOrEnqueue() = %v, want %v", got.Status(), TaskStatusEnqueue) - return - } - if got := p.Submit(context.Background(), tf, 6); got.Status() != TaskStatusTooManyWorker { - t.Errorf("SubmitOrEnqueue() = %v, want %v", got.Status(), TaskStatusTooManyWorker) - return - } - if got := p.Submit(context.Background(), tf, concurrence); got.Status() != TaskStatusQueueFull { - t.Errorf("SubmitOrEnqueue() = %v, want %v", got.Status(), TaskStatusQueueFull) - return - } - p.Stop() - if got := p.Submit(context.Background(), tf, 1); got.Status() != TaskStatusPoolClosed { - t.Errorf("SubmitOrEnqueue() = %v, want %v", got.Status(), TaskStatusPoolClosed) - return - } -} -``` - -## 总结 - -到这里这篇文章内容全部结束了,下面做一个简单的总结: - -- 介绍背景和需求 -- 根据需求定义了一组概念:`task`, `worker`, `workerSet`, `pool` -- 各个结构之前的关系以及如何实现 -- 最终给出使用的 test case. - -## 链接 🔗 - -**如果想仔细阅读源码,并持续关注这块功能的后续更新优化,请点击这里跳转到 [GitHub](https://github.com/yusank/goim/tree/main/pkg/worker).** diff --git a/docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.content b/docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.content deleted file mode 100644 index ed65056..0000000 --- a/docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.content +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";:root{--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-link:#0055bb;--color-visited-link:#8440f1;--body-background:white;--body-font-color:black;--icon-filter:none;--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}@media(prefers-color-scheme:dark){:root{--gray-100:rgba(255, 255, 255, 0.1);--gray-200:rgba(255, 255, 255, 0.2);--gray-500:rgba(255, 255, 255, 0.5);--color-link:#84b2ff;--color-visited-link:#b88dff;--body-background:#343a40;--body-font-color:#e9ecef;--icon-filter:brightness(0) invert(1);--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}}/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.flex{display:flex}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}.clearfix::after{content:"";display:table;clear:both}html{font-size:16px;scroll-behavior:smooth;touch-action:manipulation}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background);letter-spacing:.33px;font-weight:400;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box}body *{box-sizing:inherit}h1,h2,h3,h4,h5{font-weight:400}a{text-decoration:none;color:var(--color-link)}img{vertical-align:baseline}:focus{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}aside nav ul{padding:0;margin:0;list-style:none}aside nav ul li{margin:1em 0;position:relative}aside nav ul a{display:block}aside nav ul a:hover{opacity:.5}aside nav ul ul{padding-inline-start:1rem}ul.pagination{display:flex;justify-content:center;list-style-type:none}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}.book-brand{margin-top:0;margin-bottom:1rem}.book-brand img{height:1.5em;width:1.5em;margin-inline-end:.5rem}.book-menu{flex:0 0 16rem;font-size:.875rem}.book-menu .book-menu-content{width:16rem;padding:1rem;background:var(--body-background);position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;cursor:pointer;word-wrap:break-word}.book-menu a.active{color:var(--color-link)}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label+ul{display:block}.book-menu input.toggle+label::after{content:"▸"}.book-menu input.toggle:checked+label::after{content:"▾"}body[dir=rtl] .book-menu input.toggle+label::after{content:"◂"}body[dir=rtl] .book-menu input.toggle:checked+label::after{content:"▾"}.book-section-flat{margin:2rem 0}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:3rem}.book-header{display:none;margin-bottom:1rem}.book-header label{line-height:0}.book-header img.book-icon{height:1.5em;width:1.5em}.book-search{position:relative;margin:1rem 0;border-bottom:1px solid transparent}.book-search input{width:100%;padding:.5rem;border:0;border-radius:.25rem;background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:.75rem}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:.875rem}.book-footer img{height:1em;width:1em;margin-inline-end:.5rem}.book-comments{margin-top:1rem}.book-languages{margin-block-end:2rem}.book-languages .book-icon{height:1em;width:1em;margin-inline-end:.5em}.book-languages ul{padding-inline-start:1.5em}.book-menu-content,.book-toc-content,.book-page,.book-header aside,.markdown{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){#menu-control,#toc-control{display:inline}.book-menu{visibility:hidden;margin-inline-start:-16rem;font-size:16px;z-index:1}.book-toc{display:none}.book-header{display:block}#menu-control:focus~main label[for=menu-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:absolute;top:0;bottom:0;left:0;right:0}#toc-control:focus~main label[for=toc-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@font-face{font-family:roboto;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-regular.woff2)format("woff2"),url(fonts/roboto-v27-latin-regular.woff)format("woff")}@font-face{font-family:roboto;font-style:normal;font-weight:700;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-700.woff2)format("woff2"),url(fonts/roboto-v27-latin-700.woff)format("woff")}@font-face{font-family:roboto mono;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-mono-v13-latin-regular.woff2)format("woff2"),url(fonts/roboto-mono-v13-latin-regular.woff)format("woff")}body{font-family:roboto,sans-serif}code{font-family:roboto mono,monospace}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:400;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;vertical-align:middle;text-decoration:none}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus{opacity:initial}.markdown h4,.markdown h5,.markdown h6{font-weight:bolder}.markdown h5{font-size:.875em}.markdown h6{font-size:.75em}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a:hover{text-decoration:underline}.markdown a:visited{color:var(--color-visited-link)}.markdown img{max-width:100%;height:auto}.markdown code{padding:0 .25rem;background:var(--gray-200);border-radius:.25rem;font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border-radius:.25rem;overflow-x:auto}.markdown pre code{padding:0;background:0 0}.markdown p{word-wrap:break-word}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:.25rem}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200)}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;border:1px solid var(--gray-200);border-radius:.25rem}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer}.markdown details[open] summary{margin-bottom:0}.markdown figure{margin:1rem 0}.markdown figure figcaption p{margin-top:0}.markdown-inner>:first-child{margin-top:0}.markdown-inner>:last-child{margin-bottom:0}.markdown .book-expand{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden}.markdown .book-expand .book-expand-head{background:var(--gray-100);padding:.5rem 1rem;cursor:pointer}.markdown .book-expand .book-expand-content{display:none;padding:1rem}.markdown .book-expand input[type=checkbox]:checked+.book-expand-content{display:block}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden;display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-tabs input[type=radio]:focus+label{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}.markdown .book-columns{margin-left:-1rem;margin-right:-1rem}.markdown .book-columns>div{margin:1rem 0;min-width:10rem;padding:0 1rem}.markdown a.book-btn{display:inline-block;font-size:.875rem;color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:.25rem;cursor:pointer}.markdown a.book-btn:hover{text-decoration:none}.markdown .book-hint.info{border-color:#6bf;background-color:rgba(102,187,255,.1)}.markdown .book-hint.warning{border-color:#fd6;background-color:rgba(255,221,102,.1)}.markdown .book-hint.danger{border-color:#f66;background-color:rgba(255,102,102,.1)} \ No newline at end of file diff --git a/docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.json b/docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.json deleted file mode 100644 index 383eb23..0000000 --- a/docs/resources/_gen/assets/scss/goim/book.scss_50fc8c04e12a2f59027287995557ceff.json +++ /dev/null @@ -1 +0,0 @@ -{"Target":"book.min.97cfda4f5e3c9fa49a2bf8d401f4ddc0eec576c99cdcf6afbec19173200c37db.css","MediaType":"text/css","Data":{"Integrity":"sha256-l8/aT148n6SaK/jUAfTdwO7Fdsmc3PavvsGRcyAMN9s="}} \ No newline at end of file diff --git a/docs/themes/hugo-book b/docs/themes/hugo-book deleted file mode 160000 index 98d19b8..0000000 --- a/docs/themes/hugo-book +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 98d19b8e95019534622fd4c5eae707423730df2c