diff --git a/CHANGELOG.md b/CHANGELOG.md index a92de19..bc3875b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- `mark_left` function to mark removed members as `left`. + ## [2.4.5] - 2024-06-24 ### Fixed @@ -13,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Invalid events parsing. ## [2.4.4] - 2024-04-09 + ### Fixed - Invalid payload parsing in anti entropy step. diff --git a/membership.lua b/membership.lua index 9a00507..9b31d9b 100755 --- a/membership.lua +++ b/membership.lua @@ -736,6 +736,39 @@ local function leave() return true end +--- Forcefully send leave message about an instance. +-- @function mark_left +-- @treturn boolean +-- `true` if call succeeds, +-- `false` if member has already left. +local function mark_left(uri_to_leave) + if _sock == nil then + return false + end + + -- Perform artificial events.generate() and instantly send it + local myself = members.get(uri_to_leave) + if not myself or myself.status == opts.LEFT then + return false + end + local event = events.pack({ + uri = uri_to_leave, + status = opts.LEFT, + incarnation = myself.incarnation, + ttl = members.count(), + }) + local msg_msgpacked = msgpack.encode({uri_to_leave, 'LEAVE', msgpack.NULL, {event}}) + local msg_encrypted = opts.encrypt(msg_msgpacked) + for _, uri in ipairs(members.filter_excluding('unhealthy', uri_to_leave)) do + local addr = resolve(uri) + if addr then + _sock:sendto(addr.host, addr.port, msg_encrypted) + end + end + + return true +end + --- Member data structure. -- A member is represented by the table with the following fields: -- @@ -984,6 +1017,7 @@ end return { init = init, leave = leave, + mark_left = mark_left, members = get_members, broadcast = broadcast, pairs = function() return pairs(get_members()) end, diff --git a/test/test_quit.py b/test/test_quit.py index caffac7..54a7161 100644 --- a/test/test_quit.py +++ b/test/test_quit.py @@ -19,3 +19,15 @@ def test_rejoin(servers, helpers): assert servers[13302].conn.eval('return membership.init("localhost", 13302)')[0] assert servers[13301].add_member('localhost:13302') helpers.wait_for(servers[13301].check_status, ['localhost:13302', 'alive']) + + +def test_mark_left(servers, helpers): + helpers.wait_for(servers[13301].check_status, ['localhost:13302', 'alive']) + assert servers[13301].conn.eval('return membership.mark_left("localhost:13302")')[0] + helpers.wait_for(servers[13301].check_status, ['localhost:13302', 'left']) + + # already has left + assert not servers[13301].conn.eval('return membership.mark_left("localhost:13302")')[0] + + # there are no such member + assert not servers[13301].conn.eval('return membership.mark_left("localhost:10000")')[0]