<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tensoru]]></title><description><![CDATA[A simple and accessible blog about machine learning and automation software development. Clear, insightful, and practical examples for both new and experienced ]]></description><link>https://blog.tensoru.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1722334892929/7d3810fc-82f8-4461-accb-4e06a3bb3b0e.png</url><title>Tensoru</title><link>https://blog.tensoru.com</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 18 May 2026 11:36:05 GMT</lastBuildDate><atom:link href="https://blog.tensoru.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[ufw-docker rule ignored]]></title><description><![CDATA[Every time I restarted the container, ufw-docker apparently trashed the rule. This is bad, as if something really bad happened, I don't want my service left inaccessible.
How to fix this? Simple, just]]></description><link>https://blog.tensoru.com/ufw-docker-rule-ignored</link><guid isPermaLink="true">https://blog.tensoru.com/ufw-docker-rule-ignored</guid><dc:creator><![CDATA[Faruq Sandi]]></dc:creator><pubDate>Tue, 05 May 2026 14:30:40 GMT</pubDate><content:encoded><![CDATA[<p>Every time I restarted the container, ufw-docker apparently trashed the rule. This is bad, as if something really bad happened, I don't want my service left inaccessible.</p>
<p>How to fix this? Simple, just put the ufw-docker command into the crontab. Do <code>sudo crontab -e</code> , then put the command below to run it every minute.</p>
<pre><code class="language-plaintext">* * * * * ufw-docker allow traefik 80 &amp;&amp; ufw-docker allow traefik 443 2&gt;&amp;1 | systemd-cat -t cron-ufw
</code></pre>
<p>Then log it using <code>journalctl -t cron-ufw</code>:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66a090e326ea2a8a7954e947/13b3bb4a-aa87-4f52-957f-15fdf04d7315.png" alt="" style="display:block;margin:0 auto" />

<p>Done.</p>
]]></content:encoded></item><item><title><![CDATA[Setting Up a Virtual Webcam on Linux]]></title><description><![CDATA[I was testing a web app that involves taking pictures. Once again, my attached webcam wasn't working, and I didn't bother to check this minor issue. Just as 42 is the answer to everything, so is v4l2l]]></description><link>https://blog.tensoru.com/setting-up-a-virtual-webcam-on-linux</link><guid isPermaLink="true">https://blog.tensoru.com/setting-up-a-virtual-webcam-on-linux</guid><dc:creator><![CDATA[Faruq Sandi]]></dc:creator><pubDate>Tue, 05 May 2026 14:17:38 GMT</pubDate><content:encoded><![CDATA[<p>I was testing a web app that involves taking pictures. Once again, my attached webcam wasn't working, and I didn't bother to check this minor issue. Just as 42 is the answer to everything, so is v<strong>4</strong>l<strong>2</strong>loopback.</p>
<p>First, let's summon the video device out of thin air:</p>
<pre><code class="language-bash">sudo modprobe v4l2loopback
</code></pre>
<p>Check this <a href="https://wiki.archlinux.org/title/V4l2loopback"><strong>extensive documentation</strong></a> [<a href="https://archive.ph/wip/DuWZf"><strong>archive</strong></a>] for details on the arguments you can use. A newly created video device is now available. In my case, it is at <code>/dev/video3</code>.</p>
<p>Now, see how <code>ffmpeg</code> takes input from <code>testrc</code> and then writes it to <code>/dev/video3</code>.</p>
<pre><code class="language-bash">ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 -f v4l2 /dev/video3
</code></pre>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759422831106/389a289f-bf51-4a57-9616-a15a4c4627de.png" alt="" style="display:block;margin:0 auto" />

<p>Similarly, we can use <code>ffplay</code> to show the video.</p>
<pre><code class="language-bash">ffplay /dev/video3
</code></pre>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759423008047/fa7b9cb4-0ee2-4948-a3bb-b7018e198df5.png" alt="" style="display:block;margin:0 auto" />

