361 comments

[ 4.2 ms ] story [ 294 ms ] thread
> It involves bad practices and errors from multiple parties in a world that might seem

> foreign to the "Silicon Valley" world but paints an accurate picture of what

> development is for small IT companies around the world

Everybody makes mistakes even in the "Silicon Valley" world, but such problems cloud be easily caught by testing (which he did but it was restricted to the first page) and performing a simple dry-run.

Exactly, everyone makes mistakes. Sometimes huge ones. In hindsight or on the sidelines it's always easy to point out a few technical things that WOULD HAVE avoided catastrophe, but does that help? I think not (aside from a cautionary parable for interns).

Things are complicated, people are human and forget things, there are pressures to "get it done" and override the guardrails. Everybody has horror stories. Some worse than others. Welcome to the OP's day of horror. I would think "Silicon Valley" dev-ops horror stories make this one seem like a triviality.

[deleted]
I'm baffled by this too. Unnecessary bridge burning I'd call it.

It's not even necessary to the story.

Its explained on the first line: " I'm a Junior Developer with less than one year of actual experience. Some of the things that might seem obvious to some might not be so for me". I guess it applies to this, too, not just the technical aspects.
I might've missed it, but I don't think that line existed when this was first posted.
You're right and I edited the company's name (might be too late but better this way). That said I'm not very happy with the experience of working for TheCompanyTM anyways so I'm in the process of switching jobs.

Thanks for the comment :)

Talking bad about your employer is great for finding a new job. Companies are eager to hire people who bad-talk them.
He doesn't talk bad about his employer. He talks bad about his employers client.
Tech is like any other human endeavor. People talk. People change jobs and still like the people in the place they left.
Yes exactly. Which is why I wouldn't touch anyone who has no criticism for the systems OR culture of a place they've been before.

Nowhere is perfect. If people can't be honest about the flaws then they're useless.

Of course you can hire whomever you want. I would hire someone who has criticism about what he had done in the past and what they have learned. Nobody is perfect. But people with no self reflection blaming others and their employer? No thank you.
I would take down the post entirelly.

Your current job is linked in your CV.

And try emailing the hackernews mods asking them to take this post down.
As sibling comments indicate, I would advise emailing HN mods to take this post down and remove it from your blog and post it on an anonymous one. Here are the problems you will face:

1) Your current blog has your current employer + client linked to it. 2) Your github has your real name. 3) All of these have been crawled/archived.

None of this bodes well for your career in the future. While I think your blog post is a great war story, it's really not a good idea to post it on your main account which can be traced back to your real name and CV because it will come up the next time you apply for a job.

Unfortunately, even if it illustrates a great deal of ingenuity and creativity on your part in fixing a mess you made, many folks will take one look at it and be judgmental. You have to manage your reputation online and be careful.

> but at the time the code seemed completely correct to me

It always does.

> Well, it teaches me to do more diverse tests when doing destructive operations.

Or add some logging and do a dry run and check the results, literally simple prints statements:

    print("-----")
    print("Downloading videos ids from url: {url}")
    print(list of ids)
    ...
    ...
    ...
    # delete()  dangerous action commented out until I'm sure it's right
    print("I'm about to delete video {id}")

    print("Deleted {count} videos") # maybe even assert
    ...
Then dump out to a file and spot check it five times before running for real.
This was my first thought too. Another think I like to do, is to limit the loop to say one page or 10 entries and check after each run that it was correctly executed. It makes it a half-automated task, but saves time in the long run.
I do this, too, but I also take a count of the expected number of items to be deleted as well. If my collection I'm iterating over doesn't have exactly that number of objects I expect, I don't proceed.
Yes, I find command line tools that have a "--dry-run" flag to be very helpful. If the tool (or script or whatever) is performing some destructive or expensive change, then having the ability to ask "what do you think I want to do?" is great.

It's like the difference between "do what I say" and "do what I mean"...

The rule we have is that anything that is not idempotent and not run as a matter of daily routine must dry-run by default, and not take action unless you pass --really. This has saved my bacon many times!
Deleting actually is idempotent. Doing it twice wont be different from doing it once.
Deleting * may not be though. Your selection needs to be idempotent.
idempotency means that f(X) = f(f(X)). Modifying the X inbetween is not allowed. Is there really an initial environment where rm * ; rm * ; does something different than rm * once?
In the case of any live system, i would say yes. Additional, and different, files could have appeared on the file system in between the times of each rm *.
* is just short hand for a list of files. Calling rm with the same list of files will have the same results if you call it multiple times. That’s idempotent.

Your example is changing the list of files, or arguments to rm between runs. Same as pc85’s example where the timestamp argument changes.

In addition to what einsty said (which is 100% accurate), if you're deleting aged records, on any system of sufficient size objects will become aged beyond your threshold between executions.
Right. You can kind of consider the state of a filesystem on which you occasionally run rm * purges to be a system whose state is made up of ‘stuff in the filesystem’ and ‘timestamp the last purge was run’.

If you run rm * multiple times, the state of the system changes each time because that ‘timestamp’ ends up being different each time.

But if instead you run an rm on files older than a fixed timestamp, multiple times, the resulting filesystem is idempotent with respect to that operation, because the timestamp ends up set to the same value, and the filesystem in every case contains all the files added later than that timestamp.

> Is there really an initial environment where rm * ; rm * ; does something different than rm * once?

if * expands to the rm binary itself, maybe.

How is the system different after the first and after the second call?
If there is an rm executable in the current directory, and also one later in your PATH, the second run might use a different rm that could do whatever it wants to
This is actually a likely scenario, as it is common to alias rm to rm -i. Though your bash config will still run after .bashrc is nuked, some might wrap with a script instead of aliasing (e.g., to send items to Trash).
Early in my career I used --yes-i-really-mean-it and then a coworker removed it with the commit message "remove whimsy".

