<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmytro Polhorodnyk</title>
    <description>The latest articles on DEV Community by Dmytro Polhorodnyk (@stubrofx).</description>
    <link>https://dev.to/stubrofx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4124635%2F637a66af-8297-499a-9d9e-ebedd0a3cef2.png</url>
      <title>DEV Community: Dmytro Polhorodnyk</title>
      <link>https://dev.to/stubrofx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stubrofx"/>
    <language>en</language>
    <item>
      <title>What "Fully Automated" Actually Costs</title>
      <dc:creator>Dmytro Polhorodnyk</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:02:02 +0000</pubDate>
      <link>https://dev.to/stubrofx/what-fully-automated-actually-costs-23a0</link>
      <guid>https://dev.to/stubrofx/what-fully-automated-actually-costs-23a0</guid>
      <description>&lt;p&gt;I run an automated system on a remote machine with nobody watching it. When I&lt;br&gt;
started, I described it to people as passive.&lt;/p&gt;

&lt;p&gt;Over two days it needed five separate interventions. None of them were exotic, and&lt;br&gt;
that's the point of this post. The gap between "automated" and "passive" is made of&lt;br&gt;
very ordinary things.&lt;/p&gt;

&lt;p&gt;Here's what actually broke, and the patterns I use now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five failures, named
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Two copies of the same process running side by side for ten hours.&lt;/strong&gt; Both were&lt;br&gt;
writing. Neither complained. The watchdog that was supposed to prevent exactly this&lt;br&gt;
compared strings after formatting instead of before, so the duplicate didn't look&lt;br&gt;
like a duplicate to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lost login that nobody noticed.&lt;/strong&gt; The service kept running, kept polling, kept&lt;br&gt;
reporting healthy. It just wasn't authenticated any more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A component quietly detached from its host.&lt;/strong&gt; Still installed, still configured,&lt;br&gt;
no longer attached to the thing it was supposed to be attached to. Nothing logged it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dead event that never fired.&lt;/strong&gt; A file was written for a consumer that had&lt;br&gt;
stopped reading it months earlier. Correct, current, and read by nobody.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A config that silently reverted to defaults after a restart.&lt;/strong&gt; The system came&lt;br&gt;
back up with different behaviour than it had before, and the only sign was that the&lt;br&gt;
numbers looked slightly different.&lt;/p&gt;

&lt;p&gt;I'd expected the dramatic failures: crashes, disk full, network down. &lt;strong&gt;All five&lt;br&gt;
were quiet ones.&lt;/strong&gt; Not one produced an error message a human would see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: restart is a state change, not a no-op
&lt;/h2&gt;

&lt;p&gt;Two of those five happened only because something restarted.&lt;/p&gt;

&lt;p&gt;We tend to think of a restart as returning to a known good state. In practice it's a&lt;br&gt;
transition with its own failure modes. Parameters reset to defaults. Identifiers&lt;br&gt;
regenerate. Connections re-establish in a different order than they did the first&lt;br&gt;
time.&lt;/p&gt;

&lt;p&gt;I hit a third version of this elsewhere: a startup sequence read a value from a&lt;br&gt;
remote service &lt;strong&gt;before the connection to that service was authorized&lt;/strong&gt;. The read&lt;br&gt;
returned a clean, plausible zero. The system then used that zero as a measurement and&lt;br&gt;
configured itself three hours away from correct.&lt;/p&gt;

&lt;p&gt;The journal shows it by the second: component loaded at :29, value read at :36,&lt;br&gt;
connection authorized at :39. The measurement happened three seconds before the thing&lt;br&gt;
it measured existed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A measurement taken before its input exists isn't a measurement.&lt;/strong&gt; It's a default&lt;br&gt;
value wearing a measurement's clothes, and if your priority rules trust measurements&lt;br&gt;
over configuration, it will win every time.&lt;/p&gt;

&lt;p&gt;Anything that must survive a restart has to be designed to survive it, and anything&lt;br&gt;
read at startup needs to answer "is the thing I'm reading from actually available yet".&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: scheduled jobs fail in complete silence
&lt;/h2&gt;