<p>At my workplace, I used <code>ffmpeg</code> and <code>gstreamer</code> to deliver real-time video feeds to our customers. This information might be available online, but it's scattered. While this post serves as my personal note, I hope it will also be useful for you. Ciao!</p>
]]></content:encoded></item><item><title><![CDATA[Building an Image Compression Web App using FastAPI, Celery, and NextJS, Part 1: Backend.]]></title><description><![CDATA[This is my first article on @hashnode, and I'm glad they make it easy to write and let me back up my articles to GitHub. The purpose of this article is to show the tech stacks I often use. I don't want to dive into the technical aspects of this web a...]]></description><link>https://blog.tensoru.com/building-an-image-compression-web-app-using-fastapi-celery-and-nextjs-part-1-backend</link><guid isPermaLink="true">https://blog.tensoru.com/building-an-image-compression-web-app-using-fastapi-celery-and-nextjs-part-1-backend</guid><category><![CDATA[Python]]></category><category><![CDATA[FastAPI]]></category><category><![CDATA[celery]]></category><category><![CDATA[image processing]]></category><category><![CDATA[webapps]]></category><dc:creator><![CDATA[Faruq Sandi]]></dc:creator><pubDate>Wed, 31 Jul 2024 07:32:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722410698594/4717771f-44ce-48e5-aea2-955bea6c9b2e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is my first article on @hashnode, and I'm glad they make it easy to write and let me back up my articles to GitHub. The purpose of this article is to show the tech stacks I often use. I don't want to dive into the technical aspects of this web app; I prefer it to be dead simple.I really recommend to anyone finding this article difficult to hop into <a class="user-mention" href="https://hashnode.com/@bstiel">Bjoern Stiel</a>'s post about <a target="_blank" href="https://celery.school/celery-progress-bars-with-fastapi-htmx">FastAPI, Celery, and HTMX</a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">This article is not finished yet! As this writing, I am done with the backend part only. The code is available at <a target="_blank" href="https://github.com/faruqsandi/image_compression_webapp">https://github.com/faruqsandi/image_compression_webapp</a>.</div>
</div>

<p>At this occasion, we will create a python backend using FastAPI and Celery to manage image compression task. The idea is to provide the frontend a way to upload the images, get the progress, then get back the result.</p>
<p>First, clone the code from my github repo. Please be familiar with the project structure:</p>
<pre><code class="lang-bash">bit@KAUST2024:~/Playground/tensoru/image_compression_webapp$ tree -L 1
.
├── __pycache__
├── celery_app.py
├── freeze.txt
├── main.py
├── requirements.txt
├── uploads
└── venv
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Not understand why the snippet above has <code>venv</code> directory while your cloned code doesn't have it? See <a target="_blank" href="https://stackoverflow.com/questions/41972261/what-is-a-virtualenv-and-why-should-i-use-one">What is a virtualenv, and why should I use one?</a></div>
</div>

<p>I put FastAPI instance inside <code>main.py</code> and Celery instance inside <code>celery_app.py</code>.</p>
<h3 id="heading-fastapi-backend">FastAPI backend</h3>
<p>There are three endpoints currently needed for our smooth operation:</p>
<ol>
<li><p>Upload endpoint. This endpoint will save uploaded file to uploads folder with a random file name, then immediately call <code>compress_image</code> function to celery.</p>
<pre><code class="lang-python"><span class="hljs-meta"> @app.post("/upload")</span>
 <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">upload</span>(<span class="hljs-params">
     quality: int = Form(<span class="hljs-params">default=..., le=<span class="hljs-number">100</span>, ge=<span class="hljs-number">20</span></span>), file: UploadFile = File(<span class="hljs-params">...</span>)
 </span>):</span>
     random_uuid = uuid.uuid4().hex

     temporary_file_path = <span class="hljs-string">f"<span class="hljs-subst">{TEMP_DIR}</span>/<span class="hljs-subst">{random_uuid}</span>_<span class="hljs-subst">{file.filename}</span>"</span>
     os.makedirs(os.path.dirname(temporary_file_path), exist_ok=<span class="hljs-literal">True</span>)
     <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> aiofiles.open(temporary_file_path, <span class="hljs-string">"wb"</span>) <span class="hljs-keyword">as</span> buffer:
         <span class="hljs-keyword">while</span> content := <span class="hljs-keyword">await</span> file.read(<span class="hljs-number">1024</span>):
             <span class="hljs-keyword">await</span> buffer.write(content)

     <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> filetype.helpers.is_image(temporary_file_path):
         <span class="hljs-keyword">return</span> ErrorResponseSchema(message=<span class="hljs-string">"File is not an image"</span>, code=<span class="hljs-number">400</span>)

     task = compress_image.delay(temporary_file_path, quality)  <span class="hljs-comment"># type: ignore</span>
     <span class="hljs-keyword">return</span> ResponseSchema(
         data={<span class="hljs-string">"task_id"</span>: task.id}, message=<span class="hljs-string">"File uploaded successfully"</span>
     )
