Bug #20474
closedMultiple free IPs returned after record deletion
Description
In #20173 we fixed possible race condition, but there is one more:
- create reservation
- call unused_ip and X is returned
- delete the above reservation
- call unused_ip and Y is returned
Now, the contract is expected to satisfy X != Y but it's not the case right now thanks to the design of our unused_ip method (rotating list shifts to the left on delete returning already returned IP). I think we need to rethink how this is done.
We should also not use /tmp as the directory, it's getting deleted via systemd timer in RHEL7 or other distros as well.
Updated by Lukas Zapletal over 7 years ago
- Related to Bug #20475: Implement Random DB IPAM added
Updated by Lukas Zapletal over 7 years ago
- Related to Bug #20173: Concurrent calls to Subnet#unused_ip may return the same ip address added
Updated by Lukas Zapletal over 7 years ago
- Bugzilla link set to 1459644
Randomizing will look like the best solution. We can seed random generator from MAC address as we did in #20475.
Updated by Lukas Zapletal over 7 years ago
Foreman core now contains randomized IPAM DB implementation, we need similar for Smart Proxy. Ideally this would be an implementation that Ivan this is customer-facing, can you put this on your scrum?
module IPAM # Internal DB IPAM returning all IPs in random order to minimize race conditions class RandomDb < Base def generator @generator ||= Random.new(mac ? mac.gsub(':', '').to_i(16) : Random.new_seed) end def random_ip IPAddr.new(generator.rand(subnet_range.first.to_i..subnet_range.last.to_i), subnet.family) end # Safety check not to spend much CPU time when there are no many free IPs left. This gives up # in about a second on Ryzen 1700 running with Ruby 2.4. MAX_ITERATIONS = 100_000 def suggest_ip iterations = 0 loop do # next random IP from the sequence generated by MAC seed candidate = random_ip iterations += 1 break if iterations >= MAX_ITERATIONS # try to match it ip = candidate.to_s if !excluded_ips.include?(ip) && !subnet.known_ips.include?(ip) logger.debug("Found #{ip} in #{iterations} iterations") return ip end end logger.debug("Not suggesting IP Address for #{subnet} as no free IP found in reasonable time (#{iterations} iterations)") errors.add(:subnet, _('no random free IP could be found in our DB, enlarge subnet range')) nil end end end
The following unit tests simulates the race condition:
def test_unused_ip_with_concurrent_record_add @subnet.stubs(:icmp_pingable?) @subnet.stubs(:tcp_pingable?) records = [] records << Proxy::DHCP::Reservation.new('test', "192.168.0.1", "aa:bb:cc:dd:ee:01", @subnet, :hostname =>'test_01') assert_equal "192.168.0.2", @subnet.unused_ip(records) assert_equal "192.168.0.3", @subnet.unused_ip(records) records << Proxy::DHCP::Reservation.new('test', "192.168.0.2", "aa:bb:cc:dd:ee:02", @subnet, :hostname =>'test_02') assert_equal "192.168.0.4", @subnet.unused_ip(records) # this fails - returns 192.168.0.5 ensure File.delete('test/tmp/foreman-proxy_192.168.0.0_24.tmp') end def test_unused_ip_with_concurrent_record_delete @subnet.stubs(:icmp_pingable?) @subnet.stubs(:tcp_pingable?) records = [] records << Proxy::DHCP::Reservation.new('test', "192.168.0.1", "aa:bb:cc:dd:ee:01", @subnet, :hostname =>'test_01') records << Proxy::DHCP::Reservation.new('test', "192.168.0.2", "aa:bb:cc:dd:ee:02", @subnet, :hostname =>'test_02') assert_equal "192.168.0.3", @subnet.unused_ip(records) records.pop assert_equal "192.168.0.4", @subnet.unused_ip(records) # this fails - returns 192.168.0.3 ensure File.delete('test/tmp/foreman-proxy_192.168.0.0_24.tmp') end
And the following unit test never returns (CPU spikes at 100%) because Ruby is busy with creating 16 million records array in memory. The randomized can solve that:
def test_unused_aclass_ip_beware_this_never_finishes @network = "10.0.0.0" @netmask = "255.0.0.0" @subnet = Proxy::DHCP::Subnet.new @network, @netmask @subnet.stubs(:icmp_pingable?) @subnet.stubs(:tcp_pingable?) r = Proxy::DHCP::Reservation.new('test', "10.0.0.1", "aa:bb:cc:dd:ee:ff", @subnet, :hostname =>'test1') assert_equal "10.0.0.2", @subnet.unused_ip([r]) ensure File.delete('test/tmp/foreman-proxy_10.0.0.0_8.tmp') end
Ideally the code is refactored so unused_ip has it's own providers injected into subnet/server and we provide two implementations: the current one (perhaps fixed so it uses less memory) and random which I believe is very easy to implement (see the code bit from core) and workarounds the issue nicely.
Updated by Lukas Zapletal over 7 years ago
- Priority changed from Normal to High
BZ set, customer facing.
Updated by Anonymous over 7 years ago
- Status changed from New to Assigned
- Assignee set to Anonymous
Updated by The Foreman Bot over 7 years ago
- Status changed from Assigned to Ready For Testing
- Pull request https://github.com/theforeman/smart-proxy/pull/543 added
Updated by Anonymous over 7 years ago
- Status changed from Ready For Testing to Closed
- % Done changed from 0 to 100
Applied in changeset 54f44d807b8540fd3561a0d16ab68c68deb88c0f.