Blog › ICP guides

Harbour developer on retainer: DBF record lock not released on failure branch, Harbour Project developer, xHarbour developer on monthly retainer

October 9, 2026 · ~15 min read

A Harbour developer was maintaining a multi-user retail point-of-sale application at a regional auto parts retailer running Harbour 3.2 on eight Windows 10 workstations, with the DBF database files shared over a Windows Server 2019 file share using SMB. The application — originally written in Clipper 5.3 in 1997 and migrated to Harbour in 2012 to run on 64-bit Windows — managed inventory, customer accounts, and sales transactions across all eight POS workstations simultaneously. A procedure named ProcessSale in INVENTORY.PRG handled the workflow for processing a retail sale: it positioned the inventory DBF to the correct record with DBGOTO(nRecNo) and then called DBRLOCK() to acquire an exclusive record-level lock on the inventory item being sold, preventing other workstations from modifying the same inventory row while the sale was being processed. After acquiring the lock, the procedure performed an inventory quantity check — confirming that the quantity on hand exceeded the quantity requested for the sale — and if the check passed, decremented the inventory quantity with REPLACE Qty_On_Hand WITH (Qty_On_Hand - nQtyRequested) and called DBRUNLOCK() to release the lock before returning. When the quantity check failed — when the store had run out of the requested item — the failure branch at line 52 called ALERT('Insufficient stock for this item') to display a dialog to the cashier, followed by RETURN .F. to return a failure code to the calling sales form. The developer who wrote the quantity check failure branch understood that RETURN would exit the procedure cleanly. What was not accounted for was that RETURN in a Harbour procedure exits the procedure immediately and returns the specified value — but it does not release any DBF record locks that were acquired by DBRLOCK() during the procedure’s execution. The DBRLOCK() called at line 44 remained active after RETURN .F. at line 52. The exclusive lock on INVENTRY.DBF record 1247 — the “WH-2500-BLK” wheel hub assembly — was held by the cashier’s Harbour session for the duration of the session. Other POS workstations calling DBRLOCK() on the same record received .F. (lock failed) and displayed “Record is locked by another user.” Lock contention errors per shift: 8–12; 0 after DBRUNLOCK() was added before every RETURN within the DBRLOCK() scope in ProcessSale.

The root cause was the relationship between Harbour’s DBF record-level locking model and the scope at which record locks are held. In Harbour, when a DBF file is opened in shared mode (as all files must be opened on a multi-user network deployment), record-level exclusive locks are managed by the operating system’s file locking mechanism on the underlying DBF file on the network share. DBRLOCK() acquires an exclusive byte-range lock on the specific record position within the DBF file, using either DOS-compatible lock offsets (for DBFNTX RDD) or the extended lock table format (for DBFCDX or DBFNSX RDDs). This lock is owned by the workstation’s Harbour process. It is not scoped to a code block, a function, or a transaction. It is released only by one of three events: an explicit DBRUNLOCK() call for that specific record, a call to DBUNLOCK() which releases all record locks for the current workstation session, or the termination of the Harbour process. A RETURN statement in a Harbour procedure exits the procedure’s call frame and transfers control back to the caller, but it has no effect whatsoever on any active DBF record locks held by the session. After RETURN .F. exits ProcessSale, control returns to the sales form’s event handler, which displays the insufficient-stock message and waits for the cashier’s next action. The Harbour session continues running normally, with the INVENTRY.DBF record 1247 lock still active. The cashier dismisses the dialog, moves to a different transaction, and continues working — while record 1247 remains locked for their entire session.