T'was a sad day.

Going further, make it dry run by default and have an --execute flag to actually run the commands: this encourages the user to check the dryrun output first.
That's what I like about powershell. Every script can include a "SupportsShouldProcess" [1] attribute. What this means is that you can pass two new arguments to you script, which have standardized names across the whole platform:

- -WhatIf to see what would happen if you run the script;

- -Confirm, which asks for confirmation before any potentially destructive action.

Moreover these arguments get passed down to any command you write in your script that support them. So you can write something like:

    [CmdletBinding(SupportsShouldProcess)]
    param ([Parameter()] [string] $FolderToBeDeleted)
    
    # I'm using bash-like aliases but these are really powershell cmdlets!
    echo "Deleting files in $FolderToBeDeleted"
    $files = @(ls $FolderToBeDeleted -rec -file)
    echo "Found $($files.Length) files"
    rm $files
If I call this script with -WhatIf, it will only display the list of files to be deleted without doing anything. If I call it with -Confirm, it will ask for confirmation before each file, with an option to abort, debug the script, or process the rest without confirming again.

I can also declare that my script is "High" impact with the "ConfirmImpact = High" switch. This will make it so that the user gets asked for confirmation without explicitly passing -Confirm. A user can set their $ConfirmPreference to High, Medium, Low, or None, to make sure they get asked for confirmation for any script that declare an impact at least as high as their preference.

[1]: https://docs.microsoft.com/en-us/powershell/scripting/learn/...

I’m a bit confused (because I didnt read the docs)… does calling it with “—whatif” exercise the same code path as calling without, only the “do destructive stuff” automagically doesn’t do anything? Or is it a separate routine that you have to write?

Cause if it is an entirely separate code path, doesn’t that introduce a case where what you say you’ll isn’t exactly what actually happens?

It's the first option. And yes, sometimes you have to be careful if you want to implement SupportsShouldProcess correctly, it's not something you can add willy-nilly. For example, if you create a folder, you can't `cd` there in -WhatIf mode.
Well, just read the...

> because I didnt read the docs

Ouch.

> Or is it a separate routine that you have to write?

If you are writing a function or a module what would do something (eg API wrapper) then of course you need to write it yourself.

But if you are writing just a script for your mundade one-time/everyday tasks and call cmdlets what supports ShouldProcess then it works automagically. Issuing '-whatif' for the script would pass `-whatif` to any cmdlet what has 'ShouldProcess' in it's definition. Of course if someone made a cmdlet with a declared ShouldProcess but didn't write the logic to process it - you are out of luck.

But if have a spare couple of minutes check the docs in the link, it was originally a blog post by kevmarq, not a boring autodoc.

All my tools that have a possible destructive outcome use either a interactive stdin prompt or a --live option. I like the idea of dry running by default.
Yeah, that is what I recommend too.

Instead of performing the dangerous action outright, just log a message to screen (or elsewhere) and watch what is happening.

Alternatively, or subsequently, chroot and try that stuff on some dummy data to see if it actually works.

I was involved with archiving of data that was legally required to be retained for PSD2 compliance. So it was pretty important that the data was correctly archived, but it was just as important that it was properly removed from other places due to data protection.

This is basically the approach that was taken: log before and after every action exactly what data or files is being acted on and how. Don't actually do it. Then have multiple people inspect the logs. Once ok'd, run again, with manual prompts after each log item asking to continue, for the first few files/bits of data. Only after that was ok'd too did it run the remainder.

In other things I've worked on, I've taken the terraform-style plan first, then apply the plan approach, with manual inspection of the plan in between.

Yes, I love the idea of the Plan Apply.
Once we get used to doing same thing multiple times a day, it doesn't matter if the log shows that we're about to take a destructive action, we'll still do it. Only thing that is foolproof is to not take the destructive action because people make mistake, it's human nature. I don't know how this can be implemented, may be encrypt the files, take a backup in some other location (which may not be allowed).

Multiple reviewers here didn't catch the mistake

https://www.bloombergquint.com/markets/citi-s-900-million-mi...

That form had a couple weird checkboxes with odd wording. It is a famous mistake, but also rather understandable just because the form was cryptic.
Because everyone assumes that everyone else is looking at it more closely than they are. “I’ll just do a cursory look since I’m sure everyone else is doing a in-depth look.” Narrator: nobody did an in-depth search.
While this is a huge issue, a solution (well, a partial mitigation) I've seen and used is the "Pointing and Calling" technique. The basic idea is that you incorporate more actions beyond reading and typing or pressing a button—generally by having people point at something and say aloud what it is they're doing and what they expect to happen.

It's used rather extensively in safety-critical public transportation in Japan [1] and to a lesser extent in New York (along with many other countries) [2]. This can easily extend to software without overcomplicating by just setting the expectation that engineers, Q&A, etc. do this even when alone.

[1] https://www.atlasobscura.com/articles/pointing-and-calling-j...

[2] https://en.wikipedia.org/wiki/Pointing_and_calling

Hell, GitHub does that to an extent, with the "type the name of this repository to delete it" prompts. Typing the name of the repository isn't exactly perfect, but it's an interesting direction.
There was a thread recently about a repo that accidentally went private and lost all of its stars because of confusion with GH teams vs GH profile readme repo naming. I think this type of prompt is very useful for explicitly preventing the rare worst case scenarios but the problem is making any type of prompt "routine" so that our brains fail to process it.
The suggestion in that post about how to fix it is good, and mirrors one I read in the Rachael by the Bay blog - type the number of machines to continue:

https://rachelbythebay.com/w/2020/10/26/num/

The take away by both is there is actually something to do which can wake people up when the stakes are high, and they might not be doing what they expect.