&lt;p&gt;I moved a directory. The wrapper script coped fine, because it resolved its own&lt;br&gt;
location at runtime. The registered scheduled tasks did not: they held absolute&lt;br&gt;
paths recorded at registration time.&lt;/p&gt;

&lt;p&gt;They failed. Nothing on the machine said so.&lt;/p&gt;

&lt;p&gt;This is worth sitting with, because it's the quietest failure mode a system has.&lt;br&gt;
A crashed process leaves a trace. A failing request returns a status. &lt;strong&gt;A scheduled&lt;br&gt;
job that stops running produces nothing at all&lt;/strong&gt; — and "nothing" is exactly what a&lt;br&gt;
working system that had nothing to do also produces.&lt;/p&gt;

&lt;p&gt;What I do now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every scheduled job writes a heartbeat on &lt;strong&gt;every&lt;/strong&gt; run, including runs where it
decided there was nothing to do&lt;/li&gt;
&lt;li&gt;absence of heartbeat for longer than the interval is itself an alert&lt;/li&gt;
&lt;li&gt;anything path-dependent resolves its own location rather than trusting a value
stored elsewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general rule: &lt;strong&gt;if the only evidence of health is the absence of errors, you&lt;br&gt;
have no evidence of health.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: an error is not "nothing new"
&lt;/h2&gt;

&lt;p&gt;My message loop checked whether a response contained items. If it didn't, the loop&lt;br&gt;
went back to sleep.&lt;/p&gt;

&lt;p&gt;Token revoked returns an error, not items. A second instance stealing the connection&lt;br&gt;
returns an error. Rate limiting returns an error. All three looked identical to&lt;br&gt;
"nothing new happened", and the process stayed alive, the watchdog stayed green, and&lt;br&gt;
the system was completely deaf.&lt;/p&gt;

&lt;p&gt;Now: status gets checked before contents, an unexpected status is loud, and no&lt;br&gt;
network call goes out without a client-side timeout. "Waiting forever" is another way&lt;br&gt;
to look alive while doing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: confirm delivery before you mark it done
&lt;/h2&gt;

&lt;p&gt;My notification pipeline deleted each event from disk before confirming it had been&lt;br&gt;
sent. Send fails, event is already gone. No retry, no record, nothing to recover.&lt;/p&gt;

&lt;p&gt;I'd built at-most-once delivery by accident, in the one place where at-least-once was&lt;br&gt;
the entire point. A duplicate notification is annoying. A missing one is invisible,&lt;br&gt;
and invisible is worse.&lt;/p&gt;

&lt;p&gt;The order is the whole fix: confirm, then mark done. Never the reverse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: updates on a machine you can't babysit
&lt;/h2&gt;

&lt;p&gt;This is the part that took longest to get right, so I'll describe the shape I landed on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pull, not push.&lt;/strong&gt; The machine checks a manifest on a schedule and decides for&lt;br&gt;
itself. Nothing is pushed at it from outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify before applying.&lt;/strong&gt; The manifest carries a hash of every artifact. Hash&lt;br&gt;
mismatch means refuse and alert, not "try anyway".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refuse during work.&lt;/strong&gt; If the system is mid-operation, the update waits for the next&lt;br&gt;
window. An update that interrupts work is worse than an update that's a day late.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back up, swap, then self-verify.&lt;/strong&gt; After applying, the system proves it's running&lt;br&gt;
the new version by reading it back from the running process, not from the file on&lt;br&gt;
disk. If verification fails, it rolls back automatically and alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A canary ring of one.&lt;/strong&gt; New versions go to a single machine first, and that machine&lt;br&gt;
is always mine. Whoever writes the update should be the first person it breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automation moves work, it doesn't remove it.&lt;/strong&gt; The work changes shape: instead of&lt;br&gt;
doing the thing, you maintain the thing that does the thing, and you build the&lt;br&gt;
instruments that tell you whether it's still doing it.&lt;/p&gt;

&lt;p&gt;That second part is most of the cost, and it's the part nobody estimates.&lt;/p&gt;