</code></pre>
<p> Inside <code>upload</code> function above we do three things:</p>
<ol>
<li><p>Creating a random temporary file name. This is a must because if hundreds of people upload hundreds of images, original file name even like "Jakarta_20240801_0001.JPEG" is possibly not unique. Hence it is better to append a unique <a target="_blank" href="https://en.wikipedia.org/wiki/Universally_unique_identifier">UUID</a> to it.</p>
</li>
<li><p>Save uploaded file to disk. Instead of waiting a whole file loaded into memory then save it, reading and writing in 1 MB chunk is preferred.</p>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">💡</div>
 <div data-node-type="callout-text">You must be noticed that I am using <code>aiofiles</code> to handle file IO. It is because I am insisted using async function for the upload endpoint. Using regular <code>file.read</code> will require me to use synchronous function in order to not blocking the main thread.</div>
 </div>
</li>
<li><p>Checking file type. Module <code>filetype</code> provides way to infer file type using file's <a target="_blank" href="https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files">magic number</a>. We can't rely on file extension, otherwise some kids will upload zipped roblox games to compress it.</p>
</li>
<li><p>Pass the image to <code>compress_image</code> function which later executed by celery worker.</p>
</li>
</ol>
</li>
<li><p>Get status endpoint. Task queue system like celery will take care of thousands of requests using limited amount of resources. In this article I preferred dead simple way for client to get a task status. In another occasion we will implement SSE or webhook to notify client about the tasks.</p>
<pre><code class="lang-python"><span class="hljs-meta"> @app.get("/task/{task_id}")</span>
 <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_status</span>(<span class="hljs-params">task_id: str</span>):</span>
     <span class="hljs-keyword">try</span>:
         r = celery_app.AsyncResult(task_id)

         result = r.result
         <span class="hljs-keyword">if</span> isinstance(result, CeleryError):
             result = repr(result)

         task_response = TaskOut(id=r.task_id, status=r.status, result=result).dict()
         <span class="hljs-keyword">return</span> ResponseSchema(
             data=task_response,
             message=<span class="hljs-string">"Task status retrieved successfully"</span>,
         )
     <span class="hljs-keyword">except</span> Exception:
         <span class="hljs-keyword">return</span> ErrorResponseSchema(
             code=<span class="hljs-number">500</span>, message=<span class="hljs-string">"Something went wrong while getting Task status"</span>
         )
</code></pre>
<p> Inside <code>get_status</code> function above only have one purpose: getting celery result then check if the result is an error or not. While checking if the result is an instance of <code>CeleryError</code> may not a proper way to handle it, but it is the most simple way to do it.</p>
</li>
<li><p>Download endpoint. After a task is successfully executed, this endpoint provides way to retrieve compressed file from the server.</p>
<pre><code class="lang-python"><span class="hljs-meta"> @app.get("/download/{task_id}")</span>
 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">download</span>(<span class="hljs-params">task_id: str</span>):</span>
     task = celery_app.AsyncResult(task_id)
     <span class="hljs-keyword">if</span> task.status == <span class="hljs-string">"SUCCESS"</span>:
         result = task.result
         <span class="hljs-keyword">if</span> result <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
             <span class="hljs-keyword">return</span> ErrorResponseSchema(
                 message=<span class="hljs-string">"File is already deleted, please try to upload again"</span>, code=<span class="hljs-number">500</span>
             )

         file_path = result[<span class="hljs-string">"file_path"</span>]

         <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.isfile(file_path):
             <span class="hljs-keyword">return</span> ErrorResponseSchema(
                 message=<span class="hljs-string">"File is not exist, please try to upload again."</span>, code=<span class="hljs-number">400</span>
             )

         kind = filetype.guess(file_path)
         media_type = <span class="hljs-string">"application/octet-stream"</span>
         <span class="hljs-keyword">if</span> kind:
             media_type = kind.mime
         file_name = os.path.basename(file_path)
         file_name = file_name.split(<span class="hljs-string">"_"</span>, <span class="hljs-number">1</span>)[<span class="hljs-number">1</span>]
         <span class="hljs-keyword">return</span> FileResponse(
             file_path, media_type=media_type, filename=os.path.basename(file_path)
         )
     <span class="hljs-keyword">return</span> ErrorResponseSchema(
         message=<span class="hljs-string">"File is not ready for download yet. Please try again later"</span>, code=<span class="hljs-number">400</span>
     )
