News for hackers. Stuff that matters.
Re:There are only four billion floats, so test them all (2014) by tialaramex on Sunday October 11, 2020 @10:32AM (#24746066)
Many years ago now we built something that used a 60-bit truncated hash of URIs. So that's far too small to be comfortable that you won't run into collisions, but it's big enough that for modest sizes (hundreds of millions up to billions) a collision is unlikely. So you can have a slow path for the collision case, so long as that slow path is correct. For unit testing we needed at least one collision so we could see that everything behaves as designed before some real user hits one. We had test data sets with over a billion URIs in them, but none that (as far as we could tell) had a collision.

So I just wrote code to mint nonsense URIs for our system based on a small integer seed (like https://foo.example/123456789), then span up a system with a bunch of RAM (maybe 8-12GB) to iterate through each seed, storing the hash and seed it was generated from, until it hit a collision after a few gigabytes of RAM, then it spat out the two URIs from the colliding seeds. Bingo, now we had data for unit testing to show that stuff works even in the unlikely case that two distinct URIs have the same hash.

Probably a day's work to build that, and then I'd guess several hours every month saved debugging weird collision bugs because our unit tests would now tell us if we'd screwed up and collisions were slipping past.

Re:There are only four billion floats, so test them all (2014) by csense on Sunday October 11, 2020 @08:20PM (#24750305)
Figure you need at least 12 bytes per entry to store the ID and hash. Then you can only handle 1/12 of your memory size in bytes (so 1 billion entries if you have 12 billion bytes of RAM).

Much more memory efficient to use a Bloom filter. Build n different ways of hashing the hash (this can be as simple as having some standard hash function H() making the ith different way H(h, i)). Then when you add an entry you write a bit at each of these locations.

Instead of writing right away, you check to see if all the bits were already set. If they were, the entry being added is a candidate for a possible collision with some earlier entry, so stream it out to a file or something.

Once you have the (much smaller) list of a few million collision candidates, you do another iteration over the whole data set, checking each one against only the collision candidates.

The most memory-intensive part is the Bloom filter. You could probably get away with a Bloom filter size of 10 bits per entry or less. (If you want specific numbers, there are lots of online calculators to help you calibrate the size of a Bloom filter.)

But given this is a one-off search, and you had a machine with enough RAM to find a collision, an ordinary hash-based set implementation is good enough and saves the most precious resource of all, developer time :)

Re:There are only four billion floats, so test them all (2014) by tialaramex on Sunday October 11, 2020 @09:02PM (#24750503)
This is clever. At the time we needed this machine anyway (this is way before "Cloud computing" was really a thing) but a few years earlier it would have been a good choice.

We actually had most of the bloom filter code you'd need because we used a lot of Bloom Filters in that project. One of the very common queries that system had was "Hey do we know about random thing X?" for which the answer would often (but not always) be "No". So Bloom Filters enabled the system to be able to say definitively "Nope, I know nothing about X" a good fraction of the time without any further work. This actually hurt us in some third party benchmarks we ran for boasting, but helped in our real use case.

Re:There are only four billion floats, so test them all (2014) by jcims on Sunday October 11, 2020 @01:31PM (#24747491)
I embarrassed myself as a Noogler trying to explain what in my head seemed like a good tradeoff between hash size (also of URIs) and collision handling. The process had fixed storage for a table of known hashes, and every collision was tested for validity before taking action. If you could use a truncated hash, you’d obviously increase your collisions, but if you’re going to validate all collisions anyway, it seemed like you could increase your coverage substantially (in the 60-bit case it would have been 4x) while only marginally increasing your overhead of collision handling.

There's some kind of sparsity/entropy function involved with optimizing these two but I'm too clueless to sort it out. Without the vocabulary to make my case I just got weirded out by a bunch of smart people staring at me quizzically. Sooo..

Re:There are only four billion floats, so test them all (2014) by Arcuru on Sunday October 11, 2020 @03:18PM (#24748269)
That kind of thing happens to me all the time. I'm very often unable to think of or explain things in the moment.

Sometimes it helps to write a doc afterwards to get my thoughts in order and decide if it's worth sharing.

Re:There are only four billion floats, so test them all (2014) by btrask on Sunday October 11, 2020 @05:54PM (#24749436)
It's known as the birthday paradox. There is a probability table here: https://en.wikipedia.org/wiki/Birthday_paradox#Probability_t...
Re:There are only four billion floats, so test them all (2014) by jcims on Sunday October 11, 2020 @07:16PM (#24749948)
Yes! That’s the part that I was having trouble getting my head around. Looks like 96 bits would have been fine and 128 more than adequate. A small bit of vindication :)
Re:There are only four billion floats, so test them all (2014) by seanmcdirmid on Sunday October 11, 2020 @10:45AM (#24746143)
Couldn’t you also test the recovery logic with a hasher that yielded collision more often? It’s like take your system, make it worse (use very dumb hashing), and see how it degrades and/or breaks?
Re:There are only four billion floats, so test them all (2014) by tialaramex on Sunday October 11, 2020 @11:50AM (#24746581)
My preference is always to actually test parts in as close as possible to the real state, avoiding mocks unless they're necessary. The more "fake" the system is, the less it tells you about how your software actually works.

So since I could generate collisions for the real hash, that's what I did. If it had been infeasible, I would of course have found a different way to test.

This particular software was written mostly in C (today I would not write it in C) which makes it even more likely that introducing never-used-in-production behaviour unexpectedly perturbs the system. This same system for example ran into an obscure (and since fixed) libc allocator bug, and a defect in the Linux kernel, because we were really kicking the shit out of the memory mapped file I/O with what we were doing.

Re:There are only four billion floats, so test them all (2014) by tshaddox on Sunday October 11, 2020 @01:04PM (#24747254)
“Test as close as possible to the real state” makes sense as a rule of thumb, but I’m having trouble coming up with a reason in this specific case how mocking the hash function could go wrong. Assuming your testing/mocking library is reliable, this should be testing precisely what you intend to test. I guess it’s possible that your “more real” test relies on some hidden behavior, like how your real hash function utilizes system resources, which could affect production, but I don’t know that tests which are testing things you’re not even aware of is a great thing to advocate for.
Re:There are only four billion floats, so test them all (2014) by naniwaduni on Sunday October 11, 2020 @01:09PM (#24747296)
The point of the principle is that you don't know how the mocking can go wrong. Assuming your testing/mocking library is reliable gives you one more thing that can unexpectedly go wrong.

Of course, sometimes getting verisimilar test cases is difficult enough that you judge it not worth the effort over mocking it up and dealing with any issues with the mockup, foreseen and unforeseen.

Re:There are only four billion floats, so test them all (2014) by tshaddox on Sunday October 11, 2020 @02:52PM (#24748069)
I think you generally need to either assume or establish that your testing environment tends to be sound, just like you need to assume or establish that your programming language/compiler/computing environment tends to be sound. Your application code and test code itself probably shouldn't be compensating for or attempting to avoid known or unknown flaws in your testing environment.
Re:There are only four billion floats, so test them all (2014) by shawnz on Sunday October 11, 2020 @07:12PM (#24749927)
How do you establish that the testing environment is sound besides having defensive tests? It doesn't necessarily have to be the case that all your tests are like that.
Re:There are only four billion floats, so test them all (2014) by lisper on Sunday October 11, 2020 @01:24PM (#24747424)
Why not just use a hash function that special-cases two test URLs (or a whole class of test URLs) and produces collisions for them? You can use that same hash function in production.
Re:There are only four billion floats, so test them all (2014) by kortilla on Sunday October 11, 2020 @02:18PM (#24747832)
That’s a stupid principle. If you don’t have your interface well-defined enough that switching out the hashing implementation is difficult, your code is too tightly coupled.

A test handing a hash collision is not a test of the hashing algorithm so you should be able to switch out the algorithm to something super predictable. This isn’t a discussion about mocking frameworks - you should be able to do with by just plugging in a different hash implementation when you construct the test.

Re:There are only four billion floats, so test them all (2014) by kuiper0x2 on Sunday October 11, 2020 @02:54PM (#24748097)
These types of blanket rules are useful to ensure a 100% accurate test case. If an error in production costs me $15 Million I don't really care if the engineer wastes a few days to follow a dumb rule.

Also I can employ a $75k - $100k/year engineer and say follow this protocol for testing. If I want someone who can optimize test case 100% of the time with zero mistakes ever then I might have to pay $150k - $200k / year.

Re:There are only four billion floats, so test them all (2014) by kortilla on Tuesday October 13, 2020 @07:17PM (#24771432)
That’s not useful. A swapped out hashing algorithm would have been proving the same thing and would have allowed you to test important things like 3+ collisions and two sets of collisions.

Anyone who demanded the real hashing algorithm in this case doesn’t understand how hash collisions work and what problems you need to solve when using hashing. Your “100% accurate test case” serves very little value.

It’s the same as requiring a real person perform the checkout workflow in your shopping cart integration tests. It’s “100% accurate” but it’s a massive waste of resources and provides terrible coverage of the many other edge cases.

Re:There are only four billion floats, so test them all (2014) by Scarblac on Sunday October 11, 2020 @03:35PM (#24748413)
It ensures that if someone changes the hashing algorithm, your test for the collision mitigation now doesn't test that anymore even though they should be unrelated.
Re:There are only four billion floats, so test them all (2014) by halter73 on Sunday October 11, 2020 @06:20PM (#24749601)
After going through all that effort to make the test less "fake", I expect tialaramex verified the collision still happened as part of the test.
Re:There are only four billion floats, so test them all (2014) by tialaramex on Sunday October 11, 2020 @09:11PM (#24750550)
Yes, actually we also verified that all the hashes were exactly what we expected, because we were using this for content based addressing. "The hash has unexpectedly changed" was a fatal build error.

If we wanted to change how the hash is calculated all extant instances of the system need to dump to backups, then be painstakingly restored after updating. Possible, but certainly not something you'd be doing for giggles.

Re:There are only four billion floats, so test them all (2014) by seanmcdirmid on Sunday October 11, 2020 @12:25PM (#24746900)
The only high fidelity mantra in testing is a bit misguided. If you only test your system in as close to production state as possible, you are never going to encounter stress states that would only very rarely appear in production. Seeing how your components independently deal with less ideal components in the system can tell you a lot about how they might handle rare but bad cases in production, and allow you to over engineer them a bit more to avoid that.

> which makes it even more likely that introducing never-used-in-production behaviour unexpectedly perturbs the system.

Isn’t that a good thing? Isn’t the goal to deal with the unexpected in the first case?

Re:There are only four billion floats, so test them all (2014) by jniedrauer on Sunday October 11, 2020 @12:40PM (#24747026)
Tests come with a non-zero cost. They're code that needs to be maintained. Often complex code, with poor documentation. I don't think the value provided by testing states that you will never actually encounter in production outweighs the cost of maintaining the tests.
Re:There are only four billion floats, so test them all (2014) by tikhonj on Sunday October 11, 2020 @01:18PM (#24747383)
This approach works well until you do encounter a situation like that in production :). It's worth testing conservatively because our understanding of our systems is imperfect and because assumptions that may be valid now are likely to change in the future. It's especially true for code that might operate at scale or has security considerations, because both of those can magnify the effects of conditions that occur with arbitrarily low probabilities.

Personally, I see tests as not just a way to prevent bugs, but as a way to learn about the system being tested—both how it behaves now and, continually, how it behaves in future iterations. In this specific case, understanding what happens if two URIs hash to the same value even if they don't "really" do that is an important bit of information: it tells us not only what would happen with a real hash collision but also how our system would behave if there were a bug either in the hashing algorithm itself or in the code that connects the hashing algorithm to the component that uses the hashes.

What happens if we introduce a caching layer between where the hash is generated and where it's used, and there's a cache invalidation bug? I don't know how realistic that is in this specific hypothetical, but I'm sure there are other performance optimizations with the same bug potential that I'm not even thinking of. Changes like that can lead to absolute debugging nightmares! Testing even "impossible" edge cases helps catch this in a systematic way, without needing full knowledge of which conditions might matter.

Of course, none of this talks to the trade-off you pointed out: test code does have a real cost. That is absolutely something to consider. I just don't think the answer is to draw a hard line at "don't test situations that can't come up in production"—and, perhaps naively, I think the "real" solution is less about deciding what is and isn't worth testing and more about reducing the costs of testing more. Write application code in a way that's easy to test and treat your test code as code—keep it clean, include comments, and invest time in setup and tooling to make your tests as easy to write and maintain as possible.

Re:There are only four billion floats, so test them all (2014) by seanmcdirmid on Sunday October 11, 2020 @12:54PM (#24747155)
To achieve “defense in depth,” testing components in degenerate non-production situations (and then over engineering them to gracefully deal with those situations) is the only way to get there. Of course, we aren’t building bridges, so maybe it’s less important.
Re:There are only four billion floats, so test them all (2014) by TickleSteve on Sunday October 11, 2020 @01:09PM (#24747300)
Thats not really a "defense in depth" situation.

Defense in depth is regarding layering different types of security, not targeted testing.

https://en.wikipedia.org/wiki/Defense_in_depth_(computing)

Re:There are only four billion floats, so test them all (2014) by nitrogen on Sunday October 11, 2020 @01:15PM (#24747341)
It's an analogy. Multiple layers of defense against bugs.
Re:There are only four billion floats, so test them all (2014) by kortilla on Sunday October 11, 2020 @02:19PM (#24747842)
> The only high fidelity mantra in testing is a bit misguided.

That’s understating it. It’s incompetence because it’s an excuse to not test a huge surface area of edge cases.

Re:There are only four billion floats, so test them all (2014) by kqr on Sunday October 11, 2020 @12:59PM (#24747212)
> My preference is always to actually test parts in as close as possible to the real state, avoiding mocks unless they're necessary. The more "fake" the system is, the less it tells you about how your software actually works.

I've heard this before, but never actually had that problem with fakes in practise. Where can I read more about it?

Re:There are only four billion floats, so test them all (2014) by brianwawok on Sunday October 11, 2020 @01:03PM (#24747238)
Have a production outage. See the code in question is 100% tested. Scratch head a bit. Vow to fake less next time.
Re:There are only four billion floats, so test them all (2014) by nitrogen on Sunday October 11, 2020 @01:17PM (#24747360)
Supposedly the software problems with the Starliner spacecraft were also caused by extensive unit testing under test harnesses without integration testing, as a visible example that reached the news.
Re:There are only four billion floats, so test them all (2014) by xxxtentachyon on Sunday October 11, 2020 @10:48AM (#24746151)
Or, even simpler, just stub out your hash function and to something that always generates the same value and test with that.
Re:There are only four billion floats, so test them all (2014) by rocqua on Sunday October 11, 2020 @11:03AM (#24746245)
Seems like that would make it less of an integration test
Re:There are only four billion floats, so test them all (2014) by treeman79 on Sunday October 11, 2020 @11:13AM (#24746303)
This!

Take over a project with thousands of integration tests.

I trust none of them. Why? Because anything hard to test has been mocked to oblivion.

I have api endpoints that have massive complicated integration tests. I eventually just got source for every project in company and checked to see what they actually did with API.

Re:There are only four billion floats, so test them all (2014) by thaumasiotes on Sunday October 11, 2020 @11:37AM (#24746494)
In this case, I think mocking is the right thing to do. You want to know that you can handle the case where two hashes collide. But for this purpose, you don't care at all how the hashes were generated.

If the website changes its URI hash algorithm, the "honest" test that just supplies two URIs that happened to collide under the earlier algorithm will suddenly become worthless. The mocked test will continue doing exactly what you always wanted it to do -- exercise the "what if the hashes collide?" case in your code.

Re:There are only four billion floats, so test them all (2014) by cle on Sunday October 11, 2020 @10:06PM (#24750804)
You still have to reason through “what if it collides with the real hash function?” How do you know it will behave the same if you don’t test it?

(I can sympathize with both approaches and don’t think either one is better.)

Re:There are only four billion floats, so test them all (2014) by jskdvsksnb on Sunday October 11, 2020 @11:50AM (#24746591)
I have the inverse - I started at a place that has hundreds integration tests that require a database, k8s, the whole shebang. No mocking anywhere.

Tests take an hour to run, are extremely fragile and flakey. Getting your local dev machine set up to run even a subset of the tests is a half day ordeal, and it can change periodically.

If you have external dependencies that don't version their APIs well it may be inevitable that you need to run tests against them. But for developer productivity you should have a fast/non-flakey path to run local unit tests with no network dependencies. It's also great for working on a plane.

Re:There are only four billion floats, so test them all (2014) by njharman on Sunday October 11, 2020 @12:18PM (#24746821)
> Because anything hard to test has been mocked to oblivion.

Agree. Not an integration test. If it has mocks in it, it is not testing the INTEGRATION of/between components.

Mocks are for Unit tests. Where you want to isolate testing one single UNIT from all the other crap it integrates with (which have their own unit tests) After all these unit tests run, you test the whole shebang together with integration tests.

Re:There are only four billion floats, so test them all (2014) by Heliosmaster on Sunday October 11, 2020 @11:09AM (#24746281)
they mention this was a unit test, for which stubbing the hashing function would have been totally fine
Re:There are only four billion floats, so test them all (2014) by jmholla on Sunday October 11, 2020 @11:08AM (#24746273)
They said it was for unit tests.
Re:There are only four billion floats, so test them all (2014) by harpiaharpyja on Sunday October 11, 2020 @06:39PM (#24749739)
I was wondering why GP couldn't have swapped out the hash function for one that just returns a fixed value, making everything a collision. Then it would just be a matter of verifying the system still works under that condition (ignoring the performance hit).

You could even measure the performance hit from that degraded condition and create a heuristic to warn you if something is wrong with the fast path.

Re:There are only four billion floats, so test them all (2014) by wbsun on Sunday October 11, 2020 @02:23PM (#24747871)
Any reason the hash function and the collision handling can’t be separated and mocked for unit tests? What if you decide to increase the bits of hash values or change the hash algorithm?
Re:There are only four billion floats, so test them all (2014) by kortilla on Sunday October 11, 2020 @02:14PM (#24747801)
This is one of those cases where it would have been easier to fake the hashing function in the unit tests with something that collides when you want. What happens if you want a test case where 3+ URLs hash to the same bucket?
Re:There are only four billion floats, so test them all (2014) by nulptr on Sunday October 11, 2020 @12:41PM (#24747036)
I have a question about how you generated collisions.

If each seed is a 32-bit int, and each hash is 60 bits, then you'll have to store 92 bits for each (seed, hash) pair.

With 2^32 (~4.3 billion) (seed, hash) pairs, you'd need 2^32 * 92 bits ~= 50 gigabytes.

But you mention only needing 8-12GB. Is my calculation wrong, or have I missed something?

Re:There are only four billion floats, so test them all (2014) by Thorrez on Sunday October 11, 2020 @01:03PM (#24747242)
Since the hash is 60 bits, a collision is expected after 2^30 tries due to the birthday paradox. So by your math this would be 12.3GB. Although this is ignoring hash table overhead, which I expect to be at least 2x. Maybe they just got lucky.
Re:There are only four billion floats, so test them all (2014) by nulptr on Sunday October 11, 2020 @06:02PM (#24749479)
I think I found a way to do it with ~8GB. As you mentioned you only need 2^30 tries due to birthday paradox.

But, instead of using a hash table to store (seed, hash) pairs, we can just use a list of only hashes. To generate the next hash we use current hash. i.e. next_hash = hash("example.com/" + string(int64(cur_hash))). So now, we just store 60-bit hash values which takes up 2^30 * 60 bits = 8gigs.

The only other problem is detecting if a hash already exists, which I think can be done using a space efficient probabilistic hashset like a bloom filter (and when the bloom filter says the hash already exists, we do a linear scan to find where it exists and we have a possible collision).

Re:There are only four billion floats, so test them all (2014) by penteract on Sunday October 11, 2020 @06:37PM (#24749722)
Actually, using that method, you'd need much less than 8GB, since a collision would mean you'd hit a cycle, so you could store something like 1 value for every 2^10 iterations. When you found a collision with one of those, you could examine the interval more carefully to find exactly where the collision was.
Re:There are only four billion floats, so test them all (2014) by nulptr on Sunday October 11, 2020 @07:25PM (#24749997)
Ahh yes, that's very clever. Thanks for sharing :)
Re:There are only four billion floats, so test them all (2014) by jasonwatkinspdx on Monday October 12, 2020 @12:31AM (#24751406)
This is essentially the idea behind rainbow tables, which let you make a configurable time/space tradeoff when trying to find a hash collision vs a fixed known dictionary/space.
Re:There are only four billion floats, so test them all (2014) by tialaramex on Monday October 12, 2020 @11:14AM (#24755211)
Hellman (yes as in Diffie-Hellman) invented this idea (chaining a reduction function and a hash together over and over) for the improved time/space tradeoff. Hellman's idea actually predates the terrible Microsoft cryptosystems which are famously vulnerable to it, because too many software engineers don't realise Computer Science and thus Mathematics are applicable to their work whether they like it or not.

Oechslin improved on Hellman's idea to make "Rainbow Tables" by sightly perturbing the reduction function with the depth, so that collisions are fleeting unless they also coincidentally happen in the same position on a hash chain, this makes his Rainbow Table far more efficient/ reliable for the same pre-computation effort and it produces the "rainbow" picture in his slide deck.

For my purpose Oechslin's improvement was irrelevant of course because I actually wanted collisions.

But memory fades, it is possible either that I got lucky, that I actually had 16GB of RAM, or that I made some minor efficiency improvements that were enough to make it work.

Re:There are only four billion floats, so test them all (2014) by jasonwatkinspdx on Monday October 12, 2020 @02:42PM (#24757882)
Thanks for the info. I didn't know the history of that at all. And yeah, the head in the sand effect of many devs in relation to crypto is frustrating.
Re:There are only four billion floats, so test them all (2014) by Thorrez on Sunday October 11, 2020 @07:59PM (#24750175)
Interesting, that's similar to rainbow tables.
Re:There are only four billion floats, so test them all (2014) by manulp on Sunday October 11, 2020 @12:58PM (#24747205)
You’re looking for a collision, any of them will do, so your computation doesn’t actually need to go through all the elements. https://en.m.wikipedia.org/wiki/Birthday_attack
Re:There are only four billion floats, so test them all (2014) by bluesign on Sunday October 11, 2020 @02:44PM (#24748014)
You can also truncate hash at 30 bits and use it to address bucket, 12gb then would give you like 3x32 bits, which you can store your candidates. Then compare full hash with candidates.
Re:There are only four billion floats, so test them all (2014) by stouset on Sunday October 11, 2020 @01:40PM (#24747554)
Why not just run the test-cases using a 16-bit hash function?
Re:There are only four billion floats, so test them all (2014) by kortilla on Sunday October 11, 2020 @02:22PM (#24747869)
Likely a C programmer with little experience doing testing with fakes. Most C projects I’ve worked on are horrible at structuring the code in a way that makes it sane to do dependency injection or mocking.
Re:There are only four billion floats, so test them all (2014) by AtlasBarfed on Sunday October 11, 2020 @11:10AM (#24746286)
Did you truncate the hash for dataset size considerations? If you associate data with the hash, I would guess the test data would dwarf the hash size, so why not do 128 or 256 hashes?
Re:There are only four billion floats, so test them all (2014) by tialaramex on Sunday October 11, 2020 @12:07PM (#24746735)
The hash is for content-based addressing. We're going to fasten those 60 truncated bits to a 4 bit tag that says this is a URI in the content-based storage. And then we can use this 8 byte value to refer to that URI twice, or fifty thousand times in our system even if the URI is a hundred bytes long - the only problem is collisions, so we need to be sure we notice if there is a collision and handle that correctly everywhere.