And most importantly, don't let yourself get into the habit of copy pasting the value
I wonder if your could print some non visible characters in there to taint the copied value in some detectable way.
yeah, that would possibly stop the copy and paste problem. to make it robust they would need to use a string of a few non-visible characters but that would fail if the browser's clipboard system doesn't copy them over for some kind of privacy initiative. might be another way it fails that I can't think of right now.
Prompt in words, but expect the value in numbers, eg: "Twenty-five" and the box requires you to type "25"? At least this specific case, it would require you to type it.
I always copy-paste into that box as well, they should probably make at least an attempt at disabling pasting into it
Azure has the same when deleting a database just a verify this is the correct one by typing the db name
I heard of this technique, but unfortunately I don't see how it can be easily applied in software engineering/devops.

Also, I now realized that aviation checklists seem to tend to be done similarly with gestures - at least from what I saw on YouTube, not sure if that's representative or only used during education (?)

Spelling out loudly the command you are about to execute and explaining the reasoning behind it can help a lot too.
Ok, but am I to do it on every single command I do on my terminal? Or on which ones specifically? If the problem we're trying to solve is that I can sometimes overlook the "dangerous commands" among "safe ones", by definition of overlooking it won't work if I tell myself to "spell out the command only in case of the dangerous ones", no?

I'm honestly trying to think of the way how I could approach this for myself, just I don't see a clear solution yet that wouldn't require me to spell out everything I type in my terminal window.

> Multiple reviewers here didn't catch the mistake

Sure, but we can only do so much. I find its good bang for buck and alternatives that might prevent that are not always available, so we do the best we can. You gotta make a call on whether its enough or not.

I'm a fan of doing things temporally so data is very rarely actually deleted from the database. Most of the time, you just update the "valid_to" field to the current time. Sometimes real deleted are required such as with privacy requests, but I think that sort of thing is pretty rare.

If your application has space concerns, you can modify this approach to be like a recycle bin where you delete records which are no longer valid and have been invalid for over a month (or whatever time frame is appropriate for your application). However, I think this is unnecessary in most cases except for blob/file storage.

> Then have multiple people inspect the logs.

I think that this is the most important part of any check. Your parent refers to checking the log five times, but, at least in my experience, I won't catch any more errors on the fifth time than the first—if I once saw what I expected rather than what was there, I'll keep doing so. Of course everyone has their blind spots, but, as in the famous Swiss-cheese approach, we just hope that they don't line up!

mv then rm is another idiom. So long as you have the space.

For database entries, flag for deletion, then delete.

In the files case, the move or rename also accomplishes the result of breaking any functionality which still relies on those file ... whilst you can still recover.

Way back in the day I was doing filesystem surgery on a Linux system, shuffling partitions around. I meant to issue the 'rf -rm .' in a specific directory, I happened to be in root.

However ...

- I'd booted a live-Linux version. (This was back when those still ran from floppy).

- I'd mounted all partitions other than the one I was performing surgery on '-ro' (read-only).

So what I bought was a reboot, and an opportunity to see what a Linux system with an active shell, but no executables, looks like.

Plan ahead. Make big changes in stages. Measure twice (or 3, or 10, or 20 times), cut once. Sit on your hands for a minute before running as root. Paste into an editor session (C-x C-e Readline command, as noted elsewhere in this thread).

Have backups.

You mean cp then rm?

And yes, copy, verify, delete. And make sure by the code structure that you either do the three on the same files, or their fail.

Also, do it slowly, with just a bit of data on each iteration. That will make the verification step more reliable.

Anyway, for a huge majority of cases, only having backups is enough already. Just make sure to test them.

I think mv then rm is probably meant as 'windows trash bin' style.
No, mv.

Example:

  cd datadir
  mkdir delete
  mv <list of files to be deleted> ./delete
  # test to see if anything looks broken.  
  # This might take a few seconds, or months, though it's usually reasonably brief.
  rm -rf ./delete
The reasons for mv:

- It's atomic (on a single filesystem). There's no risk of ending up with a partial operation or an incomplete operation.

- It doesn't copy the data, it renames the file. (mv and rename are largely synonyms.)

- There's no duplication of space usage. Where you're dealing with large files, this is helpful.

The process is similar to the staged deletion most desktop OS users are familiar with, of "drag to trash, then empty trash". Used in the manner I'm deploying it, it's a bit more like a staged warehouse purge or ordering a dumpster bin --- more structured / controlled staged deletion than a household or small office might use.

Another good approach is do deletions slowly. Put sleeps between each operation, and log everything. That way if you realize something is broken, you have a chance of catching it before it's too late.
It never hurts to ask for another set of eyes to review. At the least if something goes awry, the blame isn't solely on you.

  > ... Then have multiple people inspect the logs. Once ok'd, run again, with manual prompts after each log item asking to continue...
This sort-of reminds me of some "critical" work I had to do a couple of decades ago. I was in a shop that used this horrifically tedious tool for designing masks for special kinds of photonic devices-- basically it was tracing out optical waveguides that would be placed on a crystal that was processed much like a silicon IC.

The process was for TWO of us to sit in front of computer and review the curves in this crazy old EDA layout tool called "L-edit" before it got sent to have the actual masks made (which were very expensive). It took HOURS to check everything.

The first hour was tolerable but then boredom started to creep in and we got sloppy. The whole reason TWO people got tasked with this was because it was thought that we would keep each other focused-- 2 pairs of eyes are better than one, right?. Instead, it just underscored the tedium of it all. One day someone walked in and found us BOTH in DEEP SLEEP in front of the monitor. Having two people didn't decrease the waste caused by mistakes, it just bored the hell out of more people.

How many mistakes did you catch?
From his story I can tell he found one big mistake. The tedious work itself.
ONE real one and some occasional nitpicks to show that we were busy (after being caught asleep).

Was it worth it? No, I don't think so from an opportunity cost perspective-- even though we were the most junior folks there. A mind is a terrible thing to waste!