</code></pre>
<p> In <code>download</code> function above, again there are four operations:</p>
<ol>
<li><p>Check if the task has <code>file_path</code>.</p>
</li>
<li><p>Check if <code>file_path</code> is exist.</p>
</li>
<li><p>Check file's <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">MIME</a>, or simply media type. Why? Because we accept ranges of images format: JPEG, PNG, etc. Hence it is important to us to give information back to browser what image they are getting.</p>
</li>
<li><p>Return the file. Of course without random UUID prefix.</p>
</li>
</ol>
</li>
</ol>
<h3 id="heading-celery-tasks">Celery tasks</h3>
<p>Function listed below are function registered for celery worker to run. There are two celery task function:</p>
<ol>
<li><p><code>compress_image</code>: This function will take care of image compression. This function is the reason we use celery since it will takes most resources.</p>
</li>
<li><p><code>delete_task_result</code>: This function will delete the task result, including the actual file. This one is important since we need to clean up piling old, unused images.</p>
</li>
</ol>
<p>As you noticed there are two more functions which are signal handlers. Depending on the task result, it will invoke <code>delete_task_result</code> function immediately in case of failure or keeping it for a longer time for user to download.</p>
<pre><code class="lang-python"><span class="hljs-meta">@app.task(queue="tnr_imgcmprs_queue", name="compress_image")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">compress_image</span>(<span class="hljs-params">file_path: str, quality: int = <span class="hljs-number">50</span></span>):</span>
    img = Image.open(file_path)
    old_filesize = os.path.getsize(file_path)
    img.save(file_path, quality=quality)
    new_filesize = os.path.getsize(file_path)
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"file_path"</span>: file_path,
        <span class="hljs-string">"old_filesize"</span>: old_filesize,
        <span class="hljs-string">"new_filesize"</span>: new_filesize,
    }


<span class="hljs-meta">@app.task(queue="tnr_imgcmprs_queue", name="delete_task_result")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">delete_task_result</span>(<span class="hljs-params">task_id</span>):</span>
    result = app.AsyncResult(task_id)
    file_path, *_ = result.args
    <span class="hljs-keyword">if</span> file_path <span class="hljs-keyword">and</span> os.path.isfile(file_path):
        os.remove(file_path)
    result.forget()


<span class="hljs-meta">@signals.task_success.connect(sender=compress_image)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task_success_handler</span>(<span class="hljs-params">sender, result, **kwargs</span>):</span>
    task_id = sender.request.correlation_id
    delete_task_result.apply_async((task_id,), countdown=<span class="hljs-number">60</span> * <span class="hljs-number">30</span>)  <span class="hljs-comment"># type: ignore</span>


<span class="hljs-meta">@signals.task_failure.connect(sender=compress_image)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task_failure_handler</span>(<span class="hljs-params">sender, task_id, **kwargs</span>):</span>
    task_id = task_id
    delete_task_result.apply_async((task_id,), countdown=<span class="hljs-number">0</span>)  <span class="hljs-comment"># type: ignore</span>
