├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── shard.yml ├── spec ├── pond_spec.cr └── spec_helper.cr └── src ├── pond.cr └── pond ├── error.cr └── version.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | paths: 5 | - '**.cr' 6 | - '.github/workflows/test.yml' 7 | pull_request: 8 | branches: [master] 9 | paths: 10 | - '**.cr' 11 | - '.github/workflows/test.yml' 12 | schedule: 13 | - cron: '0 7 * * 6' 14 | jobs: 15 | checks: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | crystal: ['1.3.2'] 20 | runs-on: ubuntu-latest 21 | continue-on-error: false 22 | steps: 23 | - name: Download source 24 | uses: actions/checkout@v3 25 | - name: Install Crystal 26 | uses: crystal-lang/install-crystal@v1 27 | with: 28 | crystal: ${{ matrix.crystal }} 29 | - name: Install shards 30 | run: shards install 31 | - name: Lint code 32 | run: ./bin/ameba 33 | specs: 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | crystal: ['1.0.0', latest] 38 | experimental: [false] 39 | include: 40 | - crystal: nightly 41 | experimental: true 42 | runs-on: ubuntu-latest 43 | continue-on-error: ${{ matrix.experimental }} 44 | steps: 45 | - name: Download source 46 | uses: actions/checkout@v3 47 | - name: Install Crystal 48 | uses: crystal-lang/install-crystal@v1 49 | with: 50 | crystal: ${{ matrix.crystal }} 51 | - name: Cache shards 52 | uses: actions/cache@v3 53 | with: 54 | path: ~/.cache/shards 55 | key: ${{ runner.os }}-shards-${{ hashFiles('shard.yml') }} 56 | restore-keys: ${{ runner.os }}-shards- 57 | - name: Install shards 58 | run: shards update 59 | - name: Run tests 60 | run: crystal spec --error-on-warnings -Dpreview_mt 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.shards/ 2 | /.vscode/ 3 | /bin/ 4 | /lib/ 5 | /tmp/ 6 | *.dwarf 7 | .env* 8 | /.crystal-version 9 | /*.lock 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.0.0] - 2024-04-16 9 | 10 | ### Added 11 | - Add `Pond.drain(&)` class method 12 | 13 | ### Changed 14 | - Replace internal fiber collection with an internal counter 15 | 16 | ### Fixed 17 | - Reduce memory footprint for long running processes 18 | 19 | ### Removed 20 | - Remove `Pond#<<` method 21 | - Remove `Pond.drain(Fiber)` class method 22 | - Remove `Pond.drain(Array(Fiber))` class method 23 | - Remove `Pond#fill(Fiber)` overload 24 | - Remove `Pond#fill(Array(Fiber))` overload 25 | 26 | ## [1.0.1] - 2024-02-14 27 | 28 | ### Fixed 29 | - Replace `Fiber.yield` with `sleep 1.microsecond` in loops to reduce CPU load 30 | 31 | ## [1.0.0] - 2023-05-29 32 | 33 | ### Added 34 | - First stable release 35 | 36 | ## [0.3.5] - 2023-03-06 37 | 38 | ### Changed 39 | - Update GitHub actions 40 | - Change argument type of `Pond.drain(Array(Fiber))` to `Indexable(Fiber)` 41 | 42 | ## [0.3.4] - 2022-03-17 43 | 44 | ### Added 45 | - Ensure support for *Crystal* v1.3.0 46 | 47 | ## [0.3.3] - 2021-12-25 48 | 49 | ### Added 50 | - Add support for *Crystal* v1.2 51 | 52 | ### Fixed 53 | - Fix segmentation fault while reading fibers collection 54 | - Clear *Pond* when all fibers are dead 55 | - Check fiber is alive before adding to pond 56 | 57 | ## [0.3.2] - 2021-06-28 58 | 59 | ### Fixed 60 | - Fix [segfault][segfault] while removing dead fibers 61 | 62 | [segfault]: https://github.com/GrottoPress/mel/runs/2932323566?check_suite_focus=true 63 | 64 | ## [0.3.1] - 2021-06-21 65 | 66 | ### Added 67 | - Add `Pond#fill` overload that accepts an array of fibers 68 | 69 | ### Fixed 70 | - Ensure dead fibers continue to be removed whenever pond is refilled 71 | 72 | ## [0.3.0] - 2021-06-18 73 | 74 | ### Changed 75 | - Allow refilling and draining a drained pond 76 | 77 | ### Added 78 | - Add `Pond#size` that returns number of fibers in a pond 79 | 80 | ### Changed 81 | - Replace travis-ci with GitHub actions 82 | 83 | ## [0.2.0] - 2021-03-31 84 | 85 | ### Added 86 | - Add `Pond.drain` class methods 87 | 88 | ## [0.1.0] - 2021-03-25 89 | 90 | ### Added 91 | - Initial public release 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present N Atta Kusi Adusei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pond 2 | 3 | **Pond** is a *Crystal* implementation of a *WaitGroup*, without channels or explicit counters. *Pond* automatically keeps track of all its fibers, and waits until all of them complete execution. 4 | 5 | ## Installation 6 | 7 | 1. Add the dependency to your `shard.yml`: 8 | 9 | ```yaml 10 | dependencies: 11 | pond: 12 | github: GrottoPress/pond 13 | ``` 14 | 15 | 1. Run `shards install` 16 | 17 | ## Usage 18 | 19 | - Spawn fibers and wait on them: 20 | 21 | ```crystal 22 | require "pond" 23 | 24 | pond = Pond.new 25 | 26 | 1000.times do |_| 27 | pond.fill { do_work } # <= Spawns fiber and passes block to it 28 | end 29 | 30 | pond.drain # <= Waits for fibers to complete 31 | ``` 32 | 33 | The code above is the same as: 34 | 35 | ```crystal 36 | require "pond" 37 | 38 | Pond.drain do |pond| 39 | 1000.times do |_| 40 | pond.fill { do_work } 41 | end 42 | end # <= Drains pond automatically at the end of the block 43 | ``` 44 | 45 | - You may spawn *nested* fibers: 46 | 47 | In this case, all *ancestor* fibers have to be added to the pond, otherwise *Pond* can't guarantee any of them would complete. 48 | 49 | ```crystal 50 | require "pond" 51 | 52 | pond = Pond.new 53 | 54 | pond.fill do 55 | pond.fill do 56 | pond.fill { do_work } 57 | end 58 | end 59 | 60 | pond.drain 61 | ``` 62 | 63 | Note that, while you can fill a pond that was created in a another fiber, draining has to be done in the same fiber the pond was created in. This is to prevent potential deadlocks. 64 | 65 | ```crystal 66 | require "pond" 67 | 68 | pond = Pond.new 69 | 70 | pond.fill { do_work } 71 | 72 | spawn { pond.drain } # <= Error! 73 | ```` 74 | 75 | ## Development 76 | 77 | Run tests with `crystal spec -Dpreview_mt`. You may set `CRYSTAL_WORKERS` environment variable with `export CRYSTAL_WORKERS=`, before running tests. 78 | 79 | ## Contributing 80 | 81 | 1. [Fork it](https://github.com/GrottoPress/pond/fork) 82 | 1. Switch to the `master` branch: `git checkout master` 83 | 1. Create your feature branch: `git checkout -b my-new-feature` 84 | 1. Make your changes, updating changelog and documentation as appropriate. 85 | 1. Commit your changes: `git commit` 86 | 1. Push to the branch: `git push origin my-new-feature` 87 | 1. Submit a new *Pull Request* against the `GrottoPress:master` branch. 88 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: pond 2 | version: 2.0.0 3 | description: Crystal WaitGroups without channels or counters 4 | 5 | authors: 6 | - GrottoPress 7 | - N Atta Kusi Adusei 8 | 9 | license: MIT 10 | 11 | crystal: ~> 1.0 12 | 13 | development_dependencies: 14 | ameba: 15 | github: crystal-ameba/ameba 16 | version: ~> 0.14.3 17 | -------------------------------------------------------------------------------- /spec/pond_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Pond do 4 | describe "#drain" do 5 | it "waits for fibers to complete" do 6 | n = 1000 7 | pond = Pond.new 8 | count = Atomic(Int32).new(0) 9 | 10 | n.times do |_| 11 | pond.fill { count.add(1) } 12 | end 13 | 14 | pond.fill do 15 | pond.fill do 16 | pond.fill { count.add(1) } 17 | end 18 | end 19 | 20 | pond.drain 21 | pond.fill { count.add(1) } 22 | pond.drain 23 | 24 | count.lazy_get.should eq(n + 2) 25 | end 26 | 27 | it "raises when drained from another fiber" do 28 | Pond.drain do |pond| 29 | pond.fill do 30 | expect_raises(Pond::Error) { pond.drain } 31 | end 32 | end 33 | end 34 | 35 | it "works for empty pond" do 36 | Pond.new.drain.should be_nil 37 | end 38 | 39 | it "can be called consecutively in same fiber" do 40 | pond = Pond.new 41 | 42 | pond.fill { } 43 | 44 | pond.drain 45 | pond.drain 46 | 47 | pond.drain.should be_nil 48 | end 49 | end 50 | 51 | describe "#size" do 52 | it "returns number of fibers in the pond" do 53 | pond = Pond.new 54 | 55 | 10.times do |_| 56 | pond.fill { } 57 | end 58 | 59 | pond.fill do 60 | pond.fill do 61 | pond.fill { } 62 | end 63 | end 64 | 65 | pond.size.should_not eq(0) 66 | pond.drain 67 | pond.size.should eq(0) 68 | 69 | 10.times do |_| 70 | pond.fill { } 71 | end 72 | 73 | pond.size.should_not eq(0) 74 | pond.drain 75 | pond.size.should eq(0) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | 3 | require "../src/pond" 4 | -------------------------------------------------------------------------------- /src/pond.cr: -------------------------------------------------------------------------------- 1 | require "./pond/version" 2 | require "./pond/**" 3 | 4 | class Pond 5 | def initialize 6 | @counter = Atomic(Int32).new(0) 7 | @fiber = Fiber.current 8 | end 9 | 10 | def fill(name = nil, same_thread = nil, &block) 11 | @counter.add(1) 12 | 13 | spawn(name: name, same_thread: same_thread) do 14 | block.call 15 | ensure 16 | @counter.sub(1) 17 | end 18 | end 19 | 20 | def drain : Nil 21 | ensure_same_fiber 22 | 23 | until size == 0 24 | sleep 1.microsecond 25 | end 26 | end 27 | 28 | def size : Int32 29 | @counter.get 30 | end 31 | 32 | def self.drain 33 | yield pond = new 34 | pond.drain 35 | end 36 | 37 | private def ensure_same_fiber 38 | return if @fiber == Fiber.current 39 | raise Error.new("Cannot drain pond from another fiber") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/pond/error.cr: -------------------------------------------------------------------------------- 1 | class Pond::Error < Exception 2 | end 3 | -------------------------------------------------------------------------------- /src/pond/version.cr: -------------------------------------------------------------------------------- 1 | class Pond 2 | VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify }} 3 | end 4 | --------------------------------------------------------------------------------