Yep, even writing a simple wildcard at command-line I will 'echo' before I 'rm'.
On computers I own, I always install "trash-cli" and i even created an alias for rm to trash. It's like rm, but it goes to the good old trash. It will not save your prod but it's pretty useful on your own computer at least.
This is how I do it in compiled code. In shell, I print the destructive command for dry runs - no conditions around whether to print or not, I go back to remove echo and printf to actually run the commands.
Rather than commenting it out, I suggest adding a --live-run flag to scripts and checking the output of --live-run=false (or omitted) before you run it "live."
But then you have double the chances of introducing a bug for the specific scenario we are talking about:

Before: there is chance there is a bug in my "delete" use case

Now: what we have before plus the change that there is a bug in my "--live-run" flag

You can make automated tests for your flag. You can’t make automated tests for your code comments.
Human-in-the-loop is so important concept in ops and yet everyone (that's including me) seems to learn it the hard way.

   SELECT COUNT(1) FROM table 
   -- UPDATE table SET col='val'
   WHERE 1=1

    BEGIN TRANSACTION 
    UPDATE table SET col='val' WHERE 1=1
    ROLLBACK
Definitely better, when you can afford the overhead!
Yes. Also, maybe not have a delete action in the middle of a script. It's usually better to build a list of items to be deleted. In that case, two lists: items to be deleted, items to be kept. Then compare the lists:

- make sure the sum of their lengths == number of total current items

- make sure items_to_be_kept.length != 0

- make sure no two items appear in both lists

- check some items chosen at random to see if they were sorted in the correct list

At this point the only possible mistake left is to confuse the lists and send the "to_be_kept" one to the delete script; a dry run of the delete list can be in order.

I've had good success with this approach, have two distinct scripts generate the two lists, then in addition to your items here also checking that every item appears in one of the lists.
What do you recommend, to not get intro trouble if there are spaces or newlines in the file names?
Don't use a shell script.
Do you mean, always pass the list directly to the next script via function calls, without writing it to an intermediate file / pipeline?
Yes, use the list argument to Python’s subprocess.run for example. It’s much easier to not mess up if your arguments don’t get parsed by a shell before getting passed.
I'm being flippant, because shell scripts are so inherently error prone they're to be avoided for critical stuff like this.

If you _absolutely_ must use a shell script:

0. Use shellcheck, which will warn you about many of the below issues: https://www.shellcheck.net/

1. understand how quoting and word splitting work: https://mywiki.wooledge.org/Quotes

2. if piping files to other programs, using `-print0` or equivalent (or even better, if using something like find, its built in execution options): https://mywiki.wooledge.org/UsingFind

3. Beware the pitfalls (especially something like parsing `ls`): https://mywiki.wooledge.org/BashPitfalls

(warning: the community around that wiki can be pretty toxic, just keep that in mind when foraying into it.)

Try not to delete stuff with Bash.

This is the most reliable way. Bash has a few niceties for error handling, but if you are using them, you would probably fare better in another language.

If you do insist on Bash, quote everything, and use the "${var}" syntax instead of "$var". Also, make sure you handle every single possible error.

`set -e` will abort on any error, anywhere in the pipeline. It’s a must for any critical script.
This. The original approach can fail horribly if there's a problem on the server when you run the script for real. Your code can be perfect but that's no guarantee the server will always return what it ought to.
This sounds like a "do nothing script."

https://news.ycombinator.com/item?id=29083367

It defaults to not doing anything so you can gradually and selectively have it do something.

Learned about when I posted my command line checklist tool on HN: https://github.com/givemefoxes/sneklist

(https://news.ycombinator.com/item?id=25811276)

You could use it to summon up a checklist of to-dos like "make sure the collection in the dictionary has the expected number of values" before a "do you want to proceed? Y/n"

Another technique that I've used with good success is to write a script that dumps out bash commands to delete files individually. I can visually inspect the file, analyze it with other tools, etc and then when I'm happy it's correct just "bash file_full_of_rms.sh" and be confident that it did the right thing.
That was our SOP for running DELETE SQL commands on production too, a script that generates a .sql that's run manually. It saved out asses a fair amount of times
Yeah, wish I'd learned that the easy way. Fresh into one of my first jobs I was working with a vendor's custom interface to merge/purge duplicate records. It didn't have a good method of record matching on inserts from the customer web interface so a large % of records had duplicates.

Anyway, I selected what I though was a "merge all duplicates" option without previewing results. What I had actually done was "merge all selected". So, the system proceeded to merge a very large % of the database... Into One. Single. Record.

Luckily the vendor kept very good backups, and so I kept my job. Because I also luckily had a very good boss and I had already demonstrated my value in other ways, he just asked me "Well, are you going to make that mistake again?". I wisely said no, and he just smiled and said "Then I think we're done here."

I have been particularly fortunate throughout my career to have very good managers. As much as managers get a lot of flack here on HN, done well they are empowering, not a hindrance, and I attribute a lot of success in my career to them.

> Yeah, wish I'd learned that the easy way.

I think that, if you've only learned something like that the easy way, then you haven't learned it yet. As long as everything's only ever gone right, it's easy to think, I'm in a rush this one time, and I've never really needed those safety procedures before, ….

At a previous job the DB admin mandated that everyone had to write queries that would create a temporary table containing a copy of all the rows that needed to be deleted. This data would be inspected to make sure that it was truly the correct data. Then the data would be deleted from the actual table by doing a delete that joined against the copied table. If for some reason it needed to be restored, the data could be restored from the copy.
This was taught to me in my first linux admin job.

I was running commands manually to interact with files and databases, but was quickly shown that even just writing all the commands out, one by one gives room personally review and get a peer review, and also helps with typos. I could ask a colleague "I'm about to run all these commands on the DB, do you see any problem with this?". It also reduces the blame if things go wrong if it managed to pass approval by two engineers.

While I'm thinking back, another little tip I was told was to always put a "#" in front of any command I paste into a terminal. This stops accidentally copying a carriage return and executing the command.

> This stops accidentally copying a carriage return and executing the command.

For a one-liner sure, but a multi line command can still be catastrophic.

Showing the contents of the clipboard in the terminal itself (eg via xclip) or opening an editor and saving the contents to a file are usually better approaches. The latter let’s you craft the entire command in the editor and then run it as a script.

From [0]:

[For Bash] Ctrl + x + Ctrl + e : launch editor defined by $EDITOR to input your command. Useful for multi-line commands.

I have tested this on windows with a MINGW64 bash, it works similarly to how `git commit` works; by creating a new temporary file and detecting* when you close the editor.

[0] https://github.com/onceupon/Bash-Oneliner

* Actually I have no idea how this works; does bash wait for the child process to stop? does it do some posix filesystem magic to detect when the file is "free"? I can't really see other ways

It does create and give a temporary file path to the editor, but then simply waits for the process to exit with a healthy status.

Once that happens, it reads from the temporary file that it created.

The 'enable-bracketed-paste' setting is an easier and more reliable way to deal with that: https://unix.stackexchange.com/a/600641/81005

It will prevent any number of newlines from running the commands if they're pasted instead of typed.

You can enable it either in .inputrc or .bashrc (with `bind 'set enable-bracketed-paste on'`)

At the point you're doing this, you should be using a proper programming language with better defined string handling semantics though. In every place it comes up you'll have access to Python and can call the unlink command directly and much more safely - plus a debugging environment which you can actually step through if you're unsure.
Eh, I think that misses the point a bit. Use whatever you want to generate the output, but make the intermediary structure trivial to inspect and execute. If you're actually taking the destructive actions within your complicated* logic then there's less room to stop, think, and test.

You could always generate an intermediary set, inspect/test/etc, and then apply it with Python. I've done that too, works just as well. The important thing is to separate the planning step from the apply step.

* where "complicated" means more complicated than, for ex, `rm some_path.txt` or `DELETE FROM table WHERE id = 123`.

Ah, I’m glad I’m not the only one who did this. It also means that you can fix things when they break halfway. Say you get an error when the script is processing entry 101 (perhaps it’s running files through ffmpeg). Just fix the error and delete the first 100 lines.
I tend to write one script that emits a list of files, and another that takes a list of files as arguments.

It's simple to manually test corner cases, and then when everything is smooth I can just

    script1 | xargs script2
It's also handy if the process gets interrupted in the middle, because running script1 again generates a shorter list the second time, without having to generate the file again.

When I'm trying to get script1 right I can pipe it to a file, and cat the file to work out what the next sed or awk script needs to be.

The only issue with that is if subsequent lines implicitly assume that earlier ones executed as expected, e.g. without error.

Over-simplified example:

1. Copy stuff from A to B

2. Delete stuff from A

(Obviously you wouldn't do it like that, but just for illustration purposes.) It's all fine, but (2) assumes that (1) succeeded. If it didn't, maybe no space left, maybe missing permissions on B, whatnot, then (2) should not be executed. In this simple example you could tie them with `&&` or so (or just use an atomic move), but let's say these are many many commands and things are more complex.

I just want to say as someone currently working on a script to delete approximately 3.2TB of a ~4TB production database, this subthread is pure gold.
>literally simple prints statements

Yes, that can be a simple but powerful live on screen log. I developed a library to use an API from a SaaS vendor, in much the same way as the author. It was my first such project & I learned the hard way (wasted time, luckily no data loss or corruption) that print() was an excellent way to keep tabs on progress. On more than one occasion it saved me when the results started scrolling by and I did an oh sh*t! as I rushed to kill the job.

The No. 2 philosophy!

Make sure you got everything out and off before you pull up your pants, or else you better be prepared to deal with all the shit that might follow!

It's amazing the number of times I look at some simple code and think "nah, this is so simple it doesn't need a test!", add tests anyway (because I know I should)... and immediately find the test fails because of an issue that would have been difficult to diagnose in production.

Automated tests are awesome :)

Dry run really is key here. Most automated tests wouldn't find this bug.
I'd make sure those include WARN or ERROR (I'd use logging to do that), that way you can grep for those. Spot checking might be difficult if the logs get long.
That is called experience.

Good decisions come from experience. Experience comes from making bad decisions.

Agreed, I've also been burned doing stupid things like this and always print out the commands and check them before actually doing the commit.

As they say, measure twice, cut once.

Don't feel bad, I think every professional in IT goes through something similar at one time or another.

To ensure that the files are actually are downloaded (step1), before deleting the original (step2). I would make make step1 an input to step2. That is step2 cannot work without step1. Something like:

    (step1) Download video from URL.  Include the Id in the filename.
    (step2) Grab the list of files that have been downloaded and parse to get the Id.  Using the Id, delete the original file.
This is why I like to always write any sort of user-script batch-job tools (backfills, purges, scrapers) with a "porcelain and plumbing" approach: The first step generates a fully declarative manifest of files/uris/commands (usually just json) and the second step actually executes them. I've used a --dry-run flag to just output the manifest, but I just read some folks use a --live-run flag to enable, with dry-run being the default, and I like that much better so I'll be using that going forward.

This pattern has the added benefit that it makes it really easy to write unit tests, which is something often sorely lacking in these sorts of batch scripts. It also makes full automation down the line a breeze, since you have nice shearing layers between your components.

http://www.laputan.org/mud/mud.html#ShearingLayers

I tend towards a --dry-run flag for creative actions and --confirm for destructive actions. Probably sightly annoying that the commands end up seemingly different, but it sure beats accidentally nuking something important.
Condensed to aphorism form:

    Decide, then act.  
There's a whole menagerie of failure modes that come from trying to make decisions and actions at the same time. This is but one of them.

Another of my favorites is egregious use of caching, because traversing a DAG can result in the same decision being made four or five times, and the 'obvious' solution is to just add caches and/or promises to fix the problem.

As near as I can tell, this dates back to a time when accumulating two copies of data into memory was considered a faux pas, and so we try to stream the data and work with it at the same time. We don't live there anymore, and because we don't live there anymore we are expected to handle bigger problems, like DAGs instead of lists or trees. These incremental solutions only work with streams and sometimes trees. They don't work with graphs.

Critically, if the reason you're creating duplicate work is because you're subconsciously trying to conserve memory by acting while traversing, then adding caches completely sabotages that goal (and a number of others). If you build the plan first, then executing it is effectively dynamic programming. Or as you've pointed out, you can just not execute it at all.

Plus the testing burden is so drastically reduced that I get super-frustrated having to have this conversation with people over and over again.

Indeed. I would say that framework or even language-level support for putting things in "dry-run" mode is something sorely missed from many modern frameworks and languages, that old C libraries used to do.
Beside doing this, I like to first just move files to another dir (keeping the relative path) instead of deleting them. It's basically like a DIY recycle bin.

If both paths are on the same disk moving files is a fast operation - and if you discover a screw up, you can easily undo it. On the other hand if everything still looks fine after a few days, you just `rm -rf` that folder and purge the files.

A few assertions would have also stopped this.

    During buildup of the our_id list: assert (vimeoId not in our_ids). 
    After creating the list:  assert len(set(our_ids)) > 10000 and assert len(set(our_ids)) == len(our_ids)
    Before each final deletion: assert id not in hardcoded_list_of_golden_samples. 
    Depending on the speed required you could hit the api again here as an extra check. 
But as always everything is obvious in hindsight. Even with the checks above, Plan+Apply is the safest approach.
in my opinion any process that isn't preceded by another identical and automated process that varies only by the data involved is very risky to do in production. your management hopefully had a big reality check? or not because of backups?
ZFS -> Snapshot....always!! Before touching writable-data (my personal mantra) ;)
I love ZFS too but that's not really relevant to this discussion because the deleted items were on a video hosting platform and the company did already have local copies.
Yes and? Make a snapshot on live. Again, never touch data before snapshot.
This reminds of some IRC threads. You post a question and someone's answer assumes you are going to rip out and replace your existing prod setup just so you can use their pet tool.
Pff, there are thousands of system's and filesystems that are capable to make snapshots, even a shadow disk from VM370 (1982) could be seen as one.
At risk of sounding snarky, you do understand how video hosting platforms work? Customers, even enterprise ones, don’t have shell access let alone control over what file system is used.

