A routine gem update ended up creating $73k worth of subscriptions
serpapi.com
serpapi.com
Yet they are in obvious violation of SemVer expectations, which they declare to follow [2]:
> Mongoid follows versioning guidelines as outlined by the Semantic Versioning Specification, so you can expect only backwards incompatible changes in major versions [sic]
[1] https://docs.mongodb.com/mongoid/current/tutorials/mongoid-u...
[2] https://mongoid.github.io/old/en/mongoid/docs/upgrading.html
EDIT: just noticed the docs I found via Google are marked as “old” in the URL: https://mongoid.github.io/old/en/mongoid/
Are you suggesting they dropped semver in between these releases?
If this is the case, I kind of hope, for maximum irony, that they dropped it as part of a minor version bump.
I will note that this reverses the direction of implications.
In SemVer, you should expect breaking changes only in major versions. (All version changes with breaking changes should be major, but nonbreaking changes can occur in major or minor versions.)
This is not the same as expecting only breaking changes in major versions. (Which would mean all changes in major versions should be breaking, nonbreaking changes cannot occur in major versions, and breaking changes might still occur in minor versions.) .
Even skilled English speakers make mistakes here because English is both very permissive about word order, but also tends to give different shades of meaning to each other. "Only" is a pernicious one. When I got my last book copyedited, fixing the location of "only" was one of the most common changes.
I remember a puzzle which presented a (fairly long) sentence and asked "provide a word that can be correctly inserted at any point in this sentence".
The answer was "only". (Of course the meaning would change according to where the "only" was placed, but still... you'd have a hard time inserting "experience" at every position of a sentence.)
(And in fact, the primary meaning of the first sentence is the one identical to the meaning of the second - you can read it "she only told him that she loved him [and she didn't do anything else]", but by default you'll read it "she only told him that she loved him [and she didn't tell him anything else]".)
The reason for the ambiguity is that the verb is the head of the sentence, and so an "only" placed to scope over the verb has several different options for where the scope "really is".
English syntax is not mathematical logic. The song, “I Can’t Get No Satisfaction” is not a song about someone who is forced to receive satisfaction.
That was kind of his thing, when he wasn’t just inventing words to fit rhyme/meter/mood.
He’s not who you would go to if you wanted unambiguous documentation of the guarantees you were providing downstream users, that wasn’t really his thing.
There's an episode of "This is Pop!" on Netflix that explores why the Swedes write so many pop hits.
One of the arguments presented is that they speak English well as a second/third language so they're less focused on the lyrics making sense and being grammatically correct, and are free to make lyrics that sound like they work but are non-sensical on reflection. It's just the right amount of separation from the language.
Ever since I saw that, I've been listening closer to a lot of the top pop songs over the last decade, and it's fascinating how English is so breakable while sounding right to our mind, but falls apart under scrutiny. (Obviously this is not unique to the swedish writers, even people who only speak English do it as well)
Probably?
Even more recently, Shake it Off by Taylor Swift.
Somewhat along the same lines, I was watching the Get Back documentary on the Beatles and the number of songs they write by effectively scatting and then filling in the words that were their big hits is amazing.
Again they're writing to fit the tune, then in filling the words based on a general theme. It's very different than the songs they write where you can tell someone's sat down and written the words first.
“Not everything that glitters is gold”. (The truly 1:1 translation is “not everything is gold that glitters” but that word order felt very wrong and I believe the change doesn’t change the meaning, does it ?)
How does it sound for a native speaker ?
To avoid ambiguity (whether actual or merely perceived by me), I might say “Just because something glitters, it need not be gold”, but that is not much of an aphorism!
Open any book or paper on semantics (the linguistics kind, not the philosophy or the early-20th-century pseudoscience kind) and you might be surprised :) It’s just that the logic is not that straightforward to get out, and of course there’s semantics (the literal meaning, where “Can you pass the salt?” is a question about ability) and then there’s pragmatics (the thing one is actually trying to communicate, where “Can you pass the salt?” is a request with certain degrees of respect and formality attached to it).
> “I Can’t Get No Satisfaction”
This is probably not a very good example as it’s just (AFAIU) not strictly “standard English” in that it exhibits negative concord absent from the standard grammar (for dramatic effect, although “We Don’t Need No Education” is probably a better case). That is to say, this is a phrase in a language (which is very similar to standard English but not quite the same) where the grammar requires dummy negations on complements of negative verbs—a purely syntactic thing that doesn’t even reach the layer of semantics / logic.
But then many other varieties of English do that (I remember reading somewhere that the dialect that gave rise to the current standard is actually somewhat unusual among other dialects in that respect), so does my native Russian (and other Slavic languages, and some but not all Romance ones), and Japanese actually requires you to negate the adjective when using the adverb meaning “hardly, not very”[1] while at the same time using some double negations as polite affirmations[2].
Red Bull Racing argued that in that sentence "any" doesn't mean "all". As a non-native speaker, this is just mind games for me.
More reading: https://english.stackexchange.com/questions/580131/does-any-...
If you really have a lot of free time, interest in regulations, F1 AND the English language, this deep dive can also be interesting: https://www.reddit.com/r/F1Technical/comments/rkv633/unpacki...
Integration tests might've done the job here.
You'd be surprised how far people go to shoot themselves in the foot.
Sure it was a mistake by OP in TFA but it's an honest mistake any of us could make. And it's a really Bad Move by the Mongoid devs. It'd be a bad move in 1.2->1.3, but by 7.3? I'd just be like "whelp, this is how this function behaves now in perpetuity, I guess just document that it has different semantics than AR" even if an 8.0 was released. This is the kind of change which causes really subtle bugs. It's rarely worth it. Just introduce .chain_or() or something.
It's really just as simple as going to the release notes and seeing if there's a mention of breaking changes or deprecations. If no such thing is mentioned then you're fine, otherwise you go and see if that change affects you at all and just take a little bit more time to test the upgrade. This has been standard practice at plenty of places that I've worked.
It also doesn't mean that it makes the change a particularly good one, but I don't think I can make a bunch of OSS maintainers responsible for my own failure to review an upgrade and test it before rolling it out. In those terms, there is some culpability both with the maintainers and with the library users.
The worst part is that breakage is often far from obvious, and spending good time wondering why everything breaks when it shouldn't is really really infuriating and makes me feel like the entire world of software is hopelessly broken.
The flip side of most breaking changes that are released is some software developer who either wasn't experienced enough to imagine that the change would break someone or else they're dealing with a very hard problem that they're trying to solve and the break change was collateral damage in some edge condition they didn't consider. Many people's breaking changes are someone else's bugfix, and often users fluidly wind up on either side of that condition on different bugs -- screaming about bugs that haven't been fixed for years and then screaming about other bugfixes that broke them.
Update to new minor version. BAM, annotation no longer works, the classes have moved around and I'm staring at an ugly exception. To be clear: these were not private modules, methods, what have you. So we're not talking about the space bar issue here, it wasn't some undocumented or buggy behavior, things just stopped working as the API got shuffled around. At that point this was literally the third semver breakage in the same month, and yeah, this is unusual, but dependencies have stayed fixed since then, it's a headache and it makes me feel like everything is just built on a pile of shifting sand.
TANSTAAFL.
I don't think that's true. I've always thought a breaking change is defined in broad terms as "the public API surface, runtime, and output". Given that, each of these would be a SemVer major:
* remove a publicly-exported function * public function changes order of arguments * drop support for an old version of the language * a function changes behavior significantly (the issue the OP wrote about)
Relying on a specific major version, you should be able to assume none of the above will happen. The following would _not_ be a major version:
* renamed/removed private versions * add extra, optional arguments to public function * add support for new version of language * the xkcd example about CPU usage
In each of these cases, if you rely on non-guaranteed behavior, you should check every version upgrade closely- package maintainers don't make any assurances about those.
I'm not sure how mongoid is developed, but this smells like a bug to me. The `or` behavior started considering a thing it didn't used to, possibly unintentionally. Unfortunately, released mistakes are the one thing SemVer doesn't cover. Testing sufficiently about your own assumptions (in this case, that the user upgraded matches the one found in the query) is the best way to CYA.
That strikes me as exactly the sort of guarantee that's _not_ made by semver. You can rely on that behavior, but then you have to check against every patch version, because that's exactly the sort of thing that could change out from under you. Maintainers shouldn't worry about breaking this case- they never promised to support it in the first place.
I think a lot of those cases should be a major version change, and that's totally fine. We _should_ minimize the number of breaking changes to code people depend on, but sometimes the best fix to the (totally reasonable) situations you outlined above is to make a new major version. Users will have to make changes to upgrade (instead of just bumping the versions), but it won't change out from under them. Some docs about what's changing, why, and how to migrate your code go a long way here.
And you have to understand that the people making these decisions make dozens of them for every single release, and when they make category mistakes that is how you get breaking changes released early. Every single bug when looked at in isolation looks obvious what should have happened given hindsight bias.
And I just don't think open source software, that isn't corporate backed in one way or another, should ever go 1.0 and it should stay 0.x.y and go ahead and break compat every 3-6 months or so as necessary. The cost of SemVer and trying to get it continuously correct on every single patch is a large tax.
We do the same thing and have the exact same experience, unfortunately. Software is a messy business and it's hard to apply strict rules to human process.
I think ultimately SemVer can be used to to accurately communicate if you, a user, are safe to upgrade between versions if applied strictly. Whether or not your users will (or be happy about it) is another ballgame.
(Edit: whoops, I misread you. I thought you were saying APIs are not in scope for SemVer. Now I think I realized you meant, you can have bugs that break things and that's not related to semver.)
Is the semver contract with respect to the end user activities or the API? For Flux and Helm for example, the APIs are supported with semver code (and the interfaces use Kubernetes API versioning)
In Helm, the CLI and user activities are explicitly not part of the semver contract as I understand it, only the API (at least with respect to experimental features like OCI charts for now?)
Unless you have a way to stay up to date with the security of each and every dependency you have, that seems dangerous. There are static security scanners out there which can parse your dependency tree and alert you, but often they have a high noise ratio ( e.g. npm audit), so that only helps so much.
You get free, trivially applied security updates and a much lower chance of breakage. Optionally you can pay for support to have them fix problems in dependencies.
Granted, It'll be difficult with languages like Javascript where many popular dependencies aren't likely to be packaged.
It's still just really bad work from the gem developers though. Ideally you shouldn't ever drastically change the behavior of a method. Just introduce it again with a new name and remove the old one. Yeah it might not be as nice, but it avoids triggering $73,000 worth of incorrect transactions.
That said, strong types can be a godsend for catching accidental breaks, even if it wouldn’t necessarily have helped here. A strongly typed language that has a more disciplined ecosystem is less likely to run into these kinds of issues.
It’s one of the tradeoffs you pick when deciding between languages. I decided the tradeoffs didn’t work well for me and left for compiled languages, for now.
This would not be caught by compiling, strong types or anything else - this is a change in behavior which has been arbitrarily done by the maintainers of the dependency.
> Certainly it has something to do with the ecosystem, if the most popular MongoDB driver in the ecosystem is making breaking changes in minor versions.
How should this be handled? Should a "good" ecosystem vet every single change in every single library that gets updated? Or should there be no libraries and only stdlib instead (wait, I think I know a compiled language which did just that for quite a few years, let me think which one that was again...)
> How should this be handled? Should a "good" ecosystem vet every single change in every single library that gets updated? Or should there be no libraries and only stdlib instead (wait, I think I know a compiled language which did just that for quite a few years, let me think which one that was again...)
This isn’t really about Ruby vs all compiled languages, I mean, Brainfuck is a compiled language and that doesn’t mean I was endorsing Brainfuck to replace Ruby. However I would certainly endorse Go to replace Ruby. Rust too, although using Rust to do web development stuff feels like using a nuclear warhead to open a door.
Anyway, the reason why types is relevant to this discussion is because types are contracts that are statically enforced and the break that occurred happened due to a contract change. This change illustrates that even if you do have types it doesn’t guarantee you won’t have breaking contract changes. However, eliminating entire classes of accidental or intentional contract breaks is an obvious win/win. If it was as complex and obtuse as C++, nobody would bother. But C++ isn’t the only game in town anymore for fast compiled strongly typed languages.
Am I to believe that you hold every other language to this same standard? A single maintainer of a reasonably popular project makes one boneheaded decision in a release, and that's enough reason to denounce the entire ecosystem?
> That said, strong types can be a godsend for catching accidental breaks...
Ruby is a strongly typed language.
You probably mean static types, but even that wouldn't have helped here. This was a behavioral change and did not affect any of the (implicit, in Ruby) type signatures of the function. It would not have affected any of the explicit type signatures of the function in other languages. I say this as an enormous proponent of Rust and static typing.
You are tilting at windmills.
> Generally, a strongly typed language has stricter typing rules at compile time, which implies that errors and exceptions are more likely to happen during compilation. Most of these rules affect variable assignment, function return values, procedure arguments and function calling. Dynamically typed languages (where type checking happens at run time) can also be strongly typed. Note that in dynamically typed languages, values have types, not variables.
Just out of curiosity, exactly what do you even think weakly typed could mean by your own definition of it?
No need to address the other points, because we're talking about anecdotes and not scientific data. Yes, I'm using a single data point as an example about a feeling I have to justify my opinion.
But if a change like this doesn't cause some kind of uproar in the Ruby community, then the community has a problem.
TFA doesn't mention or acknowledge this aspect of the issue, but it did surprise me that they considered a version bump from 7.0.8 to 7.3.3 to be innocent looking.
In fact, these 'minor' releases actually appear to be quite significant and the issue in question in TFA was explicitly documented. [0][1]
I hate to say it but this was just a case of sloppy work on the startup's part. It took me less than 5 minutes to find that info.
[0] https://docs.mongodb.com/mongoid/current/tutorials/mongoid-u... [1] https://github.com/mongodb/mongoid/releases/tag/v7.1.0
Expecting programmers to audit all of their code before a minor update is ridiculous.
It's not the responsibility of OSS maintainer to ensure your software still functions after an upgrade if you're not even willing to spend a modicum of effort to hold up your own end of that bargain. This isn't a big ask, it's why there are changelogs and release notes in the first place; they make it so you don't have to audit the code every time the version changes.
If you're not willing to do that and want unattended upgrades for all dependencies, then this is where the 'no warranty' aspect of that OSS license comes in.
The OSS maintainer has no responsibilities at all so you are right. But if anyone was to blame, it's certainly the library. It's outrageous to radically change a function while keeping the name the same. Because of exactly this issue. If they renamed it you would get an exception which is much nicer.
Some deps you can trust the owner and just carefully review the change log. Even that would have caught this issue, though I'm not sure I'd count this gem as trustworthy.
Maybe you are a very careful developer, but the vast majority out there is not.
Shipping an udate that will corrupt data if you don't read the changelog is very very dangerous.
I see why they did it. Having a method with the same name as in Active Record but with different behavior is also dangerous.
But they really could have handled this better.
These days, it's more a case of updating one thing at a time, and doing the research up front to see what I'm gaining from it or if I might as well stay on the current version. No point updating for the sake of it.
That's 5-10 minutes of up-front, preventative effort that might otherwise become hours of reactive firefighting, and as much time spent on damage control, if it got into production unchecked.
However, you can't rely on this. Many open-source projects fill a niche and become popular when it isn't the intention of the author. This causes a disconnect where the users expect the project to be for them, when the project is really for the author. Mongoid seems to be more in the former category (for the user).
Because some libraries are badly managed, you need to audit all of them.
Nitpick, but I don't think unit tests would have caught this. Integration or systems tests might have caught this however, but those can be much harder to create and maintain in practice, in this particular case.
"As of Mongoid 7.1, logical operators (and, or, nor and not) have been changed to have the the same semantics as those of ActiveRecord. To obtain the semantics of or as it behaved in Mongoid 7.0 and earlier, use any_of which is described below."
Is it just me or is this one of the most terrible breaking changes in a popular, official library ever?
[1] https://docs.mongodb.com/mongoid/current/tutorials/mongoid-q...
After such a problem, I would roll back and never ever update this dependency again.
How people blog and how they feel aren't always the same thing. Professionals tend to be a lot more tactful in public communication.
They knew they were making a breaking change, documented it, and didn't increment a major version number. That breaks the entire point of semver.
Also, this is generating SQL ffs. Like how more nasty of a breaking change could you make in terms of potential impact to live apps that upgrade? The experience in the post is a perfect example of how badly this can go wrong.
If you update your dependencies and ship it based on version numbers alone, you can’t blame the maintainers
OP did not say changes should not be tested.
> If you update your dependencies and ship it based on version numbers alone, you can’t blame the maintainers
OP did not if you update your dependencies and ship it based on version numbers alone you can blame the maintainers.
Even a minor bug fix in a library can expose a critical bug in your own code.
It sounds like the user story is:
- A user runs a query
- The user does not have any queries left in their plan
- They are billed.
- When the user runs the query again they are not billed a second time.
I'm struggling to imagine how the test would have failed to catch this. Maybe it was unit tested but with a database containing just one user? Maybe they got very unlucky and the correct user was randomly chosen?
I agree, this sort of thing seems like it should be extra-well covered, but, y'know, move fast and double charge folks.
The problem wasn't that requesters that should've gotten billed didn't. The problem wasn't even that requesters that shouldn't have gotten billed did.[1] The problem was that clients OTHER than the requester got billed.
You can, of course, write test cases that check your entire database for unintended state changes, but I struggle to find that a reasonable amount of effort. Especially since you'd have to do that for all code paths. That will very quickly cost a large multiple of the 73k this bug caused.
The adequate monitoring and quick response they did here is probably a very good trade-off. Like it or not, production is ALWAYS your last test. Issues are less costly if you realise that, than if you don't.
[1] Though for this specific bug, that scenario would've failed too, and might've triggered an extra look at the code.
I think you're overthinking this. I'm not suggesting they should have looked for any unintentional state changes, we both agree that is overkill (until you start doing FP).
> Though for this specific bug, that scenario would've failed too, and might've triggered an extra look at the code.
Yes, exactly. For this specific bug even the simplest test would have failed, which would have caused someone to take a look at what was happening. You are correct that if the bug had been more complicated, such as causing both the proper user AND an additional random user to be charged, then it's unlikely a reasonable level of testing would have caught it.
> The adequate monitoring and quick response they did here is probably a very good trade-off.
We agree that there is a trade-off here, and if sacrificing some correctness is what it takes to win you a much higher velocity then they probably made the correct trade off; nobody died as a result of this bug.
But... surely you see there are some cheap steps they could have taken which would have caught this bug? Not all bugs, but this specific bug.
- Write integration tests for important behaviors, such as charging users!
- Make sure those integration tests run in an environment which closely simulates production.
The above is zero testing. Update your dependencies and ship it based on version numbers and integration tests is different. In that case as you suggested test coverage may easily have missed something, but there’s moving fast and there’s moving blindly and the second is just wasteful.
So in order to update a dependency you must first write a test that fail on current version and is green on updated version.
The isolated point the commenter was making is that a dangerous breaking change was introduced without the versioning reflecting that fact.
Both these things can be true, and any failure on the part of the startup does not remove the failure of the package versioning. These things are not contradictory so yes, you can blame the maintainers (also) as the guilt or otherwise of the startup does not alter the original versioning fail.
Breaking change: In Mongoid 7.1, when condition methods are invoked on a Criteria object, they always add new conditions to the existing conditions in the Criteria object. Previously new conditions could have replaced existing conditions in some circumstances.
Ironically they also have a breaking change in 7.1.1, so they just don't give a fuck at all.
That said I would normally read 'User.where({id: id}).or({condition1},{condition2})' as 'User where id=id or condition1 or condition2' and not 'User where id=id and (condition1 or condition2)'. Though I could probably get used to either, after all you've also got languages like Lisp where 'or' isn't an infix operator either.
And doing something with any one of the results when you only expect one result to exist is just bad practice.
The problem is, once you deploy to production you have (hopefully) tested this case and rely on the actual implementation.
But this kind of behavior change at best should have introduced different API.
Crazy to think that someone remotely can alter your query ANDs to ORs. That may very well destroy your database data and a whole lot of pain to rollback.
I have a generally low trust approach to all dependency upgrades, irrespective of how minor they are, I've been bitten by enough "minor" changes. But I treat Mongoid as an active adversary who is setting out to break things with every update.
It's the complete opposite approach to that of the Rails core team and those that work on ActiveRecord (as the nearest equivalent to Mongoid). Changes to Rails core are always well flagged with really good advance warning of upcoming breaking changes, frequently with deprecation warnings several versions in advance. There just seems to be a core culture of thought and care towards their users.
This type of library API breaking change on a minor version update basically never happens.
And if it can happen in ruby land, it can happen in JS land too.
Literally happens all the time with Rails. To the point they decided to call their versioning schema “shifted semver” to afford themselves API changes on minor versions: https://guides.rubyonrails.org/maintenance_policy.html
Good luck if you are using Rails and ecosystem and you expect any sort of sensible versioning.
First I get that people are used to SemVer, but it is unreasonable to assume all projects follow it. And yes in semver terms, you can simply assume that Rails X.Y is a major release.
Then, any breaking change in Rails must first emit deprecation warnings, so unless you are jumping one version, this kind of scenario shouldn't happen with Rails itself.
Also note that Ruby (MRI) itself more or less behave the same regarding versioning, deprecations and breaking changes. First emit warnings, then break.
The language and framework, by your own statement, are pushing breaking changes at a yearly rate. That’s a horrible developer experience.
> pushing breaking changes at a yearly rate. That’s a horrible developer experience.
That's your opinion. I much prefer these bite sized yearly changes to much bigger changes over longer periods. e.g. Ruby with this strategy never had the big divide Python 3 had with its much larger change.
You may not need to be confrontational...
> there are other ways of versioning software
I'm sure there is. But some features and other improvements sometimes require to deprecate and remove some older features.
Each project will chose its own tradeoff between bringing the improvement faster vs keeping compatibility longer.
Are packages that don't follow semver allowed to be released?
Haskell asks for PVP for example.
And even with very strict typing, the breaking change showcased in the article wouldn't have been caught, the API stayed the same, it just behave differently. Not every backward incompatible change is as simple as a function signature change.
I really don't think this is a scalable approach and was really surprised that someone took the time to check my dependencies personally. My experience may just be rare.
I meant more on the human side than automated. Haskell doesn't have that much to do with formal verification in usual use. I'm not sure what the best policy for a good yet vibrant package ecosystem is.
We have used Mongo for good reasons, it's been appropriate for our workload, and we have used Mongoid since the inception of the product long before I joined the company. Replacing Mongoid at this stage would be a massive piece of work.
I find Mongoid's documentation is vague and lacks important detail, and the API itself violates the principal of least surprise frequently enough to be a problem.
It really is the ugly stepchild of ActiveRecord, in whose image it was created, and which by comparison has been a pleasure to both use and to manage.
He refused to fix it because, "After all, we had several hundred kilobytes of source code, and maybe 3 installations...."
https://lkml.org/lkml/2012/12/23/75
This style is indeed controversial but honestly, these kind of situations seem apt for such a chewing out. Backwards compatibility seems to be one of those things that people regularly compromise despite it repeatedly hitting back. For a database driver of one of the most popular databases in the world, it should not be taken casually. I wish more tools adopted the first rule of kernel maintenance - "If a change results in user programs breaking, it's a bug in the library. Never EVER blame user programs." (pp)
Breaking changes, major version update, bug fixes, minor version update, and think really hard about the pain the major version update will cause. It seems so easy...
The problem comes when applications depend on a bug. The library maintainers don't necessarily know about this, but it's handy to think further ahead and understand what the effect of an update is. That's why one line fixes take time - thinking through the implications.
Perl 5 vs 6, Python 2 vs 3 are good examples of breaking major changes to languages, and the various fallout that happens, both good and bad. It's amazing how far c++ has come without such a major bifurcation
When someone I work with writes something like that example:
if (a==b & c==d)
... I tend to (want to) lose it.I have decades of continuous C/C++ experience and I have no earthly idea what the relative precedence of & and == is. Don't know, don't care. Use parentheses whenever it looks even remotely like they'll help clarify the expression. They're free. (Floating-point precision shenanigans aside.)
It also just signals that they aren't really that interested in hearing from users. People who crave feedback and bug reports go to where the users are rather than making the users come to them. Even if they use Jira internally, they'll do things like monitor stack overflow and provide support on Github.
JIRA is not made for humans.
Even when you use the right email you’re still playing telephone between some poor support person and a dev. Absolute trash-tier company, I have a backlog item to completely scrap their SDK from my apps I just haven’t gotten around to it.
1. Don't use MongoDB.
2. Don't use high level ORMs. Stay (reasonably) close to SQL. And yes, it should be SQL. Almost certainly Postgres.
3. Especially don't use Mongoid.
The first thing I'd be concerned about is how do you manage changes? Code is easy to version control, deploy and roll-back. Database state and things such as triggers, stored procedures, etc, less so.
MySQL is a bunch of different engines that share a meaningful subset of SQL.
Somehow, Postgres got more popular lately, probably because it makes migrating from Oracle easier.
In my years of consulting I have yet to see someone not using just InnoDB in MySQL.
Mongo is a nightmare when things get more complex, document based DBs don't work well. It's hard to say why, but strange things tend to happen
Ether use something like Firebase which manages everything for you, I use Firebase extensively for almost all of my side projects.
It's basically magic. I'm so hooked I ended up using Firebase even though I needed to talk to AWS apis as well. Not fun...
Edit: I would NOT use Firebase for a corporate project, there's a very strange feeling of not really being in control
In my experience using any ORM will bite you sooner or later and you'll end up bypassing it and writing SQL manually. Last case we had were a couple queries that went from double digit seconds to instant as we rewrote them natively.
Seconded. I prefer high level abstractions in general. However when it comes to ORMs; I wouldn't go anywhere near them. Admittedly there's a sweet spot where you could hand-pick ORM libraries such that you hand-code the queries and they only map query results to your objects. But to achieve that you need to wade through documentations and some undocumented APIs. So developers usually go all-in on ORMs. A big mistake.
4. Don't design an API that mixes fluent style (methods are like infix operators) with conventional style (methods are like prefix operators).
Fluent methods should never take multiple operands. It's a terrible, horrible, no-good, very bad idea.
This is a problem in Haskell with infix operators being directly defined I would argue.
I disagree that infix and prefix should not be mixed. Although there is a special hell for people who use + with non-commutative operators.
> Don't use MongoDB.
Don't use MongoDB for something that should be in a relational DB. Use it as the document DB it is. Use the right tool for the right job. Mongo and other document DBs have their place.
> Don't use high level ORMs. Stay (reasonably) close to SQL. And yes, it should be SQL. Almost certainly Postgres.
Hard disagree. I've used many of the ORMs in different popular frameworks. They work well, save time, and are especially good for simple queries. They have their place, just as SQL does.
> Especially don't use Mongoid.
Unfortunately it's the go-to Ruby library for mongo. This could be an argument for not using mongo with Ruby. It could be an argument for creating an alternative. But simply saying don't use it isn't practical for many people.
Not a MongoDB expert, but you don't really need an 'ORM'[0] do you? I thought it spoke JSON natively like couch.
If I was in this project I might have argued strongly for just doing that. Others might have argued back telling me that we can't possibly send JSON to a thing that expects JSON that's too low level, let's rely on this library by some guy instead.
Sorry, flashbacks. Look libraries that do stuff for us are great. But let's make sure they're worth the cost of admission.
[0] yes I know it's not really an ORM because it's not a relational store but you know what I mean
If I cancel or disable my account for a service I don't expect them to be able to charge me money in the first place!
Are they keeping card authorisations (or direct debit mandates, or whatever other mechanism) for customers that don't even have an account with them any more?
That sounds like an... ...interesting way to do business, but perhaps I'm misinterpreting this.
For the vast majority of cases all you need to charge a card is the 16 digit number on the front and an expiry date. Pretty much everything else is optional (CVV, Name, Address etc), but opens you up to stupid levels of liability in the disputes process if you didn’t include it in the original payment request.
Joking aside, I don't see how a signature is of any use provides your card is supposed to have yours on the back, so anyone having your card has your signature too. PINs have been a thing since I've had a card (2011), and I don't see why anyone still relies on signatures for card authentication.
The way things work in developed countries is the following - payments under 30,50,100 euros (depending on the country) are contactless where you just tap your card on top and it's done, and for more you have to insert the card and type your PIN. Or you can just use your phone for any amount by tapping it.
No, the US :)
Signatures are still common enough when paying with a card in the US, for whatever reasons.
In fact, why does any customer in their database have ANY connection to Stripe after they’ve canceled? I haven’t used stripe but I’m guessing there’s some sort of customer ID/token. If they’ve canceled why are you retaining that?
So that’s two things. Even if the code encountered a bug like they did here, it shouldn’t of been possible to actually trigger a new subscription.
What’s happening here is that there’s a user who has a card on file and a cancelled subscription, and an erroneous new subscription is being created under that user.
Whether or not it makes sense to remove the card from the user account depends on whether or not that user could meaningfully have multiple subscriptions or expect to renew in the medium-term future.
As a user, if I delete my account, I'd definitely expect my payment details gone. However, if I just disable my subscription, I'd expect they keep my payment details on file in case I reactivate.
Maybe it's a culture thing. I have this oldschool thing about data meeting invariant criteria and I dislike not using a RDBMS.
Someone joked that the wave of the future will be SQL, and that those start-ups that use it will believe that they have superpowers over the ones that don't. Transactional integrity, complex queries, constraints, what's not to like?
Same. As another 'old school' person, I've been around long enough to see that data outlives whatever the current application is of the day. This means that storing data in something that is standard, has good tooling, and can have some guarantees ends up critical.
2013: https://aphyr.com/posts/284-jepsen-mongodb
> To recap: MongoDB is neither AP nor CP. The defaults can cause significant loss of acknowledged writes. The strongest consistency offered has bugs which cause false acknowledgements, and even if they’re fixed, doesn’t prevent false failures.
[N.B. I thought this was just an edge case until it happened to me in production. Luckily, I had separate storage of ground truth, but I very easily could have been in real trouble.]
2015: https://aphyr.com/posts/322-jepsen-mongodb-stale-reads
> In this post, we’ll see that Mongo’s consistency model is broken by design: not only can “strictly consistent” reads see stale versions of documents, but they can also return garbage data from writes that never should have occurred. The former is (as far as I know) a new result which runs contrary to all of Mongo’s consistency documentation. The latter has been a documented issue in Mongo for some time. We’ll also touch on a result from the previous Jepsen post: almost all write concern levels allow data loss.
2017: https://jepsen.io/analyses/mongodb-3-4-0-rc3
> MongoDB has devoted significant resources to improved safety in the past two years, and much of that ground-work is paying off in 3.2 and 3.4. Dirty reads, which we covered in the last Jepsen post, can now be avoided by using the WiredTiger storage engine and selecting majority read concern. Because dirty reads can be written back to the database in read-modify-update cycles, potentially causing the loss of committed writes, users of ODMs and other data mappers should take particular care to use majority reads unless making careful use of findAndModify.
Isn't that against the Google search Terms of Service???
If google wanted there to be a paid search api, I'm pretty sure they would just provide one.
Also, I'm pretty sure I've seen these types of startups before, and then they vanish quickly thereafter....
They do[0]. It's not as complete though.
[0]: https://developers.google.com/custom-search/v1/introduction
But this breaking change is on another level of subtle, and it impacts their entire application at a very basic level, but in a way that would be exceedingly hard to detect in almost every case. Frankly, I don't think any reasonable level of testing would have been thorough enough to catch such a subtle issue.
> I don't think any reasonable level of testing would have been thorough enough to catch such a subtle issue.
If there was a staging environment which tried to match production as closely as possible, and end to end tests of this feature were run, then this bug would have been caught. That doesn't seem like an unreasonable level of care for something as sensitive as billing.
(Of course, "everything is working" might not be true, in which case you could be trading one bug for some others. But the sad state of "modern" software development is a rant I won't go into here...)
the best change is no change, the second best change is a small change
I suppose if they'd done the update into a test environment like a regular release then it's far more likely these issues would've come out there and it wouldn't have been so stressful.
How many CVEs over the years are because of someone screwing up their operator precedence in permissions checks?
Eventually you're going to need to upgrade X for a security fix, but the new version of X isn't compatible with the version of Y you are using, and upgrading that would break Z, and Z would break your app here and here. Now you're spending a couple weeks trying to do the upgrade to get a security fix that was 0 day two weeks ago, when you should have been writing things that actually advanced your app instead.
I keep my dependencies up to date, and consider it technical debt when they are not, and would never want to go back.
This can work for internal software (see people still running old Windows and IE for some random internal app), but breaks down for software with public access. The problem is when a bug is found, and one will be found, the farther behind the current release you are, the harder it is likely to fix.
I prefer to update my dependencies every so often so that I'm never too far behind if I'm suddenly forced to update for some critical security issue.
"We recently became aware of some erroneous subscription renewals made by our platform and traced the root cause to a major bug in downstream database technology affecting a very small number of accounts. Nonetheless, we working hard with our database provider to resolve the issue.
In the meantime, if you are affected and believe you might have an unsolicited subscription, please contact our billing department by fax at 212-345...."
Hindsight is always 20/20 of course.
And I mean like 98% or more. I have worked in IT for over 20 years, and you actually have to fight clients managers to get time to do it.
I work in healthcare. I wish I had problem like that, instead, I just today had to fix something that was running on unpatched ubuntu 14. I know that there are servers running unpatched log4j where people are in "talks" who will pay for "upgrade" etc.
> In Mongoid 7.3.3 , or() now means filter documents that contain any of the argument conditions OR any of previous method conditions
It actually sounds like they "fixed" it to work the way you'd expect... but changing an API like this one in this way is extremely dangerous.
I wouldn't call what you have a code smell, you coded it correctly according to the Mongoid API at the time.
Mongoid is at fault here. An API that changes the behavior of a function are core as "or()" in this way is pure insanity - even if they were trying to make it do what it really should have in the first place, or if it were a major version, and they documented it well.
Note to self - avoid Mongoid.
Bookmarking this for future reference.
it is completely unacceptable to change semantics like this in a post 1.0 minor version
But... CHANGELOG.md with BREAKING CHANGE sections and a major version bump, maybe a DEPRECATED lint and runtime warn() ahead of time, and bam, no surprises for professional teams. For more than that, service contracts are things :)
These sorts of changes do in fact happen fairly frequently and developers need to be aware that they can't blindly accept upstream dependency changes.
Because Rails explicitly has a versioning policy where minor versions are equivalent to SemVer major versions (but with deprecation notice in a previous minor version) and where major versions are also SemVer major with subjective significance distinctions; they call it “shifted SemVer”.
https://guides.rubyonrails.org/maintenance_policy.html
This is somewhat obnoxious, but not as bad as saying you use real SemVer and then brazenly breaking things in minor releases.
Changing semantics of a query operator is something worth saving for a "big major" update, if you change it all.
Some breaking changes are really obvious and easy to catch. Others can introduce pernicious bugs which slip through tests. The change Mongoid is solidly the latter. And yeah, they say they use actual Semver.
Arguably, it was so important that they did a big major update just to deprecate it.
Semvar is beautiful bc it lets you be explicit. Likewise, end users can judge "wow major version 27 in as many months, maybe not so good for us." We had a gov customer today upfront about needing slow updates, and same deal -- maybe our SaaS and OSS libs are too fast moving, so they are probably better off with our enterprise distros.
I'm a big fan of Linus' mantra: we don't break userspace.
But that's company code we get paid to be stable on. Very diff story for OSS we at most contribute to, we don't assume that's part of the social contract.
The average OSS project is just ~1 fulltime maintainer, maybe 2, with tiny drive-by contributors: there are great studies measuring this. Even most popular and long-lived OSS projects are more like that than Linux. The exceptions are inspiring, but most (my bet, I don't recall stats here) seem to be open core largely driven by 1 company. Something foundational with many contributors under open governance like Linux is better to discuss as an abnormality ("why can't the typical project grow to this size, maturity, tooling, and stability?").
So when talking about modern OSS, the typical case of looking through a pip or npm tree is NOT big shops and instead something closer to a network of volunteer passion project. From such individuals, I'm thankful for semvar, CHANGELOG, CI, and a few niceties like that. From there, the history of BREAKING would signal the project moves too fast for us or with too big swings, or looks more acceptable.
It makes maintenance a highly predictable endeavor:
Update T+0 == dev instance
Update T+1 week == test instance
Update T+2 weeks == prod deployment.
UAT inserted as needed when there are user-facing changes.
Might obviate the update and pray nature of SemVer. You should still have comprehensive test though.
Maybe this:
1. In the first release, introduce a config value which enables the new behavior.
2. In the next release, print a warning or refuse to compile if the config option is not set.
3. In the final release, make the new behavior the default.
Plenty of time to adjust your code to the new behavior even if you don't read the changelogs. But it won't work if several releases are skipped during updates.
In this case, since we know from the article that there are 474 charges, and a total of $73k processed, 474 * $0.30 + $73k * 0.029 comes out to $2,267 that the company owes Stripe
OP, its great that you apologize to your users but your write up does not capture any actions that you would take to avoid recurrence of these issues in the future.
Also, you always need to be extra careful with payment code. We test it multiple times before deploying it.
Granted all this wouldn't had helped OP, still you need to test everything before upgrading and deploying.
Does anyone know how this works behind the scenes at scale? How do they get around Google trying to stop their scraping attempts? Just lots of proxies and some way around captcha challenges?
>In addition, each API request runs in a full browser, and we'll even solve all CAPTCHAs. Mimicking completely what a human will do.
Wow how would they do that?
I know there's companies doing similar things and I'm not saying they should get in trouble, but it feels so risky basing a business around it, unless I'm missing something. Lots of companies seem to do similar scraping to get SEO data for example that Google probably has an interest in preventing.
You may as well just have swapped the behavior of "+" and "-", this is crazy.
Note that my comment isn't about the OP, but about the root cause of the breaking change: the driver.
That they ended up returning it is good but either the amount was meant to have some kind of significance and before they returned it they received it.
I'm well aware of the effects of various kinds of risks to merchants.
Depending on how their testing environment was set up, they might not even have enough database users in their DB during automated testing to notice this bug if it did trigger.
“It should not call the Stripe API if customer has credits remaining.”
I want to clarify some things. This code was running on staging for three weeks before the deployment to production. There were four application errors on staging that were related to the problem with subscription renewals.
I and the author of the pull request have made three mistakes:
- haven't carefully read code in all methods from the backtrace of four app errors on a staging environment
- upgraded several gems at once
- didn't review the changelog of mongoid gem
As multiple people commented here, integration tests for renewals should've been caught that bug. We hadn't a test case when all user's subscriptions are checked after the renewal of a specific test user.
So the lesson here is to not yolo minor upgrades of dependencies and avoid mistakes above.
Mongodb is good for your high-school app or prototype demos not for production SaaS that debits credit cards.
It is ironic to me that software teams have code review processes and seem to trust each other less than random people they have never met on the internet. Every new dependency, you need to read the source. Read it completely.
Every new change in every dependency needs to be reviewed as well. I’m a big fan of forking all dependencies and using only those forks in my code. Then, do upgrades via standard rebase or pull requests on that fork.
If you have dependencies that are too large to do this with your time, you probably should avoid them in the first place.
To me this post is more about lack of due process in Serpapi itself.
Peter: since when did they change the meaning of for to from?
I would almost expect there to be problems jumping up 3 minor versions even.
Proactive would be tests failing before going live. The action described is totally reactive.
There might be an argument to make that your test suite could have caught this, however.
Unfortunately it’s crazy difficult to even get such a small addition committed upstream, but that’s a different story
The query here returned where id = xxx or renew_locked_at >= xxx or renew_locked_at is null. That will return more than 1 result. If find_one actually did what the name says it does, find 1. Not 0, not 2, not 475. This issue would've never existed.
Both mongoid and activerecord (don't remember if this was the case with hibernate / jpa) will not throw an exception if there are more than one records. They will check for zero results, although it would just result in an NPE if they didn't..
Besides this, I'd have made it more explicit by doing something like: where(xxx, or(yyy, yyy)). This is still an implicit 'and', but chaining always seems confusing to me, so I never use it like that.
Mongoid shouldn't have done such a change, even with a major upgrade. At least not in this way.
IMO, both the author and mongoid set themselves up for issues.
I like to throw around exceptions and asserts. I like to fail fast. Strictness is easy to handle for a programmer. Only external human input should/could be handled less strict. Unfortunately, many disagree..
It's different with other types of software (client side apps), but for applications that have no state, or a state that can easily be recovered (webapps), it seems plain stupid not to fail fast whenever something's wrong
Both raise an exception unless a single record is found.
Of course this would not make a difference when working with mongoid... I also like the fail fast approach.
https://blog.saeloun.com/2021/03/16/rails-adds-sole-and-find...
With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody. -- Hyrum's Law
Semver doesn't mean "guarantee no breaking changes in minor" - that's impossible - and people love to point this out for whatever reason. It totally misses the point. I think most engineers have a decent intuition most of the time about whether a change is breaking or not.
"It's semver bro"
No.
To me this is basically a malicious hack of a dependency under the excuse of conforming it to activerecord
I would investigate contributors to this version and the code review and discussion
This could be actually malicious as someone could know or suspect that an app is vulnerable to this fundamental change
1. Build it out as a standalone operation/class/module so it has more ceremony around it, while also being limited in scope and easy to audit.
2. Continue to utilize the DB/ORM's filtering/querying to grab data as was done in this case
3. Additionally, when it comes down to performing the big io/side-effect, ensure additional checks are in place that confirm the data I am working on matches the query that was used to ask for the data. So in each customer loop I might double check that this customer is indeed needing an upgrade/etc. It can be slower, but it is worthwhile.
I am joking of course :D
Integration test on the other hand…
Or Mocking one level further down, not sure about ruby but in Python there is a mongomock package which simulates most of the mongo queries in memory, so an ORM on top of raw queries does not need to be mocked. Because it simulates the database rather than just EXCPECT_CALL it’s also invariant to how you chain operations as long as end result is the same.
Renewing automatically subscriptions
Stripe access somehow from gem
Hmmmmm