<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c" -->

---
title: One Flag, Two Checkpoints: How a Canceled Upload...
description: A detailed postmortem traces a bug where canceling a video upload in an Android SDK could still get reported as a successful publish, leaving ghost entries in...
canonical: https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: One Flag, Two Checkpoints: How a Canceled Upload Reported Success | daily.dev
og:description: A detailed postmortem traces a bug where canceling a video upload in an Android SDK could still get reported as a successful publish, leaving ghost entries in...
og:url: https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c
og:image: https://api.daily.dev/og/posts/sMSiLdP0c.png
og:image:alt: One Flag, Two Checkpoints: How a Canceled Upload Reported Success
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# One Flag, Two Checkpoints: How a Canceled Upload Reported Success

**[ProAndroidDev](https://daily.dev/sources/pand)** · 13 min read · 4 upvotes · 2 comments

## Summary

A detailed postmortem traces a bug where canceling a video upload in an Android SDK could still get reported as a successful publish, leaving ghost entries in the UI grid. The root cause was two independent silent failures: an interrupt swallowed by an old polling loop's catch block, and an executor that called future.cancel() but never invoked onCancel() to set the cancellation flag. Either fix alone would have solved it, but both bugs stacked for years until a screen recording caught the race. The team first built an elaborate synchronous cancellation API across seven files, then deleted it an hour later in favor of a minimal two-checkpoint fix inside the task itself that checks a volatile flag after upload() returns and inside the progress callback. The piece closes with a call to encode contracts like 'cancel must set a flag' as automated tests rather than relying on reviewers to catch them.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://proandroiddev.com/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-9f8d3e42f0cc>

## Questions this post answers

### Why does canceling a Future with future.cancel(true) sometimes not actually stop the running task?

Interrupting a thread only sets a boolean flag; it is a request, not a guaranteed stop. If the thread is blocked in an interruptible operation like a channel read, it throws an exception such as ClosedByInterruptException, but if that exception gets caught and swallowed somewhere in the call stack without rethrowing, the task can return normally as if nothing happened, silently defeating the cancellation.

_Developers debugging flaky cancellation races can dig into similar concurrency postmortems on daily.dev._

### How should a task's cancellation flag be checked to avoid races between interrupts and normal returns?

Set the cancellation flag before triggering the interrupt, and check that flag at multiple checkpoints rather than relying on an exception being thrown. A checkpoint inside a progress callback catches most cases, but a second checkpoint immediately after the blocking call returns is needed because a swallowed interrupt can cause the call to exit without invoking the callback again.

_Anyone hardening cancellation logic against swallowed exceptions can find deeper concurrency writeups on daily.dev._

### Why is @Volatile necessary on a cancellation flag shared between threads in Java or Kotlin?

Without @Volatile, a worker thread may keep reading a stale cached value of the flag even after the caller thread sets it to true, because there is no happens-before guarantee between the write and the read. Marking the flag @Volatile ensures the worker thread observes the update, which matters especially since a boolean write cannot be swallowed the way an exception can.

_Engineers reasoning about shared mutable state across threads can track more concurrency patterns via daily.dev._

## Community discussion

Top comments from developers on daily.dev.

**@leonidbugaev** · 0 upvotes

> The interrupt got swallowed and cancel() never set the flag, so the UI took the success branch. I've hit that shape. The seven-file cancellation API was the tempting move; checking the flag after upload() returns is the check that would have failed on the recording.

## Similar posts on daily.dev

- [A simple fix can save a business – not every solution needs fancy development](https://daily.dev/posts/a-simple-fix-can-save-a-business-not-every-solution-needs-fancy-development-o9pyykp2k) · Medium · 0 upvotes · 0 comments

---

Tags: [#java](https://daily.dev/tags/java), [#android](https://daily.dev/tags/android)

[View this post on daily.dev](https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"One Flag, Two Checkpoints: How a Canceled Upload Reported Success","url":"https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c"},"datePublished":"2026-08-31T14:13:38.037Z","dateModified":"2026-09-14T07:20:44.500Z","description":"A detailed postmortem traces a bug where canceling a video upload in an Android SDK could still get reported as a successful publish, leaving ghost entries in...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/f30c1f94df2ec61edbb4303e45924283?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/f30c1f94df2ec61edbb4303e45924283?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"ProAndroidDev","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"ProAndroidDev","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/656c1eb1f1ef4f91936e86fc16545114","url":"https://daily.dev/sources/pand"},"commentCount":2,"discussionUrl":"https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":4},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":2}],"keywords":"java,android","timeRequired":"PT13M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"ProAndroidDev","item":"https://daily.dev/sources/pand"},{"@type":"ListItem","position":3,"name":"One Flag, Two Checkpoints: How a Canceled Upload Reported Success"}]}
{"@context":"https://schema.org","@type":"WebPage","@id":"https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c","comment":[{"@type":"Comment","text":"The interrupt got swallowed and cancel() never set the flag, so the UI took the success branch. I’ve hit that shape. The seven-file cancellation API was the tempting move; checking the flag after upload() returns is the check that would have failed on the recording.","datePublished":"2026-08-31T14:57:58.878Z","url":"https://daily.dev/posts/sMSiLdP0c#c-8ugdgNyvU","author":{"@type":"Person","name":"Leonid Bugaev","url":"https://daily.dev/leonidbugaev","image":"https://lh3.googleusercontent.com/a/ACg8ocLhMgurwTmJElWaH9A8Ju8cHvZxgMCDt009jEYkmFCyUGAoaGSw=s96-c"}}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/one-flag-two-checkpoints-how-a-canceled-upload-reported-success-smsildp0c#faq","mainEntity":[{"@type":"Question","name":"Why does canceling a Future with future.cancel(true) sometimes not actually stop the running task?","acceptedAnswer":{"@type":"Answer","text":"Interrupting a thread only sets a boolean flag; it is a request, not a guaranteed stop. If the thread is blocked in an interruptible operation like a channel read, it throws an exception such as ClosedByInterruptException, but if that exception gets caught and swallowed somewhere in the call stack without rethrowing, the task can return normally as if nothing happened, silently defeating the cancellation. Developers debugging flaky cancellation races can dig into similar concurrency postmortems on daily.dev."}},{"@type":"Question","name":"How should a task's cancellation flag be checked to avoid races between interrupts and normal returns?","acceptedAnswer":{"@type":"Answer","text":"Set the cancellation flag before triggering the interrupt, and check that flag at multiple checkpoints rather than relying on an exception being thrown. A checkpoint inside a progress callback catches most cases, but a second checkpoint immediately after the blocking call returns is needed because a swallowed interrupt can cause the call to exit without invoking the callback again. Anyone hardening cancellation logic against swallowed exceptions can find deeper concurrency writeups on daily.dev."}},{"@type":"Question","name":"Why is @Volatile necessary on a cancellation flag shared between threads in Java or Kotlin?","acceptedAnswer":{"@type":"Answer","text":"Without @Volatile, a worker thread may keep reading a stale cached value of the flag even after the caller thread sets it to true, because there is no happens-before guarantee between the write and the read. Marking the flag @Volatile ensures the worker thread observes the update, which matters especially since a boolean write cannot be swallowed the way an exception can. Engineers reasoning about shared mutable state across threads can track more concurrency patterns via daily.dev."}}]}
```