There are a hundred ways this problem could have been prevented but ZFS isn’t one of them.

>you do understand how video hosting platforms work?

No, no i don't.

So, “i am under NDA” but I reveal my client’s name and a lot of sensitive details about what we are doing. LOL.
Where do you see the clients name? I only see Vimeo being mentioned.
Got it. To be honest I'd be hesitant to publish a blog post like that with your name + current company name attached to it.

It's a bit different to share a fun story a few years later about that time you almost wiped production.

Well at least deleting the secret is a step back toward the NDA he left behind.
It still breaks the NDA:

* Firstly, you don't have to name the company to break the NDA anyway (you are still disclosing information you aren't supposed to disclose regardless of if it can be linked back to the company).

* Secondly, the client is still named on the front page of the website.

* Thirdly, OP posted this with his real name that trivially links back to the dev shop he is working for. The site also has his CV which lists the client again, with a description of the project to link it to the post.

* Finally, The client can trivially be identified by googling the description in the second paragraph (i.e. just search the named countries in operation plus the word Gym).

Not all NDAs have the same terms. I could write up and serve an NDA right now that still counts as an NDA yet permits everything in your list.
All contracts vary in terms, but I've never seen an NDA that says "you can talk about the content under NDA as long as you don't mention the businesses name, and just identify who they are in a roundabout way instead".