The single-workstation test invisibility follows the standard multi-user lock bug pattern. The developer who wrote the ProcessSale quantity check failure branch tested it on a development workstation with the application running in standalone mode — with the DBF files opened locally rather than over the file share, or as the only Harbour process accessing the file share at the time of testing. They called ProcessSale with a SKU that had zero inventory, confirmed that the insufficient-stock dialog appeared and that the procedure returned .F., and verified that the sales form handled the failure return code correctly. Since only one workstation was accessing INVENTRY.DBF, no other workstation attempted DBRLOCK() on record 1247 during the test, and no lock contention occurred. In production with eight POS workstations sharing the same DBF files over the file share, the collision became visible at peak afternoon trading when multiple cashiers were processing sales simultaneously: one cashier at workstation 3 called ProcessSale for a out-of-stock wheel hub assembly, held the lock on record 1247 after the insufficient-stock dialog, and other cashiers at workstations 5 and 7 attempting to process sales for the same item received “Record is locked by another user.” The lock was resolved only when the cashier at workstation 3 closed and restarted the Harbour POS application, terminating the Harbour process and releasing all held locks.

Harbour and xHarbour: the open-source successors to Clipper for the legacy xBase installed base

Harbour is an open-source xBase compiler that compiles programs written in the xBase language (a superset of dBase III+ and Clipper syntax) to C source code, which is then compiled to a native executable using GCC, Clang, or MSVC. The Harbour Project was initiated in 1999 by Antonio Lovol and a group of volunteers who sought to create a freely available, open-source successor to CA Clipper (Computer Associates Clipper, the dominant PC database application development language of the late 1980s and early 1990s) after Clipper’s commercial future became uncertain under Computer Associates ownership. Clipper 5.2 and 5.3 had been the workhorse of a generation of DOS business applications — inventory systems, accounting systems, POS systems, order management systems, and customer databases built by small businesses, vertical-market ISVs, and government departments throughout the 1980s and 1990s — and the Harbour Project allowed those applications to be recompiled to run on 32-bit and later 64-bit Windows and Linux without the complete rewrite that a migration to Visual Basic or Delphi would require.

xHarbour is a competing fork of the early Harbour Project codebase, created in the early 2000s by a separate group of developers who wanted to extend the Clipper-compatibility layer with additional features including OLE automation, expanded Windows API access, and modifications to the locking model for network deployments. xHarbour and Harbour diverged significantly through the 2000s, and code written for xHarbour may require modification to compile cleanly under current Harbour 3.2. Both projects target the same installed base of legacy Clipper applications, but the Harbour Project has been the more actively maintained of the two since approximately 2010. The distinction matters for retainer engagements because the DBRLOCK/DBRUNLOCK behavior and the lock table format differ between the two compilers on Windows SMB file shares — what resolves a lock contention issue under Harbour 3.2 with DBFNTX may require slightly different handling under xHarbour with DBFCDX.

The DBF file format itself — the dBase III database format from the early 1980s — has a fundamental architectural constraint that makes multi-user locking over a network file share qualitatively different from multi-user locking in a client-server relational database. In a client-server database like SQL Server or PostgreSQL, the server process manages all lock state centrally: every lock acquisition and release goes through the server, the server has full visibility into all active locks, and the server can detect deadlocks, enforce timeout policies, and automatically release locks when a client connection terminates abnormally. In a DBF file shared over a Windows SMB file share, there is no central server process managing lock state. Each workstation’s Harbour process acquires and releases locks by making Windows file locking API calls (LockFileEx / UnlockFileEx) directly on the shared DBF file. If a Harbour process acquires a record lock with DBRLOCK() and then terminates abnormally (process crash, network disconnection, or power failure), the Windows operating system releases the lock because the process handle is gone. But if the Harbour process is still running — still displaying its window to the cashier — but simply failed to call DBRUNLOCK() on a specific record, the lock remains held by the operating system file lock, invisible to every other Harbour workstation, until the holding Harbour process terminates.

Harbour DBF record locking API: DBRLOCK, DBRUNLOCK, DBRLOCKLIST, DBFLOCK, DBUNLOCK