&lt;p&gt;The question I ask before automating anything now isn't "can this run without me". It's&lt;br&gt;
&lt;strong&gt;"which failure am I prepared to discover late"&lt;/strong&gt; — because that's the one I'll get.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>reliability</category>
      <category>programming</category>
      <category>automation</category>
    </item>
    <item>
      <title>Plausible Is What Gets Through</title>
      <dc:creator>Dmytro Polhorodnyk</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:49:16 +0000</pubDate>
      <link>https://dev.to/stubrofx/plausible-is-what-gets-through-5330</link>
      <guid>https://dev.to/stubrofx/plausible-is-what-gets-through-5330</guid>
      <description>&lt;p&gt;I ran the same logic against two market data vendors for the same period. Twelve&lt;br&gt;
percent of the resulting signals disagreed.&lt;/p&gt;

&lt;p&gt;Neither feed was broken. Neither validator complained. Every value was a number, in&lt;br&gt;
range, correctly typed, and wrong.&lt;/p&gt;

&lt;p&gt;That gap taught me the thing I now check for first: &lt;strong&gt;validators check shape, and&lt;br&gt;
shape is not meaning.&lt;/strong&gt; "Is this a number" and "is this the number" are different&lt;br&gt;
questions, and almost every validation library I've used only answers the first.&lt;/p&gt;

&lt;p&gt;Here are four cases from my own system, all of which passed every check I had.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Two sources, same question, different answers
&lt;/h2&gt;

&lt;p&gt;I only ran that comparison because I was switching providers. I expected them to&lt;br&gt;
agree to within a rounding error, and I was doing it as a formality.&lt;/p&gt;

&lt;p&gt;Twelve percent of signals differed.&lt;/p&gt;

&lt;p&gt;The cause wasn't corruption. Each vendor timestamped and rounded slightly&lt;br&gt;
differently, and my logic sat on top of those boundaries. Small differences at the&lt;br&gt;
edges, compounded through a pipeline, produced materially different outputs.&lt;/p&gt;

&lt;p&gt;Nothing in the data looked wrong. You could stare at either feed all day and see&lt;br&gt;
nothing to report. The disagreement was only visible when I &lt;strong&gt;diffed the outputs&lt;/strong&gt;&lt;br&gt;
rather than inspecting the inputs.&lt;/p&gt;

&lt;p&gt;That's now a rule: when you swap a data source, don't validate the new feed. Run&lt;br&gt;
both through your full pipeline and compare what comes out the other end. The&lt;br&gt;
inputs will always look fine. They're supposed to.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The thousands separator that became a decimal point
&lt;/h2&gt;

&lt;p&gt;The string &lt;code&gt;2,650&lt;/code&gt; parsed as &lt;code&gt;2.65&lt;/code&gt; in my system, and the result stayed a perfectly&lt;br&gt;
valid number.&lt;/p&gt;

&lt;p&gt;Different sources write thousands differently. Mine assumed one convention. On a&lt;br&gt;
source that used the other, prices quietly shrank by three orders of magnitude and&lt;br&gt;
stayed numeric, stayed positive, stayed parseable.&lt;/p&gt;

&lt;p&gt;Every downstream check passed, because every downstream check asked the same&lt;br&gt;
question: is this a number? It was. Beautifully.&lt;/p&gt;

&lt;p&gt;This is the shape of the problem in one line. A type check cannot tell you that&lt;br&gt;
2.65 should have been 2650, because 2.65 is a completely reasonable price for&lt;br&gt;
something.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Position in a string is not meaning
&lt;/h2&gt;

&lt;p&gt;A number in my logs read &lt;code&gt;414&lt;/code&gt; when the real value was &lt;code&gt;4414&lt;/code&gt;. The parser had eaten&lt;br&gt;
a digit, and nobody noticed for weeks.&lt;/p&gt;

&lt;p&gt;The input format numbered its items: &lt;code&gt;TP 1: 4414&lt;/code&gt;. My pattern matched the index and&lt;br&gt;
the value together. On a source that omitted the index, the first digit of the&lt;br&gt;
value became the index.&lt;/p&gt;

&lt;p&gt;4414 became 414. Which is plausible. It's in range for other instruments, it&lt;br&gt;
doesn't look like garbage, and no sanity check I had would flag it.&lt;/p&gt;