"Well i'm under an NDA, so I can tell you all the specifics of the project, but I can't tell you the companies name. I can say they own the largest search engine though, and have a market cap of 1.5 trillion, and rhyme with "Roogle", but I really can't say who they are. Anyway, here is some code I wrote for them and a description of how we nearly ruined their project along with me calling them incompetent..."

(comment deleted)
Just a note: being able to click yourself a server at Google, AWS etc. Might be cheap enough even paying for 15tb of traffic.
The more I read about vimeo the more I wonder what's up with these guys.

Only recently they made some god aweful policy changes for content creators(1), but it looks like they treat their enterprise customers just the same.

Surely, there must be better alternatives for hosting videos than being at the mercy of a company who couldn't care less about big paying customers.

(1) https://www.theverge.com/2022/3/18/22985820/vimeo-bandwidth-...

mux.com seems like a great alternative and is super developer focused.
Under NDA but I'll give rough details of what's occurring while also naming my client and disparaging them to the public.

Well that's a brave move...

They said they are a junior developer with not much experience. I'm afraid they may not know what is and isn't covered under NDA.
My tip would be: read what you sign.
Just to clarify, my company is under an NDA and not personally me. It also encompasses only the actual project details so a post like this is legally compliant. (Not a lawyer, might be wrong)
So you're not under an NDA as you wrote.

I don't know your position but I would assume a NDA is part of your freelancer or employee contract.

OP might at least want to consult with a contract lawyer in Italy to make sure.
In every contract I've ever signed, part of the NDA clause with my employer is that I'm also bound by NDA's my employer is bound by, so if the employer signs an NDA with a customer, I would also be bound by that. It might be worth checking your contract, otherwise having a company sign an NDA doesn't hold much weight if their staff are free to go around sharing the information themselves.
You likely have a confidentiality clause in your contract.

If your company is under an NDA, your company will have an obligation to ensure that you also do not disclose information.

Companies are mostly just collections of people, and an NDA is mostly meant to stop people working on the project from talking about the project.

