DEV Community

Anton Brilliantov
Anton Brilliantov

Posted on

A Chain of Links, Not a Guess

Measure the path the data takes, one number per link - not the endpoint that answered last.


๐Ÿ‘‹ Hi, I'm Anton - a software engineer working mostly in PHP/Symfony and Go, currently carving a live PHP monolith into Go services. This series is about a complaint that keeps landing on that migration: a user sees an empty screen, the system thinks everything is fine, and one service gets blamed by default. This part is the method I use now to take those apart. Running notes live on my GitHub: github.com/brilliant-almazov.

It is how I work today, not a methodology - maybe you already do it better, maybe you'd do it another way entirely.


Two different questions

Checking a handle means asking: did the call answer correctly? Status code, body shape, latency, error text. Clean question, clean test.

Checking data means asking a different one: should this entity have had anything here at all?

The second question closes most of the complaints. It also can't be asked until the path the data takes is split into links - "should there be data here" is meaningless for a whole path at once. It only has an answer per link.

Before that split, the first question was all I had, and it produced a specific kind of noise: two thirds of the entities came back with nothing, that read as a contract defect, and the few real failures drowned in it.

The move from the first question to the second isn't a better assertion library. It's a different object under test: not the call, the path.

The chain, and who owns each link

A link is a place where data is obliged to appear, and it has exactly one owner. Top to bottom, from the schedule to the finished markup:

schedule of the configuration        โ†’ the scheduler
set of keyword queries               โ†’ the dictionary/catalog service
collection task                      โ†’ the tool-task service
task run + its counters              โ†’ the tool-task service
raw result rows                      โ†’ the provider + the raw-data service
snapshot of entities                 โ†’ the domain-rule-map service
applied rules โ†’ markup               โ†’ the rule-set-markup service
rebuild of the result on request     โ†’ the rule-set-markup service
Enter fullscreen mode Exit fullscreen mode

The eight links of the path from schedule to markup, each with the one number that counts as its fact on the left and its owning service on the right

Two things about that list before any measurement.

The owner column is the point of the exercise, not decoration. A link without a single named owner isn't a link - it's a region of the system, and a region can't be handed to anyone.

And some links are governed by conditions no read exposes: which types of schedule exist and what each means, which regions the provider covers at all. Not defects, not gaps in the code - domain facts that live outside every handle. The largest cohort below is decided by one of them.

Note what the last two lines mean. The service blamed for the empty screen owns the bottom of the chain. It can't produce markup from a snapshot that was never taken, or snapshot raw rows that never arrived. Everything above it is somebody else's obligation - and until the chain is written down, saying that out loud sounds like deflection.

One number per link

The measurement rule is what makes this cheap enough to repeat.

Per link, take exactly one number, with a cheap request at page size 1: read the total count off the response header, don't download the selection.

That's the difference between a method and an intention. Pulling rows for eight links across a vertical takes hours, so nobody runs it twice. Reading eight counts takes tens of seconds, so it gets re-run after every fix - and "before" and "after" become the same measurement rather than two investigations.

The number isn't evidence of correctness. It's evidence of presence. That's all a link has to answer: is there anything here. Whether what's there is right is a later question, asked only of the links that came back full.

Choosing that number is the one genuinely hard step, and it's a decision rather than a lookup: one thing per link is declared to be the fact, and every later reading uses it - including the ones that contradict what I expect.

A control subject next to the suspect

One number on its own is a guess. A link gets checked as a pair: a control subject where everything works, and the suspect, same measurement, side by side.

                  control subject          suspect subject
                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
schedule        โ”‚      n > 0      โ”‚      โ”‚      n > 0      โ”‚
                โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
queries         โ”‚      n > 0      โ”‚      โ”‚      n > 0      โ”‚
                โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
task            โ”‚      n > 0      โ”‚      โ”‚      n > 0      โ”‚
                โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
runs            โ”‚      n > 0      โ”‚      โ”‚        0        โ”‚
                โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
raw rows        โ”‚      n > 0      โ”‚      โ”‚        0        โ”‚
                โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
snapshot        โ”‚      n > 0      โ”‚      โ”‚        0        โ”‚
                โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
markup          โ”‚      n > 0      โ”‚      โ”‚        0        โ”‚
                โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
rebuild         โ”‚      n > 0      โ”‚      โ”‚        0        โ”‚
                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

a conclusion from one field without the pair is a guess
Enter fullscreen mode Exit fullscreen mode

Two columns, a control subject and a suspect subject, one row per link and one number per cell, under a caption saying a conclusion from one field without the pair is a guess

In this breakdown I got that wrong twice, the same shape both times: a conclusion drawn from one field, without the pair.