Harbour’s DBF record locking functions are inherited from the Clipper 5.x and dBase locking model and operate at the granularity of individual records within a single DBF file. DBRLOCK() (also available as the function alias RLOCK()) attempts to acquire an exclusive lock on the current record in the active DBF work area. Returns .T. if the lock was acquired successfully, .F. if the lock failed (because another workstation holds the lock). The lock is acquired by placing a byte-range lock on the record’s position within the DBF file using the underlying OS file locking API. DBRUNLOCK() releases the exclusive lock on the current record, removing the byte-range lock from the DBF file. It must be called on the same workstation session that called DBRLOCK(), positioned to the same record. DBRLOCKLIST() returns an array of record numbers that are currently locked by the current Harbour session — useful for diagnosing which records the current process is holding locks on, analogous to querying sys.dm_tran_locks in SQL Server for the current session. DBFLOCK() acquires a file-level exclusive lock on the entire DBF file, blocking all other workstations from accessing any record. Used for operations that modify the DBF header (PACK, ZAP, REINDEX) where record-level locking is insufficient. DBUNLOCK() releases all record-level locks held by the current session for the active work area — the bulk unlock equivalent that avoids the need to call DBRUNLOCK() individually for each locked record in procedures that acquire multiple locks. The correct pattern for any Harbour procedure that calls DBRLOCK() is the same as the correct pattern for any platform requiring explicit lock management: every code path that exits the procedure — the success path, every validation failure path, and every error-handling path — must call either DBRUNLOCK() for the specific locked record or DBUNLOCK() for the entire work area before returning.

The ProcessSale procedure in the auto parts retailer incident had three RETURN statements within the DBRLOCK() scope, not one. The developer who identified the insufficient-stock failure path at line 52 as the lock-hold bug fixed it by adding DBRUNLOCK() before RETURN .F.. A thorough audit of the full procedure revealed two additional RETURN paths that also lacked DBRUNLOCK(): the payment authorization failure at line 67 (which called RETURN .F. after a credit card decline, leaving any lock acquired during the payment authorization window held for the session), and the receipt printer error at line 81 (which called ALERT('Printer error') followed by RETURN .F. after a receipt printer communication failure). Both paths were correct in their business logic — a declined payment should abort the sale, and a printer error should prompt the cashier to retry — but both left the inventory record locked after returning. The correct fix for all three paths: DBRUNLOCK() immediately before every RETURN .F. that exits the procedure while the inventory record might be locked, or (the preferred pattern for procedures with many exit paths) a single DBUNLOCK() call at the top of the cleanup block before any RETURN, regardless of success or failure.

Diagnosing a Harbour DBF record lock hold requires correlating the symptoms on the blocked workstations with the lock state on the holding workstation. DBRLOCKLIST() on the holding workstation returns the array of record numbers currently locked by that session — if called from within a Harbour debug session or a diagnostic utility, it will confirm that record 1247 is locked on workstation 3. Network-level diagnosis uses the Windows Server’s Computer Management console: under Shared Folders → Open Files, the server lists all files currently held open by remote sessions, with the workstation name and lock count. A DBF file that shows a non-zero lock count for a workstation session that has been idle for several minutes is a strong indicator of a stuck record lock. Alternatively, the Windows Server’s net files command lists open files with their lock counts: net files | findstr INVENTRY will show the file ID, path, user (workstation), and number of locks for each connected session holding the DBF file open. Matching the workstation name from net files with the DBRLOCKLIST() output from a diagnostic utility on that workstation confirms both the holding session and the specific locked record numbers.

Typical Harbour developer retainer work and what it looks like in a work log

DBF record lock not released on RETURN failure branch is the canonical Harbour invisible production lock bug for any procedure that uses DBRLOCK() with multi-path validation logic. It appears in the work logs of Harbour retainer engagements for retail POS systems, inventory management systems, order processing systems, and customer account management systems that were originally written for Clipper in the early 1990s and migrated to Harbour in the 2000s or early 2010s. The work log entry that makes this diagnosable: “Harbour 3.2 / DBFNTX RDD; PRG file INVENTORY.PRG; procedure ProcessSale; DBF file INVENTRY.DBF (shared on \\\\FILESERVER\\POSDB); DBRLOCK() at line 44 (after DBGOTO(nRecNo) positioning to record 1247, SKU WH-2500-BLK); inventory quantity check failure at line 52: IF nQtyRequested > nQtyOnHand; ALERT('Insufficient stock for this item'); RETURN .F.; ENDIF — RETURN .F. without DBRUNLOCK(); lock diagnosis: DBRLOCKLIST() on workstation 3 = {1247}; net files output on FILESERVER: INVENTRY.DBF, user WORKSTATION3\CASHIER, locks 1; blocked workstations: 5 and 7, DBRLOCK() returned .F., displayed 'Record is locked by another user'; lock contention errors per shift before fix: 8–12; fix: DBRUNLOCK() added before RETURN .F. at line 52; full ProcessSale procedure audited for additional RETURN paths within DBRLOCK() scope — 2 more found: payment authorization failure at line 67 (RETURN .F. after card decline) and receipt printer error at line 81 (RETURN .F. after printer communication failure) — both fixed; lock contention errors per shift after fix: 0; hours: 2.5h.”