(comment deleted)
Controversial opinion: And this is why block syntax by white space is not for production.
This is hardly a whitespace issue
Ah, yes, I just noticed the difference in indentation. In actuality, the error about the mental model of variable states.
"What does this teach us? Well, it teaches me to do more diverse tests when doing destructive operations. It also should probably teach something to Vimeo and to my contractor but I doubt it will (and yes, the upload for some reason is still manual to this day. Go figure!)"

So you wrote bad code, didn't test it properly, ran it on production on the Friday before a release and are blaming Vimeo and [name redacted]?

And your resolution was yet another cobbled together script that you probably didn't test?

This isn't a great article to have attached your name to

Not to mention that he _deleted_, but not _lost_ videos. Nothing to see here.
I'd hire this guy if only being for this frank about his mistake. He owned it and that is what I would look for.

After deletion, what should he have done? Postpone the go-live? That's often not a a cost-effective option. As for a risk-analysis the worst what could happen was deletion of the remaining videos. I don't think that that makes big difference in this situation. And to do the right thing, you have to have the infrastructure in place, if you are in a hurry. I doubt that's the case for a 10 heads shop.

Agree 100%. Acknowledged mistake, moved forward to find a solution. Reflected on lessons learned. Shared valuable lesson.

To me this indicates intelligence, competence, integrity, grit and generosity. TechnicL proficiency is much easier to come by than integrity, grit and generosity. I would trust the author to deliver on commitments.

Owning the mistake would be fine if he did that - he did'nt. He blamed the company he was contracting for. That's a big no from me
I'm sorry if it came off like that. The mistake in this case was completely mine (bad code and bad testing). The detour on the other two companies was mostly because this way of deleting/recovering stuff should've probably been avoided in the first place, other than that I'm absolutely not blaming anyone else!
Don't worry about all that - there isn't a developer worth their salt that hasn't made a mistake. But I'd consider having this blog post and HN post retracted purely for future internet checks. It isn't a reflection on you, and your honesty is fantastic. But there is a lot to be said about using a pseudonym when it comes this close to your employers
(comment deleted)
I'd probably make your github profile private for a while as well. Or at least removing your real name from it.
It's as if we read different articles. He literally writes that he made "A series of mistakes that could've probably been easily prevented."
Agreed. But I’d also fire him from this job.
Doesn't make sense. Their employer literally paid them to learn from their mistake.

Now, you think they should be fired? So that another employer rips the benefits of that learning experience.

For having got into a sticky situation and out of it?
(comment deleted)
"Recently, I was asked if I was going to fire an employee who made a mistake that cost the company $600,000. No, I replied, I just spent $600,000 training him. Why would I want somebody to hire his experience?"

-- Thomas J. Watson

Aye, this is how you learn and make sure it doesn't happen again.

I did a similar thing ~20 years ago when I first started my career, accidentally deleting a production database because I thought I was working on the test database.

I owned it, learned lessons from it, and it's never happened again.

Vimeo completed a major migration of videos between accounts with no confirmation or communication before commiting it, then refused to reverse the change. Hardly the best service.

The article hardly comes across as 'blaming' them for the core issue but they were definitely not helpful.

Will every developer who has never checked in bad code on Friday, or accidentally deleted the wrong data, please raise their hand?

‘Judgment comes from experience, and experience comes from poor judgment.’

:-)

Better to do it before the release then afterwards. I'm assuming this way nobody noticed the issue.

Also, would you rather everyone only ever posted about all the times they were successful?

Oof, we wouldn't work well together. Very rarely is someone good enough to be this obnoxious.
I very much doubt you would ever work with or for me.
> This isn't a great article to have attached your name to

A million times better than your comment.

All I did was give advice. If you don't like it it's fine.
Earlier in the article, the author does call out that it's bad code, so he's not entirely blaming these companies. Anyway: You should not be afraid of thinking about what each party could have done better. Not just yourself, but other people too. When I look back on times where I only blamed myself for prod issues, it was less of a learning experience, and more focused on beating myself up for no good reason. That approach shows that I'm afraid of the consequences, and it's an effective way to feel isolated from the team instead of improving.
(comment deleted)
(Since the OP redacted the company name from the post, I've done the same in your comment here. I hope that's ok.)

