├── .pdd ├── .gitignore ├── renovate.json ├── .0pdd.yml ├── .github └── workflows │ ├── xcop.yml │ ├── reuse.yml │ ├── typos.yml │ ├── pdd.yml │ ├── yamllint.yml │ ├── copyrights.yml │ ├── markdown-lint.yml │ ├── actionlint.yml │ ├── codecov.yml │ └── rake.yml ├── Gemfile ├── .rubocop.yml ├── .rultor.yml ├── REUSE.toml ├── Rakefile ├── LICENSE.txt ├── LICENSES └── MIT.txt ├── syncem.gemspec ├── lib └── syncem.rb ├── Gemfile.lock ├── README.md ├── test └── test_syncem.rb └── logo.svg /.pdd: -------------------------------------------------------------------------------- 1 | --source=. 2 | --verbose 3 | --rule min-words:20 4 | --rule min-estimate:15 5 | --rule max-estimate:90 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | .DS_Store 3 | .idea/ 4 | .yardoc/ 5 | *.gem 6 | coverage/ 7 | doc/ 8 | node_modules/ 9 | rdoc/ 10 | vendor/ 11 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.0pdd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | errors: 5 | - yegor256@gmail.com 6 | # alerts: 7 | # github: 8 | # - yegor256 9 | 10 | tags: 11 | - pdd 12 | - bug 13 | -------------------------------------------------------------------------------- /.github/workflows/xcop.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: xcop 6 | 'on': 7 | push: 8 | pull_request: 9 | jobs: 10 | xcop: 11 | timeout-minutes: 15 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: g4s8/xcop-action@master 16 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: reuse 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | reuse: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: fsfe/reuse-action@v5 20 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: typos 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | typos: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: crate-ci/typos@v1.32.0 20 | -------------------------------------------------------------------------------- /.github/workflows/pdd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: pdd 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | pdd: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: volodya-lombrozo/pdd-action@master 20 | -------------------------------------------------------------------------------- /.github/workflows/yamllint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: yamllint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | yamllint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: ibiqlik/action-yamllint@v3 20 | -------------------------------------------------------------------------------- /.github/workflows/copyrights.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: copyrights 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | copyrights: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: yegor256/copyrights-action@0.0.8 20 | -------------------------------------------------------------------------------- /.github/workflows/markdown-lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: markdown-lint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | markdown-lint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: DavidAnson/markdownlint-cli2-action@v20.0.0 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko 4 | # SPDX-License-Identifier: MIT 5 | 6 | source 'https://rubygems.org' 7 | gemspec 8 | 9 | gem 'minitest', '~>5.25', require: false 10 | gem 'rake', '~>13.2', require: false 11 | gem 'rdoc', '~>6.11', require: false 12 | gem 'rubocop', '~>1.71', require: false 13 | gem 'rubocop-minitest', '~>0.38', require: false 14 | gem 'rubocop-performance', '~>1.25', require: false 15 | gem 'rubocop-rake', '~>0.7', require: false 16 | gem 'threads', '~>0.4', require: false 17 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | AllCops: 5 | DisplayCopNames: true 6 | TargetRubyVersion: 2.3 7 | SuggestExtensions: false 8 | NewCops: enable 9 | plugins: 10 | - rubocop-rake 11 | - rubocop-minitest 12 | - rubocop-performance 13 | Minitest/EmptyLineBeforeAssertionMethods: 14 | Enabled: false 15 | Layout/EndOfLine: 16 | EnforcedStyle: lf 17 | Metrics/MethodLength: 18 | Max: 50 19 | Layout/ParameterAlignment: 20 | Enabled: false 21 | Style/OptionalBooleanParameter: 22 | Enabled: false 23 | Metrics/CyclomaticComplexity: 24 | Max: 10 25 | Metrics/PerceivedComplexity: 26 | Max: 10 27 | Metrics/AbcSize: 28 | Max: 25 29 | require: [] 30 | -------------------------------------------------------------------------------- /.rultor.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | docker: 6 | image: yegor256/ruby 7 | assets: 8 | rubygems.yml: yegor256/home#assets/rubygems.yml 9 | install: | 10 | sudo gem install pdd 11 | pdd -f /dev/null 12 | sudo bundle install --no-color "--gemfile=$(pwd)/Gemfile" 13 | release: 14 | pre: false 15 | script: |- 16 | bundle exec rake 17 | rm -rf *.gem 18 | sed -i "s/0\.0\.0/${tag}/g" syncem.gemspec 19 | git add syncem.gemspec 20 | git commit -m "Version set to ${tag}" 21 | gem build syncem.gemspec 22 | chmod 0600 ../rubygems.yml 23 | gem push *.gem --config-file ../rubygems.yml 24 | merge: 25 | script: |- 26 | bundle exec rake 27 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: actionlint 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | actionlint: 15 | timeout-minutes: 15 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Download actionlint 20 | id: get_actionlint 21 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 22 | shell: bash 23 | - name: Check workflow files 24 | run: ${{ steps.get_actionlint.outputs.executable }} -color 25 | shell: bash 26 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: codecov 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | jobs: 11 | codecov: 12 | timeout-minutes: 15 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 3.3 19 | bundler-cache: true 20 | - run: bundle config set --global path "$(pwd)/vendor/bundle" 21 | - run: bundle install --no-color 22 | - run: bundle exec rake 23 | - uses: codecov/codecov-action@v5 24 | with: 25 | files: coverage/.resultset.json 26 | fail_ci_if_error: true 27 | -------------------------------------------------------------------------------- /.github/workflows/rake.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | --- 4 | # yamllint disable rule:line-length 5 | name: rake 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-24.04, macos-15] 18 | ruby: [3.3] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - run: bundle config set --global path "$(pwd)/vendor/bundle" 27 | - run: bundle install --no-color 28 | - run: bundle exec rake 29 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko 2 | # SPDX-License-Identifier: MIT 3 | 4 | version = 1 5 | [[annotations]] 6 | path = [ 7 | ".DS_Store", 8 | ".gitattributes", 9 | ".gitignore", 10 | ".pdd", 11 | "**.json", 12 | "**.md", 13 | "**.svg", 14 | "**.txt", 15 | "**/.DS_Store", 16 | "**/.gitignore", 17 | "**/.pdd", 18 | "**/*.csv", 19 | "**/*.jpg", 20 | "**/*.json", 21 | "**/*.md", 22 | "**/*.pdf", 23 | "**/*.png", 24 | "**/*.svg", 25 | "**/*.txt", 26 | "**/*.vm", 27 | "**/CNAME", 28 | "**/Gemfile.lock", 29 | "Gemfile.lock", 30 | "README.md", 31 | "renovate.json", 32 | ] 33 | precedence = "override" 34 | SPDX-FileCopyrightText = "Copyright (c) 2025 Yegor Bugayenko" 35 | SPDX-License-Identifier = "MIT" 36 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko 4 | # SPDX-License-Identifier: MIT 5 | 6 | require 'rubygems' 7 | require 'rake' 8 | require 'rake/clean' 9 | 10 | CLEAN = FileList['coverage'] 11 | 12 | def name 13 | @name ||= File.basename(Dir['*.gemspec'].first, '.*') 14 | end 15 | 16 | def version 17 | Gem::Specification.load(Dir['*.gemspec'].first).version 18 | end 19 | 20 | task default: %i[clean test rubocop] 21 | 22 | require 'rake/testtask' 23 | desc 'Run all unit tests' 24 | Rake::TestTask.new(:test) do |test| 25 | test.libs << 'lib' << 'test' 26 | test.pattern = 'test/**/test_*.rb' 27 | test.verbose = false 28 | end 29 | 30 | require 'rdoc/task' 31 | RDoc::Task.new do |rdoc| 32 | rdoc.main = 'README.md' 33 | rdoc.rdoc_dir = 'rdoc' 34 | rdoc.rdoc_files.include('README.md', 'lib/**/*.rb') 35 | end 36 | 37 | require 'rubocop/rake_task' 38 | desc 'Run RuboCop on all directories' 39 | RuboCop::RakeTask.new(:rubocop) do |task| 40 | task.fail_on_error = true 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Yegor Bugayenko 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 13 | in 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 NON-INFRINGEMENT. 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Yegor Bugayenko 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 13 | in 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 NON-INFRINGEMENT. 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /syncem.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko 4 | # SPDX-License-Identifier: MIT 5 | 6 | require 'English' 7 | Gem::Specification.new do |s| 8 | s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version= 9 | s.required_ruby_version = '>=2.3' 10 | s.name = 'syncem' 11 | s.version = '0.0.0' 12 | s.license = 'MIT' 13 | s.summary = 'Thread-safe decorator of Ruby objects' 14 | s.description = 'Sometimes you have an object that is not thread-safe, 15 | but you need to make sure each of its methods is thread-safe, because they 16 | deal with some resources, like files or databases and you want them to 17 | manage those resources sequentially. This small gem will help you achieve 18 | exactly that without any re-design of the objects you already have. Just 19 | decorate them with SyncEm decorator and that is it.' 20 | s.authors = ['Yegor Bugayenko'] 21 | s.email = 'yegor256@gmail.com' 22 | s.homepage = 'https://github.com/yegor256/syncem' 23 | s.files = `git ls-files | grep -v -E '^(test/|\\.|renovate)'`.split($RS) 24 | s.rdoc_options = ['--charset=UTF-8'] 25 | s.extra_rdoc_files = ['README.md'] 26 | s.metadata['rubygems_mfa_required'] = 'true' 27 | end 28 | -------------------------------------------------------------------------------- /lib/syncem.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko 4 | # SPDX-License-Identifier: MIT 5 | 6 | # SyncEm is a simple decorator of an existing object that makes all its 7 | # methods thread-safe. 8 | # 9 | # For more information read 10 | # {README}[https://github.com/yegor256/syncem/blob/master/README.md] file. 11 | # 12 | # Author:: Yegor Bugayenko (yegor256@gmail.com) 13 | # Copyright:: Copyright (c) 2018-2025 Yegor Bugayenko 14 | # License:: MIT 15 | class SyncEm 16 | undef_method :send 17 | 18 | def initialize(origin) 19 | @origin = origin 20 | @mutex = Mutex.new 21 | end 22 | 23 | def method_missing(*args) 24 | @mutex.synchronize do 25 | mtd = args.shift 26 | if @origin.respond_to?(mtd) 27 | params = @origin.method(mtd).parameters 28 | reqs = params.count { |p| p[0] == :req } 29 | if params.any? { |p| p[0] == :key } && args.size > reqs 30 | @origin.__send__(mtd, *args[0...-1], **args.last) do |*a| 31 | yield(*a) if block_given? 32 | end 33 | else 34 | @origin.__send__(mtd, *args) do |*a| 35 | yield(*a) if block_given? 36 | end 37 | end 38 | else 39 | super 40 | end 41 | end 42 | end 43 | 44 | def respond_to?(method, include_private = false) 45 | @origin.respond_to?(method, include_private) 46 | end 47 | 48 | def respond_to_missing?(method, include_private = false) 49 | @origin.respond_to?(method, include_private) || super 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | syncem (0.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.3) 10 | backtrace (0.4.0) 11 | concurrent-ruby (1.3.5) 12 | date (3.4.1) 13 | json (2.12.0) 14 | language_server-protocol (3.17.0.5) 15 | lint_roller (1.1.0) 16 | minitest (5.25.5) 17 | parallel (1.27.0) 18 | parser (3.3.8.0) 19 | ast (~> 2.4.1) 20 | racc 21 | prism (1.4.0) 22 | psych (5.2.6) 23 | date 24 | stringio 25 | racc (1.8.1) 26 | rainbow (3.1.1) 27 | rake (13.2.1) 28 | rdoc (6.13.1) 29 | psych (>= 4.0.0) 30 | regexp_parser (2.10.0) 31 | rubocop (1.75.5) 32 | json (~> 2.3) 33 | language_server-protocol (~> 3.17.0.2) 34 | lint_roller (~> 1.1.0) 35 | parallel (~> 1.10) 36 | parser (>= 3.3.0.2) 37 | rainbow (>= 2.2.2, < 4.0) 38 | regexp_parser (>= 2.9.3, < 3.0) 39 | rubocop-ast (>= 1.44.0, < 2.0) 40 | ruby-progressbar (~> 1.7) 41 | unicode-display_width (>= 2.4.0, < 4.0) 42 | rubocop-ast (1.44.1) 43 | parser (>= 3.3.7.2) 44 | prism (~> 1.4) 45 | rubocop-minitest (0.38.0) 46 | lint_roller (~> 1.1) 47 | rubocop (>= 1.75.0, < 2.0) 48 | rubocop-ast (>= 1.38.0, < 2.0) 49 | rubocop-performance (1.25.0) 50 | lint_roller (~> 1.1) 51 | rubocop (>= 1.75.0, < 2.0) 52 | rubocop-ast (>= 1.38.0, < 2.0) 53 | rubocop-rake (0.7.1) 54 | lint_roller (~> 1.1) 55 | rubocop (>= 1.72.1) 56 | rubocop-rspec (3.6.0) 57 | lint_roller (~> 1.1) 58 | rubocop (~> 1.72, >= 1.72.1) 59 | ruby-progressbar (1.13.0) 60 | stringio (3.1.7) 61 | threads (0.4.1) 62 | backtrace (~> 0) 63 | concurrent-ruby (~> 1.0) 64 | unicode-display_width (3.1.4) 65 | unicode-emoji (~> 4.0, >= 4.0.4) 66 | unicode-emoji (4.0.4) 67 | 68 | PLATFORMS 69 | arm64-darwin-22 70 | arm64-darwin-23 71 | arm64-darwin-24 72 | x64-mingw-ucrt 73 | x86_64-linux 74 | 75 | DEPENDENCIES 76 | minitest (~> 5.25) 77 | rake (~> 13.2) 78 | rdoc (~> 6.11) 79 | rubocop (~> 1.71) 80 | rubocop-minitest (> 0) 81 | rubocop-performance (> 0) 82 | rubocop-rake (> 0) 83 | rubocop-rspec (> 0) 84 | syncem! 85 | threads (~> 0.4) 86 | 87 | BUNDLED WITH 88 | 2.5.16 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![EO principles respected here](https://www.elegantobjects.org/badge.svg)](https://www.elegantobjects.org) 4 | [![DevOps By Rultor.com](https://www.rultor.com/b/yegor256/syncem)](https://www.rultor.com/p/yegor256/syncem) 5 | [![We recommend RubyMine](https://www.elegantobjects.org/rubymine.svg)](https://www.jetbrains.com/ruby/) 6 | 7 | [![rake](https://github.com/yegor256/syncem/actions/workflows/rake.yml/badge.svg)](https://github.com/yegor256/syncem/actions/workflows/rake.yml) 8 | [![Gem Version](https://badge.fury.io/rb/syncem.svg)](https://badge.fury.io/rb/syncem) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/5528e182bb5e4a2ecc1f/maintainability)](https://codeclimate.com/github/yegor256/syncem/maintainability) 10 | [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/github/yegor256/syncem/master/frames) 11 | [![Hits-of-Code](https://hitsofcode.com/github/yegor256/syncem)](https://hitsofcode.com/view/github/yegor256/syncem) 12 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/syncem/blob/master/LICENSE.txt) 13 | 14 | Read this blog post: 15 | [_SyncEm: Thread-Safe Decorators in Ruby_](https://www.yegor256.com/2019/06/26/syncem.html). 16 | 17 | Sometimes you have an object that is not thread-safe, 18 | but you need to make sure each of its methods is thread-safe, because they 19 | deal with some resources, like files or databases and you want them to 20 | manage those resources sequentially. This small gem will help you achieve 21 | exactly that without any re-design of the objects you already have. Just 22 | decorate them with `SyncEm` [thread-safe decorator](https://www.yegor256.com/2017/01/17/synchronized-decorators.html) 23 | and that is it. 24 | 25 | First, install it: 26 | 27 | ```bash 28 | $ gem install syncem 29 | ``` 30 | 31 | Then, use it like this: 32 | 33 | ```ruby 34 | require 'syncem' 35 | obj = SyncEm.new(obj) 36 | ``` 37 | 38 | That's it. 39 | 40 | ## How to contribute 41 | 42 | Read [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html). 43 | Make sure your build is green before you contribute 44 | your pull request. You will need to have [Ruby](https://www.ruby-lang.org/en/) 2.3+ and 45 | [Bundler](https://bundler.io/) installed. Then: 46 | 47 | ``` 48 | $ bundle update 49 | $ bundle exec rake 50 | ``` 51 | 52 | If it's clean and you don't see any error messages, submit your pull request. 53 | -------------------------------------------------------------------------------- /test/test_syncem.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko 4 | # SPDX-License-Identifier: MIT 5 | 6 | require 'minitest/autorun' 7 | require 'threads' 8 | require_relative '../lib/syncem' 9 | 10 | # Syncem test. 11 | # Author:: Yegor Bugayenko (yegor256@gmail.com) 12 | # Copyright:: Copyright (c) 2018-2025 Yegor Bugayenko 13 | # License:: MIT 14 | class SyncEmTest < Minitest::Test 15 | class Account 16 | def initialize(file) 17 | @file = file 18 | end 19 | 20 | def balance 21 | File.exist?(@file) ? File.read(@file).to_i : 0 22 | end 23 | 24 | def add(amount) 25 | now = File.exist?(@file) ? File.read(@file).to_i : 0 26 | File.write(@file, (now + amount).to_s) 27 | end 28 | 29 | def read 30 | yield balance 31 | end 32 | end 33 | 34 | def test_wraps_simple_object 35 | Dir.mktmpdir do |dir| 36 | path = File.join(dir, 'f.txt') 37 | acc = Account.new(path) 38 | assert_equal(0, acc.balance) 39 | acc.add(50) 40 | assert_equal(50, acc.balance) 41 | acc.add(-10) 42 | assert_equal(40, acc.balance) 43 | end 44 | end 45 | 46 | def test_respond_to 47 | Dir.mktmpdir do |dir| 48 | path = File.join(dir, 'f.txt') 49 | acc = SyncEm.new(Account.new(path)) 50 | assert_respond_to(acc, :balance) 51 | assert_respond_to(acc, :add) 52 | end 53 | end 54 | 55 | def test_works_with_send_method 56 | obj = Object.new 57 | def obj.send(_first, second) 58 | second 59 | end 60 | synced = SyncEm.new(obj) 61 | assert_equal(3, synced.send(2, 3)) 62 | assert_equal({ y: 2 }, synced.send({ x: 1 }, { y: 2 })) 63 | end 64 | 65 | def test_works_with_optional_arguments 66 | obj = Object.new 67 | def obj.foo(first, _second, ext1: 'a', ext2: 'b') 68 | first + ext1 + ext2 69 | end 70 | synced = SyncEm.new(obj) 71 | assert_equal('.xy', synced.foo('.', {}, ext1: 'x', ext2: 'y')) 72 | assert_equal('fzb', synced.foo('f', {}, ext1: 'z')) 73 | assert_equal('-ab', synced.foo('-', {})) 74 | end 75 | 76 | def test_works_with_splat_arguments 77 | obj = Object.new 78 | def obj.foo(one, two, *rest) 79 | one + two + (rest.empty? ? '' : rest.first.to_s) 80 | end 81 | synced = SyncEm.new(obj) 82 | assert_equal('ab', synced.foo('a', 'b')) 83 | assert_equal('abc', synced.foo('a', 'b', 'c')) 84 | end 85 | 86 | def test_works_with_default_value 87 | obj = Object.new 88 | def obj.foo(first, second = 42) 89 | first + second 90 | end 91 | synced = SyncEm.new(obj) 92 | assert_equal(15, synced.foo(7, 8)) 93 | assert_equal(43, synced.foo(1)) 94 | end 95 | 96 | def test_works_with_block 97 | Dir.mktmpdir do |dir| 98 | path = File.join(dir, 'f.txt') 99 | acc = SyncEm.new(Account.new(path)) 100 | acc.add(50) 101 | before = 0 102 | acc.read do |b| 103 | assert_equal(50, b) 104 | before = b 105 | end 106 | assert_equal(50, before) 107 | end 108 | end 109 | 110 | def test_multiple_threads 111 | Dir.mktmpdir do |dir| 112 | path = File.join(dir, 'f.txt') 113 | acc = SyncEm.new(Account.new(path)) 114 | threads = 10 115 | Threads.new(threads).assert do 116 | acc.add(10) 117 | end 118 | assert_equal(threads * 10, acc.balance) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | syncem 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------