Harbour application compilation and Clipper compatibility patching is the most common day-to-day Harbour retainer task for organizations that still run applications originally written for Clipper 5.2 or 5.3 and have been maintaining them on Harbour since the 2010s. The typical engagement: the organization has a new Windows 11 deployment and needs to recompile the Harbour application to run as a 64-bit executable. The retainer developer pulls the current Harbour source (PRG files) and the current Harbour 3.2 build environment (MinGW64 on Windows), runs the Harbour compiler, resolves any compilation warnings or errors from Clipper-era syntax that Harbour 3.2 does not accept without modification, adjusts the makefile for the 64-bit target, tests the compiled executable against the production DBF data, and confirms that the multi-user locking behavior over the file share is identical to the previous 32-bit build. Work log: “Harbour 3.2.0 / MinGW64; recompile INVENTORY.PRG, SALES.PRG, CUSTOMER.PRG, REPORTS.PRG for 64-bit Windows 11; 3 compilation warnings resolved (deprecated __PROTO__ syntax removed from 2 header files; MEMVARBLOCK() replaced with {|x| &(x)} in 1 procedure); makefile updated for WIN64 target; tested on Windows 11 VM with shared DBF files on FILESERVER; all 47 test transactions completed without error; multi-user lock behavior confirmed identical to 32-bit build via concurrent test on 2 workstations; deployed; 4h.”

Harbour DBF-to-SQL backend migration is the highest-investment Harbour retainer engagement for organizations whose DBF-based applications have outgrown DBF’s multi-user limits — either because the DBF record count is approaching the practical limit (around 1 billion records for DBFCDX), because the SMB file share performance is no longer adequate for peak-concurrency transactions, or because the organization needs SQL query capabilities (JOINs, GROUP BY, subqueries) that are not available with DBF/NTX indexing. The migration path for a Harbour application is typically to Advantage Database Server (ADS) RDD, which allows the Harbour application to use SQL Server on the back end while keeping the Harbour source code largely unchanged (ADS provides a Harbour-compatible RDD layer that maps DBRLOCK/DBRUNLOCK to ADS SQL Server lock management), or to a full rewrite of the data access layer using Harbour’s ODBC or MySQL/PostgreSQL native connectivity. Work log: “Harbour 3.2 DBFNTX migration assessment; 15 DBF files (INVENTRY.DBF 847,000 records; CUSTOMER.DBF 124,000 records; ORDERS.DBF 2.1M records; 12 smaller reference tables); ADS RDD migration scoped: Advantage Database Server 12.0 on Windows Server 2022; Harbour ADS RDD library confirmed compatible with current Harbour 3.2; DBRLOCK/DBRUNLOCK retained in existing PRG source; migration plan: ADS schema import from DBF structure, data migration via ADS import utility, Harbour rebuild with ADS RDD, regression test of all POS transactions; estimated 12h migration + 4h regression testing; migration plan approved; 3h assessment.”

Track Harbour developer retainer hours without the status emails