(We do this sort of thing to protect users, usually as the result of an emailed request, and you can tell when we've done it because of the word 'redacted' in square brackets.)

Code without constant logging of “utc [who] does what exactly” is a no-go for me for a long time. Also, if you have to be destructive, replace the <rm/sell/halt> with log() for at least one time (aka --verbose --dry-run) and check your expectations. One-shot scripts like this are screaming disaster.

(The problematic line lacks the closing ", probably a typo? I though it closed in an unexpected location)

You can automate using puppeteer or selenium
The author used Playwright in the end to automate uploads. Using e2e tools for automating tasks is clever, I'm not sure I would've thought of it.
It's clever, but also brittle. And might have disastrous error conditions (like hitting "Delete" instead of "Continue" if the wrong UI part has focus).
I accidentally deleted a printer from the printserver by using a python script. The docs weren't exactly clear, so i thought it would only remove the local printer connection. After reading this post i feel better now. My fuckup wasn't that bad in comparison. :)
Honestly, this is positively representative of any junior developer with comparable experience. Depending on their background and how much production work they had, there's an overwhelming sense of eagerness and enthusiasm. Quick to script and perhaps a bit too quick to execute.

A friendly team will harness that enthusiasm and tame the quickness / encourage respect for production. We all made a massive doo doo and its how you proceed that'll define your career.

This is one of those times that even if you don’t use a fully functional language, trying to make as much of your program logic pure functions would be helpful.

It also makes it more testable. Instead of putting the delete call right in the loop, split it into four functions.

    function getAllVimeoVideos()

    function getAllDbVideos()

    function getVideosToDelete(vimeo_videos, db_videos)

    function deleteVideos(videos_to_delete)

Your core logic lives in getVideosToDelete which is simply a set difference.

Given that there are only a few hundred videos, it is easy to run the getter functions above and quickly verify they are returning what you expect.

This was going to be my exact recommendation. By “separating the concerns”, you make it easier on my pretty much every dimension: testing in unit tests, doing a dry run in production, ability to read the code (you and code reviews), and in some cases your code will be written in a more functional way reducing variable scoping issues.
Yes that's fun. a

    List<Foo> getFoosToUpdate(List<Foo> foos, List<Bar> bars) 
function is the first time I thought about time complexity in my job.

Say Foo and Bar have fields in common, such that you can say a Foo object "equals" or "matches to" a Bar object, like if they have name and dateOfBirth fields or something else that are the same (nothing like a common ID between the two). Now say there are some other fields too, like amountSpentThisYearOnDogFood that you know is always accurate for Bars, but might be out of date for Foos. How do you get the list of all the Foos to update?

Initially I did the nested for loop solution that's like

   List<Foo> getFoosToUpdate(List<Foo> foos, List<Bar> bars)
   {
    List<Foo> returnList = new List<Foo>();
    foreach (var foo in foos)
    {
     foreach (var bar in bars)
     {
      // check if "equal" or "matching" based on some criteria
      // if equal, update foo dog food expenditure with bar dog food expenditure, add to returnList, and break
     }
    }
    return returnList;
   }
but that's O(n^2) right.

The solution with a Dictionary is obviously better. All you need to ensure is that you have a method for both the Foo and Bar classes that will produce the equivalent hash for both, if they would be considered equal or matching by whatever criteria you are using.

So you could have something like

    int GetHashOfFoo(Foo foo)
    {
     string firstName = foo.FirstName;
     string lastName = foo.LastName;
     DateTime dob = foo.Dob;

     return (firstName, lastName, dob).GetHashCode(); // convenient c# method
    }

    int GetHashOfBar(Bar bar)
    {
     string firstName = bar.FirstName;
     string lastName = bar.LastName;
     DateTime dob = bar.Dob;

     return (firstName, lastName, dob).GetHashCode();
    }
These two functions will return the same value if those fields are the same. So then you can do something like

   List<Foo> getFoosToUpdate(List<Foo> foos, List<Bar> bars)
   {
    List<Foo> returnList = new List<Foo>();
    Dictionary<int, Bar> barsByHash = new Dictionary<int, Bar>(bars.Count);

    foreach (var bar in bars)
    {
     int barHash = GetHashOfBar(bar);
     barsByHash[barHash] = bar;
    }

    foreach (var foo in foos)
    {
     int fooHash = GetHashOfFoo(foo);
     if (barsByHash.ContainsKey(fooHash) 
     {
      returnList.Add(foo.CopyWith(dogFoodExpenditure: barsByHash[fooHash].DogFoodExpenditure))
     }
    }
    
    return returnList;
   }
Which is faster cause you only have to go through the bars list once.

I actually messed up something like OP with this, but with doing undesired additions instead of undesired deletions.

You can think of it as having two endpoints, both expecting a .csv with rows being the things you were updating/changing/deleting.

The problem was, there was a column to indicate (with a character) whether the row was for an edit, or addition, or deletion, but this was only with one of these endpoints. For the other, there was only addition functionality, but I thought changes and deletions were also options for the other kind of .csv due to some unwise assumptions on my part (thinking that the other .csv would have the same options as the other). That's how we accidentally put in over 100 additions that should have been changes that had to be manually deleted. Luckily I had a list of all the mistaken additions.

I like these stories. I think they resonate well for 'the rest of us'. I've made plenty of mistakes like this - you learn and grow, right?

One of the best things about HN is that so many incredible, talented people post. It's incredibly inspiring to raise your own game, to see what the best are doing. But sometimes it's equally important to realise we all fuck up, and for every unicorn dev there's another thousand of us grinding away.

OP - well done for sorting the problem and telling us all about it!

Experience is directly proportional to the amount of equipment ruined or data lost.

Even though you were fortunate not to lose any data, you gained a lot of experience!

Shouldn't that be `page={page}` rather than `page{page}`? Or better yet, use the requests `params` argument.
It should really be something like: "a flaw in our system allowed me to delete 7am TB of videos". Not entirely your fault.
System and/or development processes
It's like the first time you run

  rm -rf /path/to/delete/ * 
And realize it is taking too long...
Can you explain? I feel like it removes / but not sure why.
The error is the space before the asterisk. The original intention was to delete the contents of the folder /path/to/delete/. Instead, the asterisk enumerates files in the current directory and they get deleted
Besides recursively deleting /path/to/delete/ the command also deletes all (non hidden) content of the current directory (note the * at the end of the line). I assume the correct command would be /path/to/delete/*.
It removes everything in the current directory

   rm -rf /path/to/delete/ *
Note the space between the last / and the *

This will recursively remove the directory /path/to/delete and remove every file/directory that matches * in the current directory where 'rm' is being run.

When what was most likely meant was:

   rm -rf /path/to/delete/*
Note the lack of a space between the last / and . This will remove all files that match that reside in the /path/to/delete/ directory.
Related: is there any HTTP API model that supports transactions with commit and rollback? Also isolation levels? Usually one wants to set_stock(get_stock() + 10) but there may be competing from various clients between both calls, resulting in races. Usual web APIs seem vulnerable to this.
Wouldn't the model be to expose an increment_stock(10) type HTTP endpoint instead, and the backend can ensure it's atomic?
For many years I have had a private blog. I like to write but realised 99% of us are not interesting to read. This is a young guy processing his thoughts. Not "teaching" the rest of us as he frames it. This should have stayed in-house and personal. The company can then decide which clients, authorities to contact if necessary. There is a book in all of us as they say. For most of us it should stay there.