The clearer of the two: I concluded a task had never reached the provider, because the field naming the provider on the keyword-query row was empty. Then I ran the same read against a control subject that already had data - same field, also empty, while the task's own status said it was accepting data. The field was never the fact; the status was. The full version of that trap, and the coverage factor next to it, is in Part 3.

The pair isn't a nicety or "running it twice". It's the only thing separating this entity has no data from this reading has no data - an empty number means both, and the response won't say which.

Both mistakes cost the same: a conclusion about a service that was doing its job. Worse, they're wrong in the expensive direction - they send work to a team with nothing to fix.

Eight cohorts

Once the chain is measured, every case lands in exactly one cohort:

  • result present - nothing to explain
  • schedule of a different type - never scheduled for this kind of collection ยท the scheduler
  • no keyword queries - nothing to collect for ยท the dictionary/catalog service
  • schedule present, no runs - it should have fired and didn't ยท the tool-task service
  • runs present, no raw rows - collected, nothing back yet ยท the provider + the raw-data service
  • raw rows present, no snapshot ยท the domain-rule-map service
  • snapshot present, no result ยท the rule-set-markup service
  • region the provider does not cover - decided from the configuration's region alone ยท the provider
COHORT                              OWNER OF THE EMPTY LINK      COUNT
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€   โ”€โ”€โ”€โ”€โ”€
result present                      nothing to explain              76
schedule of a different type        scheduler                      184
no keyword queries                  dictionary/catalog service      14
schedule present, no runs           tool-task service                โ€”
runs present, no raw rows           provider + raw-data service     21
raw rows present, no snapshot       domain-rule-map service          0
snapshot present, no result         rule-set-markup service          0
region the provider does not cover  the provider                     4
Enter fullscreen mode Exit fullscreen mode

Table of the eight cohorts with the owner of the empty link and the count of configurations in each, the row for entities that already have a result highlighted

The rule that makes this useful: "unclear" is not a cohort. It's an unfinished measurement. A case that doesn't land means the next link hasn't been read - a task, not a verdict.

How a conclusion is phrased

Not "the service is broken". The form is:

link X is empty; the owner of link X is role Y.

That's the only phrasing I can hand to another team without a discussion attached. It names a fact and an address, it alleges no defect, and the receiving team can check it with the same number I read. "The markup service is losing data" starts an argument. "The snapshot link is empty, and that link belongs to the domain-rule-map service" starts a look.

It also survives being wrong. If the owner reads the same link and finds it full, the disagreement is about one measurement, not about whose service is at fault.

What the numbers did

The vertical had 276 configurations. Sorted by cohort:

Cohort Configurations
result present 76
schedule of a different type 184
no keyword queries 14
region the provider does not cover 4
waiting for collection 21
raw rows present, no snapshot 0
snapshot present, no result 0

The last two rows carry the accusation. Those are the two states where the blamed service would actually be losing data - raw rows in, no snapshot; snapshot in, no result - and both are zero. Where there was input, there was output.

The 184 is the row to read twice. Those configurations are scheduled for a different type of collection: they were never going to have this result, and the system was right about every one of them.

After the sorting, two configurations were left as genuine questions: one whose schedule is active but has never fired once, and one whose tasks are created daily and sit in the provider's queue. Two hundred-odd "problems" became two - and neither sat in the service the complaint pointed at.

What it costs

Two costs, and I'd rather name them than pretend the method is free.

The chain has to be written down once, by someone who knows the whole path: eight links, eight owners, and per link a decision about which single number counts as its fact. Nobody hands you that - it comes out of reading the system, before any measurement starts.

And every link has to be measurable that cheaply, which isn't always true. Where a total-count header lies, or a schedule isn't exposed by any read at all, the link gets measured by hand, subject by subject. There the breakdown stops being a sweep and becomes enumeration, and the cost argument above collapses for that link. It's worth knowing which links those are before promising anyone a fast answer.

The part that generalises

A chain of links with named owners is a form, not a document. It's the shape in which work can be handed to any executor - a teammate, a new hire, a fast automated one - and checked afterwards without re-running it, because the instruction and the result are both single numbers with addresses.

Without that form, a faster executor just brings a wrong conclusion sooner. Speed multiplies whatever discipline is already there; it supplies none.


This is my current answer to "there's no result", and the price of it is written above. I'd like to hear the other side: if you do this better, tell me what your links are and how you measure them; if you have been through this, tell me which link turned out to be the liar; if you see it differently, say where the pair-based reading breaks down. How is it solved on your side, and what broke there?


Why there is no result - Part 2.

Next: the two factors that cut cases out of the breakdown before any link is read - a provider that does not cover a region at all, and a provider queue where a task is waiting rather than lost.

Top comments (0)