When a 2.5-hour investigation traces 8–12 “Record is locked by another user” errors per POS shift to a ProcessSale procedure in INVENTORY.PRG that calls DBRLOCK() on INVENTRY.DBF record 1247 and then returns via RETURN .F. on the insufficient-stock failure branch without calling DBRUNLOCK() — leaving the Harbour DBF record lock held for the workstation session lifetime and blocking all other POS terminals from locking the same record — the work log must name the procedure, the DBF file, the record number, the failure path that lacked DBRUNLOCK(), the lock diagnostic evidence, and the lock error rate before and after the fix. HourTab gives your Harbour retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the missing DBRUNLOCK() on the failure branch. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks Harbour developer retainer hours

Harbour DBF record lock hold bugs caused by DBRUNLOCK() missing on RETURN failure paths within a DBRLOCK() scope are invisible in single-workstation development testing by the same mechanism that makes all multi-user file-lock bugs invisible when only one process accesses the shared file at a time. The developer who writes ProcessSale tests the insufficient-stock failure path on a development workstation: they call the procedure with a test SKU that has zero inventory, confirm that the insufficient-stock dialog appears and the procedure returns .F., and verify that the sales form handles the failure return correctly. The workstation’s Harbour session holds the INVENTRY.DBF record lock from DBRLOCK() after the test, but since no other workstation is running the POS application during the development test, no other session calls DBRLOCK() on the same record, and no lock contention occurs. The DBRLOCKLIST() array on the development workstation would show the locked record number if queried, but the developer never queries it — there is no visible symptom to prompt a diagnostic query. In production with eight POS workstations sharing INVENTRY.DBF over the file share, the collision is visible only when two cashiers attempt to process sales for the same SKU within the lock hold window: one cashier holding the lock after a stock-out failure, and another cashier attempting DBRLOCK() on the same record for a concurrent sale.

The work log entry that makes Harbour lock work auditable must name every element of the causal chain: the Harbour version and RDD driver, the PRG file and procedure name, the DBF file name and network share path, the record number involved, the DBRLOCK() call site, the validation failure path that lacked DBRUNLOCK(), the lock diagnostic evidence (DBRLOCKLIST() output, net files output on the file server), the blocked workstations, the lock contention frequency before and after the fix, and the audit result for additional unprotected RETURN paths within the same procedure. A log that says “fixed Harbour locking issue, 2.5h” is not auditable to a retail operations manager reviewing a POS system incident. A log that names the procedure, the DBF file, the record number, the specific RETURN statement that lacked DBRUNLOCK(), and the before/after error rate is auditable, defensible, and builds the trust that sustains a long-term Harbour retainer relationship with a small business that depends on their POS system for daily operations. HourTab gives Harbour developers a public retainer-hours URL they send to clients — retailers, distributors, service businesses, and small manufacturers that run core POS, inventory, and order management operations on Harbour and xHarbour applications built on Clipper-era code and maintained by a Harbour specialist on monthly retainer.

The broader context for Harbour retainer billing is that the DBRLOCK() without DBRUNLOCK() pattern is the Harbour instantiation of the universal principle that every platform requiring explicit lock management demands that the developer pair every lock acquisition with a lock release on every exit path. Clarion developer retainers cover the equivalent in Clarion: ACCESS:File.TryLock(Record) acquiring a TPS record lock that is held at the file driver instance level, not released at code block boundaries, with Release() missing on the validation failure branch — same invisible single-workstation test, same multi-user network share lock contention in production. Progress OpenEdge ABL developer retainers cover the equivalent in Progress: FIND FIRST EXCLUSIVE-LOCK acquiring a record lock that persists until explicit RELEASE or transaction end, with RELEASE missing on the failure path — same class of invisible explicit-lock management bug. DataFlex developer retainers cover the equivalent in DataFlex: an implicit lock acquired by FIND in a mode that creates a write lock on the record, not released on the failure path — same pattern, different platform.

FAQ: Harbour developer retainers

What does a Harbour developer on retainer typically do?