&lt;p&gt;That's the thing about silent corruption: &lt;strong&gt;it never produces obviously broken&lt;br&gt;
values.&lt;/strong&gt; It produces slightly wrong ones that survive every check you thought to&lt;br&gt;
write, because you wrote those checks imagining garbage, not plausibility.&lt;/p&gt;

&lt;p&gt;The fix was to stop treating position in a string as meaning, and to parse the label&lt;br&gt;
explicitly rather than relying on where things sat.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. If your clock starts when you look, you will never measure a delay
&lt;/h2&gt;

&lt;p&gt;My freshness check measured age from the moment my code noticed a message, not from&lt;br&gt;
when the message was sent.&lt;/p&gt;

&lt;p&gt;Which means after any outage, the backlog was fresh by construction. The service&lt;br&gt;
comes back up, reads a queue of messages hours old, stamps every one with "now", and&lt;br&gt;
hands me yesterday's decisions with today's timestamp.&lt;/p&gt;

&lt;p&gt;I found it when a stale instruction showed up with live action buttons attached.&lt;/p&gt;

&lt;p&gt;The general form of this bug is worth stating plainly: &lt;strong&gt;any measurement taken from&lt;br&gt;
your own observation time will always look current.&lt;/strong&gt; Everything is fresh, including&lt;br&gt;
the things that aren't. Age has to come from the source, or it isn't age.&lt;/p&gt;

&lt;h2&gt;
  
  
  The related case that isn't about data at all
&lt;/h2&gt;

&lt;p&gt;While auditing this, I found the same class of error in a completely different place.&lt;/p&gt;

&lt;p&gt;My own analysis tool marked 87 ideas as "refuted" when nobody had actually judged&lt;br&gt;
them. I'd built a two-pass system: one pass generates findings, a second sends&lt;br&gt;
skeptics to challenge each. The script counted a finding as refuted when it had zero&lt;br&gt;
supporting votes.&lt;/p&gt;

&lt;p&gt;The verifiers never ran. They crashed on startup and returned nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero votes and zero verifiers look identical if you only count votes.&lt;/strong&gt; So 87 ideas&lt;br&gt;
went into the graveyard with a confident label, and the report printed a clean number&lt;br&gt;
with no warning anywhere.&lt;/p&gt;

&lt;p&gt;Absence of evidence rendered silently as evidence of absence, inside a tool I built&lt;br&gt;
specifically to be skeptical. I now keep "not checked" as a third bucket, visible in&lt;br&gt;
the output, separate from "checked and rejected".&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually do now
&lt;/h2&gt;

&lt;p&gt;Four habits, all cheap:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diff outputs, not inputs.&lt;/strong&gt; When you change a source, a library, or a version, run&lt;br&gt;
the full pipeline both ways and compare the end result. Input validation will pass&lt;br&gt;
on both sides. That's what it's for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assert ranges that mean something, not just types.&lt;/strong&gt; Not "is this a number" but "is&lt;br&gt;
this number within the band this field can physically occupy". Most silent&lt;br&gt;
corruption lands outside a tight band and inside a loose one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parse labels, not positions.&lt;/strong&gt; Anything that relies on where a value sits in a&lt;br&gt;
string will eventually meet a source that formats it differently, and it will fail&lt;br&gt;
quietly rather than loudly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep "unknown" as a separate state.&lt;/strong&gt; Not zero, not false, not missing. If your&lt;br&gt;
code can't distinguish "checked and found nothing" from "never checked", it will&lt;br&gt;
report the second as the first at the worst possible moment.&lt;/p&gt;

&lt;p&gt;None of this is complicated. It's just that the bugs worth fearing don't announce&lt;br&gt;
themselves. They arrive looking exactly like the values you expected, which is&lt;br&gt;
precisely why they get through.&lt;/p&gt;

</description>
      <category>data</category>
      <category>programming</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Test That Could Not Fail</title>
      <dc:creator>Dmytro Polhorodnyk</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:45:39 +0000</pubDate>
      <link>https://dev.to/stubrofx/the-test-that-could-not-fail-46a3</link>
      <guid>https://dev.to/stubrofx/the-test-that-could-not-fail-46a3</guid>
      <description>&lt;p&gt;I shipped a safety feature that passed every test I had and could never have fired&lt;br&gt;