</code></pre>
<h3 id="heading-run-the-backend">Run the backend</h3>
<p>If you are using Visual Studio Code, I included <code>launch.json</code> for you to run the backend. Otherwise both FastAPI and Celery instances can be launched by executing these command separately:</p>
<pre><code class="lang-bash">uvicorn main:app
celery -A celery_app:app worker -l info -n tnr_imgcmprs -Q tnr_imgcmprs_queue
</code></pre>
<p>In the next article next week, we will try to build the frontend for this backend.</p>
]]></content:encoded></item><item><title><![CDATA[Difference in How FastAPI handle Asynchronous and Synchronous Endpoint]]></title><description><![CDATA[Asynchronous (async) and synchronous (sync) are topics I always been avoided. But recently I came across the path to look into it again. Two weeks ago an article What Color is Your Function? remind me the pain of mixing async and sync in the code. In...]]></description><link>https://blog.tensoru.com/difference-in-how-fastapi-handle-asynchronous-and-synchronous-endpoint</link><guid isPermaLink="true">https://blog.tensoru.com/difference-in-how-fastapi-handle-asynchronous-and-synchronous-endpoint</guid><category><![CDATA[Python]]></category><category><![CDATA[FastAPI]]></category><dc:creator><![CDATA[Faruq Sandi]]></dc:creator><pubDate>Wed, 31 Jul 2024 06:32:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722407472586/e8a8f1c8-e7dc-4c1a-aadb-bf0deed36ea1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Asynchronous (async) and synchronous (sync) are topics I always been avoided. But recently I came across the path to look into it again. Two weeks ago an article <a target="_blank" href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">What Color is Your Function?</a> remind me the pain of mixing async and sync in the code. In FastAPI, the way endpoints handle requests depends on whether they are defined as synchronous (<code>def</code>) or asynchronous (<code>async def</code>). Let's break down the explanation:</p>
<h3 id="heading-synchronous-endpoints-def">Synchronous Endpoints (<code>def</code>)</h3>
<ol>
<li><p><strong>Synchronous Function</strong>: A function defined with <code>def</code> is synchronous, meaning it will execute from start to finish before moving on to the next task.</p>
</li>
<li><p><strong>External Threadpool</strong>: FastAPI runs these synchronous endpoints in a separate thread from an external threadpool.</p>
</li>
<li><p><strong>Asynchronous Handling</strong>: Even though the function itself is synchronous, FastAPI manages it asynchronously by waiting for the thread to complete. This allows the server to handle other requests concurrently.</p>
</li>
<li><p><strong>Concurrency</strong>: Each incoming request to a synchronous endpoint will use a new thread or reuse an existing one from the threadpool. This way, the server avoids being blocked by long-running synchronous tasks.</p>
</li>
</ol>
<h3 id="heading-asynchronous-endpoints-async-def">Asynchronous Endpoints (<code>async def</code>)</h3>
<ol>
<li><p><strong>Asynchronous Function</strong>: A function defined with <code>async def</code> is asynchronous, allowing it to yield control back to the event loop at certain points using <code>await</code>.</p>
</li>
<li><p><strong>Event Loop</strong>: These endpoints run directly in the event loop, which operates in the main (single) thread.</p>
</li>
<li><p><strong>Non-Blocking Operations</strong>: If the function contains <code>await</code> calls to non-blocking I/O operations (like reading from a database, waiting for network data, etc.), the server can handle multiple requests concurrently.</p>
</li>
<li><p><strong>Concurrency</strong>: The event loop can manage many such asynchronous tasks at once, making the server more efficient in handling I/O-bound operations.</p>
</li>
</ol>
<h3 id="heading-blocking-behavior">Blocking Behavior</h3>
<ol>
<li><p><strong>Lack of</strong> <code>await</code> in <code>async def</code>: If an <code>async def</code> function does not contain any <code>await</code> calls, it does not yield control back to the event loop.</p>
</li>
<li><p><strong>Sequential Processing</strong>: In this case, the function will block the event loop until it finishes. As a result, requests to this endpoint (or others) will be processed sequentially, which can degrade performance.</p>
</li>
</ol>
<h3 id="heading-summary">Summary</h3>
<ul>
<li><p><code>def</code> Endpoints: Run in separate threads using an external threadpool, allowing FastAPI to handle them asynchronously by managing the threads.</p>
</li>
<li><p><code>async def</code> Endpoints: Run in the event loop, and can handle multiple requests concurrently if they contain <code>await</code> calls for non-blocking I/O operations.</p>
</li>
<li><p><strong>Avoiding Blocking</strong>: For <code>async def</code> endpoints to be efficient, they should include <code>await</code> calls to ensure they do not block the event loop, thus maintaining asynchronous processing.</p>
</li>
</ul>
<p>This approach ensures that FastAPI can handle both synchronous and asynchronous functions effectively, providing concurrency and efficient request handling. But make sure that you don't choose wrong function call. See <a target="_blank" href="https://stackoverflow.com/questions/71516140/fastapi-runs-api-calls-in-serial-instead-of-parallel-fashion">FastAPI runs api-calls in serial instead of parallel fashion</a> for deeper insight.</p>
]]></content:encoded></item><item><title><![CDATA[Test article]]></title><description><![CDATA[Test article]]></description><link>https://blog.tensoru.com/test-article</link><guid isPermaLink="true">https://blog.tensoru.com/test-article</guid><dc:creator><![CDATA[Faruq Sandi]]></dc:creator><pubDate>Tue, 30 Jul 2024 10:11:26 GMT</pubDate><content:encoded><![CDATA[<p>Test article</p>
]]></content:encoded></item></channel></rss>