Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement advisory locks and some other methods #14

Merged
merged 11 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
ruby: [ 2.6, 2.7, '3.0', '3.1', head, truffleruby, jruby ]
ruby: [ '3.1', '3.2', '3.3', head, truffleruby, jruby ]
db_adapter: [ sqlite, mysql, postgresql ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- run: mv test/database.yml.example test/database.yml
- run: mv docker-compose.yml.example docker-compose.yml
if: ${{ matrix.db_adapter == 'mysql' || matrix.db_adapter == 'postgresql' }}
- run: docker-compose up -d ${{ matrix.db_adapter }}
- run: docker compose up -d ${{ matrix.db_adapter }}
if: ${{ matrix.db_adapter == 'mysql' || matrix.db_adapter == 'postgresql' }}
- uses: ruby/setup-ruby@v1
with:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
Gemfile.lock
docker-compose.yml
/test/database.yml
/data/
/log/*
!/log/.keep
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
inherit_gem:
ruboconf: ruboconf.yml
AllCops:
TargetRubyVersion: 2.6
TargetRubyVersion: 3.1
inherit_mode:
merge:
- Exclude
Expand Down
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project tries to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Major Changes

This version introduces **advisory locks**. Advisory locking is **automatically enabled** if your model class responds to `#with_advisory_lock` (ex. `User.with_advisory_lock`).

From now on the lexorank gem requires ruby version 3.1 or higher. This decision is based on ruby's end of life dates (3.0 went eol in April 2024).

All internal API methods that lexorank was using until 0.1.3 were moved to another location. If you rely on those (and you should not), have a look at the `Lexorank::Ranking` class. An instance of this class can be accessed via the `lexorank_ranking` attribute on your model class.

### Added

- Add advisory locks if the model class responds to `with_advisory_lock`
- Add `#move_to_end` and `#move_to_end!` to move a record to the end of a collection
- The CI now runs against multiple database adapters (sqlite, mysql, postgresql)

### Changed

- Blocks passed to all `move_to` methods will now be executed after the rank was assigned. When using advisory locks, the block will be executed while the lock is still active.
- When calling `#move_to` with a position that is larger than the number of records in the collection it will now be moved to the end of the list
- Require ruby version 3.1 or higher
- Moved Changelog from [README.md](https://github.com/richardboehme/lexorank/blob/main/README.md) to [CHANGELOG.md](https://github.com/richardboehme/lexorank/blob/main/CHANGELOG.md)

## [0.1.3] - 2021-07-16

### Added

- Add support to move elements into another group ([#5](https://github.com/richardboehme/lexorank/pull/5), by [@bookis](https://github.com/bookis))
- Add the `no_rank?` method ([#5](https://github.com/richardboehme/lexorank/pull/5), by [@bookis](https://github.com/bookis))

### Fixed

- Removed in-memory operations while trying to find records around the model that should be moved

## [0.1.2] - 2021-03-08

### Fixed

- Fixed gemspec to be valid

### Changed

- Updated Changelog format

## [0.1.1] - 2021-03-08

### Changed

- Updated license year

## [0.1.0] - 2021-03-08

*Initial Release*
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ if defined?(JRUBY_VERSION)
else
gem 'mysql2'
gem 'pg'
gem 'sqlite3', '~> 1.4'
gem 'sqlite3'
end
gem 'm'
gem 'minitest'
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2021 Richard Böhme
Copyright (c) 2024 Richard Böhme

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
84 changes: 48 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Add this line to your application's Gemfile:

```ruby
gem 'lexorank'
gem 'with_advisory_lock' # recommended to get locking out of the box
```

And then execute:
Expand Down Expand Up @@ -129,15 +130,15 @@ end

## Class methods


<details>
<summary><a id="rank"></a><code>rank!(field: :rank, group_by: nil)</code></summary>
<summary><a id="rank"></a><code>rank!(field: :rank, group_by: nil, advisory_lock: {})</code></summary>

This is the entry point to use lexorank in your model.

Options:
* `field`: Allows you to pass a custom field which is being used to store the models rank. (defaults to `:rank`)
* `group_by`: Makes it possible to split model ordering into groups by a specific column. [Learn more](#associations-and-grouping)
* `advisory_lock`: The advisory lock configuration. [Learn more](#locking)

</details>
<details>
Expand All @@ -156,10 +157,14 @@ Those will only be available if your model calls `rank!` before.


<details>
<summary><a id="move_to"></a><code>move_to(position)</code></summary>
<summary><a id="move_to"></a><code>move_to(position, &block)</code></summary>

This method will set your object's rank column according to the new position. Position counts start at zero.
This will not persist the rank to the database.

The passed block will be executed after the new rank was assigned.

When using [Locking](#locking) it is **discouraged** to use `move_to` without passing a block. The block will be executed inside of the advisory lock and should persist the change to the rank to ensure that no positioning conflicts will occur.
</details>
<details>
<summary><code>move_to_top</code></summary>
Expand Down Expand Up @@ -242,6 +247,46 @@ Retrieving data in a grouped manner is as simple as utilizing built-in ActiveRec
Page.first.paragraphs.ranked
```

## Locking

Since version 0.2.0 lexorank ships with advisory locking by default. Advisory locks are a locking mechanism on the database level that ensures that only one record in a collection can change their rank at a time. This is important to prevent two records being assigned the same rank.

Advisory locking is enabled by default if the model class responds to the `with_advisory_lock` method. The easiest way to achieve this is by installing the incredible [`with_advisory_lock` gem](https://github.com/ClosureTree/with_advisory_lock).

It is also possible to implement advisory locking yourself. The `with_adivsory_lock` method must accept one name argument and arbitrary keyword arguments similar to the signature of the [`with_advisory_lock` gem](https://github.com/ClosureTree/with_advisory_lock).

With advisory locking enabled it is actively **dicouraged** to call `move_to` or `move_to_top` without a block. This is because those methods do not persist to the database and thus cannot acquire a lock. Make sure the bang equivalents or pass a block in which the record is persisted.

### Opting out of locking

If you manage locking yourself or you do not need locking, you can disable advisory locks:

```ruby
class Page < ActiveRecord::Base
rank!(advisory_lock: { enabled: false })
end
```

Note that locking will be disabled by default if the model class does not respond to the `with_advisory_lock` method.

### Configuring locking

The lexorank gem will choose an appropriate lock name by taking the class name, the ranking column and grouping into account. It's still possible to supply a `lock_name` callable that returns a custom name.

```ruby
class Page < ActiveRecord::Base
rank!(advisory_lock: { lock_name: ->(page) { "custom_lock_for_page_#{page.id}" } })
end
```

Also it's possible to pass other options (e.g. `timeout_seconds` when using the [`with_advisory_lock` gem](https://github.com/ClosureTree/with_advisory_lock)). All options are passed to the `with_advisory_lock` method as keyword arguments.

```ruby
class Page < ActiveRecord::Base
rank!(advisory_lock: { timeout_seconds: 3 })
end
```

## Internals - How does lexorank work?

The gem works quite simple. When calling `move_to` the gem will identify the item which is on the wanted position and the one before.
Expand Down Expand Up @@ -346,39 +391,6 @@ Setting up the different database adapter environments *should* be as simple as
5. Build gem and push to rubygems.org
</details>

## Changelog

<details>
<summary>0.1.3</summary>

* add support to move element into another group ([#5](https://github.com/richardboehme/lexorank/pull/5), by [@bookis](https://github.com/bookis))
* add the `no_rank?` method ([#5](https://github.com/richardboehme/lexorank/pull/5), by [@bookis](https://github.com/bookis))
* remove in memory operations with the collection when calling `move_to`
</details>

<details>
<summary>0.1.2</summary>

* fix gem specification
* update changelog format
</details>

<details>
<summary>0.1.1</summary>

* update license year
* let rubygems be happy to have an updated version
</details>


<details>
<summary>0.1.0</summary>

*Initial Release*
</details>

## License

Copyright (c) 2021-2022 Richard Böhme ([email protected])

Lexorank is released under the [MIT License](https://opensource.org/licenses/MIT).
10 changes: 6 additions & 4 deletions docker-compose.yml.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.8"

services:
postgresql:
image: postgres:latest
Expand All @@ -10,7 +8,7 @@ services:
ports:
- "5432:5432"
volumes:
- ./data/postgres:/var/lib/postgresql/data
- postgres_data:/var/lib/postgresql/data

mysql:
image: mariadb
Expand All @@ -21,4 +19,8 @@ services:
ports:
- "3306:3306"
volumes:
- ./data/mysql:/var/lib/mysql
- mysql_data:/var/lib/mysql

volumes:
postgres_data:
mysql_data:
8 changes: 6 additions & 2 deletions lexorank.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ Gem::Specification.new do |spec|
spec.version = Lexorank::VERSION
spec.authors = ['Richard Böhme']
spec.email = ['[email protected]']
spec.metadata['rubygems_mfa_required'] = 'true'

spec.summary = 'Store order of your models by using lexicographic sorting.'
spec.homepage = 'https://github.com/richardboehme/lexorank'
spec.license = 'MIT'
spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0')

spec.metadata['rubygems_mfa_required'] = 'true'
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = spec.homepage
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"

spec.files = Dir['LICENSE', 'lib/**/*']

Expand Down
1 change: 1 addition & 0 deletions lib/lexorank.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
# SOFTWARE.
module Lexorank
class InvalidRankError < StandardError; end
class InvalidConfigError < StandardError; end

MIN_CHAR = '0'
MAX_CHAR = 'z'
Expand Down
Loading
Loading