in production. Not "rarely fired". Could not, structurally, under any market&lt;br&gt;
condition, ever.&lt;/p&gt;

&lt;p&gt;It took me two days to find out, and the only reason I found out at all is that I&lt;br&gt;
stopped trusting green checkmarks.&lt;/p&gt;

&lt;p&gt;Here's what happened, and the four habits I picked up from it. I build an automated&lt;br&gt;
trading system, so my bugs settle in money rather than in tickets. That tends to&lt;br&gt;
sharpen the lessons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature
&lt;/h2&gt;

&lt;p&gt;The system takes one position per day. I wanted a daily loss limit: if the account&lt;br&gt;
drops by more than a set percentage today, stop trading until tomorrow.&lt;/p&gt;

&lt;p&gt;Simple enough. Parse the threshold, compare it against the current drawdown, block&lt;br&gt;
the entry if we're past it, log an alert.&lt;/p&gt;

&lt;p&gt;I wrote it. Compilation clean. The test suite went green. The regression harness&lt;br&gt;
confirmed the results matched the previous version byte for byte. I shipped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where nothing happened
&lt;/h2&gt;

&lt;p&gt;Two days later I ran what I call a dose test. The idea is simple: set the threshold&lt;br&gt;
so absurdly tight that the feature &lt;strong&gt;must&lt;/strong&gt; fire. If it doesn't, something's wrong&lt;br&gt;
with the feature, not with the market.&lt;/p&gt;

&lt;p&gt;I set the daily loss limit to a value the account crosses within minutes of opening.&lt;/p&gt;

&lt;p&gt;The results came back byte for byte identical to the run without the feature.&lt;/p&gt;

&lt;p&gt;Not "mostly similar". Identical. The feature had done nothing at all, and it had&lt;br&gt;
been doing nothing in production for two days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why every test was green
&lt;/h2&gt;

&lt;p&gt;The check sat in the right place logically and in the wrong place temporally.&lt;/p&gt;

&lt;p&gt;It ran before the entry block: "are we past the daily loss? then don't enter."&lt;br&gt;
That reads correctly. But the system takes one position per day. By the time the&lt;br&gt;
check ran, today's position had already been opened. The guard was standing in&lt;br&gt;
front of a door nobody was going to walk through again until tomorrow.&lt;/p&gt;

&lt;p&gt;Every test I'd written asked the same question in different clothes: &lt;em&gt;given a&lt;br&gt;
drawdown value, does the function return the correct decision?&lt;/em&gt; And it did. Every&lt;br&gt;
time. Correctly.&lt;/p&gt;

&lt;p&gt;Not one test asked the other question: &lt;em&gt;does this function get called at a moment&lt;br&gt;
when its answer can still change anything?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those two things fail independently, and I'd only been testing one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 1: a test that can't go red isn't a test
&lt;/h2&gt;

&lt;p&gt;This is the one that changed how I work.&lt;/p&gt;

&lt;p&gt;A test proves nothing unless you've seen it fail. Until then you haven't tested&lt;br&gt;
your code, you've tested the happy path of your test.&lt;/p&gt;

&lt;p&gt;So now every meaningful check gets a dose: I deliberately break the thing under&lt;br&gt;
test and require the suite to go red. Set the threshold absurdly tight and the&lt;br&gt;
feature must trigger. Feed the parser a malformed input and it must refuse. Point&lt;br&gt;
the monitor at a dead process and it must alert.&lt;/p&gt;

&lt;p&gt;If the suite stays green while the system is visibly broken, the test was&lt;br&gt;
decoration.&lt;/p&gt;

&lt;p&gt;This has caught more of my mistakes than any coverage report ever did. Coverage&lt;br&gt;
tells you a line was executed. It doesn't tell you that executing it proved&lt;br&gt;
anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habit 2: count, don't claim
&lt;/h2&gt;

&lt;p&gt;While I was auditing tests, I found this in my own harness:&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>programming</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