A Harbour developer on monthly retainer covers DBF record lock audits (reviewing all procedures containing DBRLOCK() to confirm that every RETURN and every failure path within the lock scope also calls DBRUNLOCK() or DBUNLOCK()); multi-user DBF contention diagnosis (using DBRLOCKLIST() and Windows Server net files to identify holding workstations and locked record numbers); Harbour application compilation and maintenance (recompiling Clipper 5.2/5.3 legacy code for 64-bit Windows using Harbour 3.2 + MinGW64, resolving deprecated syntax warnings, testing multi-user locking behavior over SMB); Harbour GUI framework work (MiniGUI, HWGui, Qt5xHb); and DBF-to-SQL backend migration scoping and implementation (ADS RDD, MariaDB/PostgreSQL via ODBC, or full SQL rewrite of the data access layer).

What Harbour DBF lock debugging work is most commonly underlogged?

DBF record lock hold bugs caused by DBRUNLOCK() missing on RETURN failure paths within a DBRLOCK() scope are the most systematically underlogged Harbour retainer work. The pattern: a procedure calls DBRLOCK() for a sale, inventory adjustment, or account update; developer tests with single workstation in development; production validation failure causes RETURN without DBRUNLOCK(); DBF record lock held for session lifetime; other POS terminals receive “Record is locked by another user” at peak trading hours. The work log must name the procedure, the DBF file, the record number, the RETURN path lacking DBRUNLOCK(), and the lock error rate before and after the fix.

What are typical Harbour developer retainer rates?

Entry-level Harbour developers with experience in basic xBase syntax, DBF/NTX file maintenance, and Windows file share configuration typically bill at $45 to $75 per hour. Mid-level Harbour developers with experience in Harbour’s multi-user locking API (DBRLOCK, DBRUNLOCK, DBRLOCKLIST, DBFLOCK, DBUNLOCK), Harbour GUI frameworks (MiniGUI, HWGui), and ODBC/ADS RDD integration typically bill at $65 to $110 per hour. Senior Harbour developers with deep knowledge of Harbour’s RDD architecture, Clipper 5.2/5.3 vs. Harbour compatibility differences, xHarbour fork differences, and DBF-to-SQL backend migration architecture typically bill at $90 to $160 per hour. Monthly retainer ranges: $1,500 to $2,800/month for advisory and DBF lock audits; $2,500 to $5,000/month for active Harbour maintenance and POS system support.

What should a Harbour developer retainer agreement include?

A Harbour developer retainer agreement should specify: Harbour version (3.2 vs. xHarbour — locking semantics differ on TCP/IP SMB shares); DBF file driver (DBFNTX, DBFCDX, DBFNSX — different lock table formats); network file share configuration (Windows Server SMB, Samba, or local); whether the retainer developer has access to all PRG source files containing DBRLOCK() for the lock audit; whether the retainer covers a full DBF record lock audit (reviewing all DBRLOCK() scopes for DBRUNLOCK() on every RETURN path); whether the retainer covers DBF-to-SQL backend migration scoping; and the operating environment (standalone executable per workstation vs. Harbour application server).

How should Harbour developer retainer hours be logged?

Log each Harbour retainer session with Harbour version, PRG file, procedure name, DBF file, specific issue, and before/after metric. For DBF record lock not released on RETURN: Harbour version (3.2.0 / MinGW64), PRG file (INVENTORY.PRG), procedure (ProcessSale), DBF file (INVENTRY.DBF on \\\\FILESERVER\\POSDB), record number (1247, SKU WH-2500-BLK), DBRLOCK() call site (line 44), failure path lacking DBRUNLOCK() (inventory quantity check at line 52: IF nQtyRequested > nQtyOnHand; ALERT('Insufficient stock'); RETURN .F.; ENDIF without DBRUNLOCK()), lock diagnostic (DBRLOCKLIST() on workstation 3 = {1247}; net files on FILESERVER: INVENTRY.DBF, user WORKSTATION3\CASHIER, locks 1), blocked workstations (5 and 7), lock errors per shift before fix (8–12), fix (DBRUNLOCK() added before RETURN .F. at line 52; 2 additional RETURN paths within DBRLOCK() scope fixed at lines 67 and 81), lock errors per shift after fix (0), hours (2.5h). For Harbour compilation and migration: version, PRG files changed, deprecated syntax resolved, backend migration scope, hours.