Blog › ICP guides

DataFlex developer on retainer: FIND record lock not released, Visual DataFlex, DataFlex WebApp Framework on monthly retainer

October 8, 2026 · ~15 min read

A DataFlex developer was maintaining a legacy customer order management application written in DataFlex 19 for a regional wholesale distributor. The application managed customer account records and order history. A batch credit verification routine ran each morning to identify customers whose outstanding balance exceeded their credit limit: for each customer record, the routine read the customer’s balance and credit limit, and if the balance exceeded the limit, updated a credit-hold status field on the customer record to mark it as “ON HOLD”. The developer ran the credit verification routine successfully and confirmed it logged the correct count of customers placed on hold. But within minutes of the batch completing, four customer service representatives called to report that they could not update customer records for specific accounts — the application showed no error message, but their save operations simply did not complete, and eventually a lock timeout message appeared. The locked accounts corresponded to customers whose balance was exactly equal to (not exceeding) their credit limit — a boundary condition that the business rule check was intended to pass without holding.

The root cause was a missing UNLOCK call in the “skip” branch of the credit verification loop. In DataFlex (versions 17, 18, and 19), the FIND command (and its variants FIND_EQ, FIND_GT, FIND_LT, FIND_GE, FIND_LE) opens a record and acquires an implicit exclusive record lock. The lock is held until one of three things happens: UPDATE writes the record buffer to the database and releases the lock; RELEASE closes the record and releases the lock; or UNLOCK releases the lock while keeping the record data in the DataFlex buffer (so subsequent field-level reads are still valid but the lock is no longer held). In the credit verification routine, the developer used FIND_EQ to open each customer record, read the balance and credit limit fields with GET_FIELD, and then conditionally UPDATE the record if the balance exceeded the credit limit. For customers where the balance was below the credit limit (the “pass” branch), the routine immediately continued to the next record — but there was no UNLOCK or RELEASE before the continue. Four customers were in the “exactly equal” boundary category that the business rule treated as “pass” — their FIND_EQ locks were held for the full duration of the batch run and into the working day. Customer service representatives trying to FIND_EQ those customer records encountered the held exclusive locks and could not open the accounts. Concurrent lock hangs: 4 → 0 after adding UNLOCK at the top of the skip/pass branch, before continuing to the next record.

The DataFlex record-locking model is implicitly transactional at the record level: every FIND that opens a record for potential modification immediately acquires an exclusive lock, regardless of whether the program ultimately decides to UPDATE or not. This design reflects DataFlex’s origins as an embedded single-file database engine (the DataFlex Embedded Database, also called DFDB) where record-level locking was managed entirely within the DataFlex runtime on the local file system. Unlike a client-server relational database where locks are typically acquired when a transaction begins and released at COMMIT or ROLLBACK, DataFlex acquires the lock at the FIND call itself. The developer’s pattern — FIND, read fields, conditional UPDATE — is correct for the update path but requires an explicit UNLOCK (or RELEASE) on every non-update path. The distinction between UNLOCK and RELEASE matters in DataFlex: RELEASE closes the record entirely, discarding the buffered field values; UNLOCK releases the lock while keeping the record data in the DataFlex buffer so that the program can still read the fields it loaded (useful if the program needs to log or display the field values after deciding not to update). In this case, either UNLOCK or RELEASE before the continue would have fixed the bug; the developer chose UNLOCK to preserve the ability to log the customer’s balance and credit limit values for audit purposes even on the “pass” branch.

DataFlex also distinguishes between its embedded database mode (DFDB — DataFlex’s built-in single-user or multi-user file-based storage engine, accessed via the DataFlex DAW runtime, using .dat and .tag files for data and index storage) and its SQL connectivity layer (DataFlex SQL Client, which connects to SQL Server, Oracle, PostgreSQL, or MySQL via ODBC or native drivers, using the same DataFlex language constructs like FIND / UPDATE / RELEASE but translating them to SQL SELECT / UPDATE / COMMIT behind the scenes). In DFDB mode, the exclusive lock acquired by FIND is a file-system-level lock on the specific record offset in the .dat file; in SQL mode, the lock is a row-level database lock acquired via a SELECT ... FOR UPDATE (or equivalent) at the SQL layer. In either mode, the pattern is the same: a FIND that is not followed by UPDATE or RELEASE (or UNLOCK) leaves a lock held. The fix — UNLOCK before every non-update code path — is equally correct in both DFDB and SQL mode.

DataFlex, the embedded database model, and record-level locking

Data Access Corporation was founded in 1981 in Melbourne, Florida. The original DataFlex 1.x ran on DOS with a procedural command-based language and a proprietary file-based database engine designed for embedded single-user and small multi-user deployments. The language was command-oriented: operations like FIND, UPDATE, RELEASE, GET_FIELD, and SET_FIELD were language keywords that operated directly on table records, not SQL statements. Subsequent versions added Windows GUI support; Visual DataFlex (VDF) was introduced in the late 1990s with Windows forms, object-oriented programming syntax (objects, events, inheritance), and a visual development IDE — a significant architectural shift from the procedural command model toward an OOP component model while retaining backward compatibility with the core record-handling commands. The DataFlex WebApp Framework (DAW) was introduced in the 2000s for web-based DataFlex applications running in a browser with a server-side DataFlex application server; the same DataFlex language constructs and the same record-locking rules apply server-side in the WebApp Framework. DataFlex has continued to release major annual versions: DataFlex 17 (2017), 18 (2018), 19 (2019), 20 (2020), 21 (2021), and continuing to the present. Data Access Corporation remains the developer and publisher.

The DataFlex language constructs that appear throughout application code: FIND (position to a record on the current index); FIND_EQ table_name key_value (find the record whose index key exactly equals the given value); FIND_GT table_name key_value (find the first record whose index key is greater than the given value); FIND_LT table_name key_value (less than); FIND_GE table_name key_value (greater than or equal); FIND_LE table_name key_value (less than or equal); FIND_FIRST table_name and FIND_LAST table_name (position to the first or last record in index order); FIND_NEXT table_name and FIND_PREV table_name (sequential navigation to the next or previous record in index order). Every one of these FIND variants acquires an implicit exclusive record lock when it positions to a record. GET_FIELD table_name.field_name TO variable reads a field value from the current record buffer into a DataFlex variable; it does not touch the database directly — it reads from the in-memory buffer that was populated by the preceding FIND. SET_FIELD table_name.field_name TO value writes a value into the record buffer without committing it to the database — the change is staged in the buffer and written when UPDATE is called. UPDATE writes the entire record buffer to the database and releases the exclusive lock. RELEASE discards the buffer and releases the lock without writing. UNLOCK releases the lock while leaving the buffer intact. SAVE is equivalent to UPDATE in some DataFlex dialects and contexts.

The DataFlex field type system uses fixed-width field types stored in the .dat file: ASCII (fixed-length character fields), numeric (fixed-precision decimal, stored as ASCII-encoded digits), date (stored as a numeric date serial), and time fields. The DataFlex index model uses .tag files containing B-tree indexes; each DataFlex table has a .dat data file and a .tag index file. Indexes are defined in the DataFlex data dictionary and maintained automatically on UPDATE, SAVE, and DELETE. The DataFlex data dictionary (the .DD file, or the IDE’s Data Dictionary editor) defines tables, fields, indexes, and relations between tables; it is the central schema definition that both the application code and the DataFlex runtime consult. In DFDB multi-user mode, the DataFlex Lock Manager service on Windows or Linux coordinates record locks across multiple DataFlex client processes: the Lock Manager is a background service that must be running for multi-user DFDB applications to function correctly; a stopped Lock Manager causes all FIND lock acquisitions to fail, preventing any record from being opened for modification. In Visual DataFlex Windows forms applications, DDOs (Data Dictionary Objects) are data-aware UI controls that automatically call FIND, buffer field values, call UPDATE, and call RELEASE as the user navigates through records in the UI. Custom batch routines that manually call FIND / UPDATE / RELEASE bypass the DDO automation entirely and must manage all lock acquisitions and releases explicitly.

In DataFlex WebApp Framework applications, the same server-side record-locking rules apply. A web-based DataFlex application runs a DataFlex application server process that handles incoming browser requests; when a request handler opens a FIND, the DFDB file-system lock or the SQL-layer row lock is acquired on the application server. If the request handler returns without calling UNLOCK, RELEASE, or UPDATE, the lock is held for the remainder of the application server session — potentially minutes or longer, depending on the session timeout configuration — during which time no other DataFlex process can open that record for modification. Web-based DataFlex applications are therefore subject to the same FIND lock-not-released failure mode as batch routines, but the session lifetime in a web context may be longer than a batch run, making the lock-hold duration potentially larger. The fix is the same: every code path in a web request handler that calls FIND and does not call UPDATE must call UNLOCK or RELEASE before the handler returns. In DataFlex SQL Client mode, the underlying SQL-layer lock (typically a SELECT ... FOR UPDATE in the database engine) is what gets held; the DataFlex-level UNLOCK or RELEASE causes the SQL Client to issue the appropriate SQL to release the row lock.

Typical DataFlex retainer work and what it looks like in a work log

FIND lock-not-released in conditional branches is the canonical DataFlex invisible production lock. The pattern is consistent: a batch routine uses FIND_EQ (or another FIND variant) to open records for potential modification, reads field values with GET_FIELD, and then applies a business-rule condition to decide whether to call UPDATE. In development testing, the developer tests with a dataset where most or all records satisfy the update condition — every branch calls UPDATE and releases the lock, and the batch routine completes without any visible problem. In production, a subset of records hits the non-update branch (the “pass”, “skip”, or “already processed” branch). For each such record, FIND_EQ has already acquired the exclusive lock, and the code continues to the next iteration without calling UNLOCK or RELEASE. Those locks accumulate and are held for the full duration of the batch run. Any interactive user who tries to open one of those records — to edit a customer account, approve an order, or view a status field — encounters the held exclusive lock and either blocks until the batch completes or receives a lock timeout message. The batch routine itself shows no error; it completed successfully. DataFlex never warns that FIND acquired a lock that was not subsequently released. The developer must know the rule independently: every FIND that does not lead to UPDATE must be followed by UNLOCK or RELEASE on every code path, including all conditional branches, early-continue branches, and error branches. Work log: “credit_verify.sl: FIND_EQ CUSTOMER: lock held through balance ≤ credit_limit branch; 4 customers locked for full batch duration; customer service UPDATE blocked on those 4; UNLOCK added before continue branch; concurrent hangs: 4 → 0; 1.5h.”

DataFlex index corruption after DFDB Lock Manager restart is the second most common DataFlex retainer pattern. When the DataFlex Lock Manager service is restarted while DataFlex client processes are active — for example, during a routine server maintenance window or after a service crash — the in-flight lock table maintained by the Lock Manager is cleared. Any DataFlex client that was mid-UPDATE at the moment of the Lock Manager restart (record buffer written to the .dat file, but the B-tree update to the .tag index file not yet complete) may leave the .tag index file in a partially-written state. The B-tree structure for one or more indexes has been partially updated: some leaf nodes have been written with new record offset entries, but the root or intermediate nodes still point to the pre-update structure. Subsequent FIND_EQ calls on the corrupted index traverse the stale B-tree path and return wrong record offsets, causing GET_FIELD to read field values from the wrong record — a record with the wrong customer number, wrong order ID, or wrong account — with no error raised. The symptom is FIND_EQ ‘ACME CORP’ returning a record with the name “BETA SUPPLIES” or returning a “record not found” indication for a customer that visibly exists in the data. The fix is always REBUILD-INDEXES (the DataFlex command that rebuilds all index tags from the .dat data file by re-scanning every record and constructing fresh B-tree structures in the .tag file). Work log: “CUSTOMER.TAG: B-tree node inconsistent after Lock Manager restart during UPDATE; FIND_EQ ‘ACME CORP’ returned wrong record number; REBUILD-INDEXES rebuilt all tags from CUSTOMER.DAT; FIND_EQ now returns correct record; Lock Manager restart procedure documented (drain active sessions first); 2h.”

SET_FIELD without UPDATE on multi-record batch is the third common DataFlex retainer pattern. A developer writes a batch routine that processes a set of related records inside a loop: for each pass, the routine calls FIND_EQ on the first record, calls SET_FIELD to stage a change, then calls FIND_EQ on a second related record to read a value, then calls SET_FIELD to stage a change on a third record, and so on — accumulating all the staged changes before calling UPDATE once at the end of the loop body. The developer’s intention is to batch up the field changes and commit them together. But the DataFlex record buffer holds only one record at a time. When the second FIND_EQ positions the buffer to a new record, the buffer is implicitly replaced by the new record’s data. Any SET_FIELD changes that were staged in the buffer for the first record are discarded — overwritten by the new record’s data — without being written to the .dat file. When UPDATE is eventually called, it writes the current buffer (the last record positioned by FIND_EQ) but the earlier SET_FIELD changes are gone. Six records in the first-record set received no update because the second FIND_EQ inside the loop discarded their staged field changes before UPDATE was called. No DataFlex error is raised; the batch routine completed successfully from the runtime’s perspective. Work log: “ORDER.BATCH.SL: SET_FIELD ORDER_LINE.qty applied to 6 line records inside FIND_EQ / SET_FIELD / FIND_EQ loop; second FIND_EQ discarded first SET_FIELD; 6 line quantity fields not updated; explicit UPDATE added after each SET_FIELD / before next FIND_EQ; missed updates: 6 → 0; 2h.”

Track DataFlex developer retainer hours without the status emails

When a 1.5-hour investigation traces 4 concurrent customer-service lock hangs to a missing UNLOCK before the credit-verification loop’s pass branch — DataFlex’s FIND acquires an implicit exclusive record lock that persists until UPDATE, RELEASE, or UNLOCK is called — the work log must name the routine, the FIND variant used, the lock-hold branch, the number of records affected, and the concurrent-hang count before and after. HourTab gives your DataFlex retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the FIND lock opcode and the fix. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks DataFlex retainer hours

DataFlex FIND lock-not-released bugs are invisible by the same mechanism that makes them dangerous: the FIND command executes without raising any error or warning when it acquires a lock; the batch routine runs to completion and logs a success message; the developer sees the correct count of records processed and the correct count of records updated. Nothing in the DataFlex runtime output indicates that some records were opened with FIND_EQ, their locks were acquired, and those locks were never released because the business-rule condition sent the code down a branch that had no UNLOCK or RELEASE. The symptom only appears when a second process — an interactive user in a customer service workstation, a concurrent batch job running minutes later, a web request handler in the DataFlex WebApp Framework — tries to FIND_EQ the same record and encounters the held exclusive lock. At that point, the first process (the batch job that held the lock) may have finished long ago. The customer service representative sees a lock timeout; the batch job’s log shows no problem. The developer must connect two temporally separated events — a batch job that ran in the morning and a set of lock timeout errors that appeared when customer service staff arrived — and recognize that the batch job’s non-update code path is the cause. In a batch job with a large dataset, the lock-hold branch may affect only boundary-condition or edge-case records that are not touched by interactive users until much later in the working day, making the causal connection even less obvious.

The work log needs to name the mechanism: which .sl routine (DataFlex source files use the .sl extension for source language files), the FIND variant used (FIND_EQ vs FIND_GT vs FIND_NEXT — each one acquires a lock, and the specific variant matters for understanding which record positions the buffer), the business-rule branch that bypassed UPDATE / UNLOCK / RELEASE (the balance ≤ credit_limit pass branch, not the update branch), the number of records that hit the non-update branch and were left locked (4 customers in the exactly-equal boundary category), the concurrent-hang count before and after the fix (4 → 0), whether UNLOCK or RELEASE was used and the reason for the choice (UNLOCK to preserve the buffer for audit logging vs RELEASE to discard it entirely), and the hours (1.5h). A log entry that says “fixed lock issue in credit batch, 1.5h” is not auditable. A log entry that names the FIND_EQ CUSTOMER opcode, the balance ≤ credit_limit branch, the 4 locked records, and the UNLOCK fix is auditable and defensible. HourTab gives DataFlex developers a public retainer-hours URL they send to clients — regional wholesale distributors, manufacturing companies, and logistics firms that built DataFlex applications in the 1990s and 2000s for order management, inventory control, and customer account tracking, maintained today on DataFlex 17–21 or Visual DataFlex with SQL Client connectivity.

Comparative context: DataFlex FIND lock-not-released bugs have structural parallels with implicit lock-hold patterns in other legacy platforms. Progress OpenEdge retainers cover the implicit transaction model where FOR EACH without DO TRANSACTION holds an implicit share lock on every record read — a similar pattern where the developer intends a read-only scan but the language acquires locks silently, and a non-update branch leaves those locks held. IBM i RPG retainers cover batch update invisible bugs in RPG programs where record locks held during CHAIN and READ operations are not explicitly released on all code paths. Informix IDS retainers cover isolation level invisible bugs where the default repeatable-read isolation level holds share locks on every row read within a transaction, accumulating locks across a long-running batch query that was not designed with lock escalation in mind.

FAQ: DataFlex developer retainers

What does a DataFlex developer on retainer typically do?

A DataFlex developer on monthly retainer covers FIND lock-not-released audits for all batch routines (reviewing every FIND, FIND_EQ, FIND_GT, FIND_LT, FIND_GE, FIND_LE, FIND_NEXT, and FIND_PREV call in the application codebase to confirm that every non-UPDATE code path calls UNLOCK or RELEASE); DataFlex index integrity checks after Lock Manager restarts (verifying that .tag B-tree index files are consistent with .dat data files after the DataFlex Lock Manager service is restarted while client processes were active; running REBUILD-INDEXES to restore consistency); Visual DataFlex DDO vs manual FIND coordination (ensuring that custom batch routines that manually call FIND / UPDATE / RELEASE do not conflict with DDO-managed data-aware UI controls); DataFlex SQL Client connectivity verification (confirming correct FIND / UPDATE / RELEASE patterns in both DFDB and SQL mode against SQL Server, Oracle, PostgreSQL, or MySQL); and DataFlex WebApp Framework server-side session lock management (ensuring that web request handlers that open a FIND always call UNLOCK or RELEASE before the response completes).

What DataFlex FIND lock work is most commonly underlogged?

Batch routines that use FIND_EQ (or another FIND variant) to open records for conditional UPDATE but omit UNLOCK or RELEASE on the non-update branches are the most systematically underlogged DataFlex retainer work. The pattern: developer writes a FIND_EQ / read fields / IF condition UPDATE / continue loop, tests it with a dataset where all records satisfy the condition (so every branch calls UPDATE and releases the lock), and confirms the routine runs without error. In production, a boundary-condition subset hits the non-update branch — their FIND_EQ locks are held for the full batch duration. No DataFlex error is raised; FIND acquires its lock silently. The symptom only appears when a second process tries to FIND_EQ the same record and encounters the held exclusive lock. The developer must independently know that every FIND that does not lead to UPDATE must be followed by UNLOCK or RELEASE on every possible code path.

What are typical DataFlex developer retainer rates?

Entry-level DataFlex developers with experience in basic DataFlex procedural programming, FIND / UPDATE / RELEASE record handling, and standard batch routine development typically bill at $65 to $115 per hour. Mid-level DataFlex programmers with experience in Visual DataFlex DDO architecture, DataFlex SQL Client connectivity, FIND lock management across both DFDB and SQL mode, and DataFlex WebApp Framework server-side development typically bill at $95 to $170 per hour. Senior DataFlex developers with deep knowledge of the DataFlex embedded database engine (DFDB .dat / .tag file internals), Lock Manager multi-user coordination, Visual DataFlex OOP object and event model, and DataFlex migration assessment typically bill at $140 to $255 per hour. Monthly retainer ranges: $1,600 to $3,000 per month for advisory engagements covering FIND lock audits, index integrity checks, and Lock Manager monitoring (12 to 20 hours per month); $2,200 to $4,800 per month for active maintenance including application code fixes, SQL connectivity work, and DataFlex WebApp Framework updates.

What should a DataFlex developer retainer agreement include?

A DataFlex developer retainer agreement should specify: DataFlex version (17, 18, 19, 20, 21, or current — each version has differences in WebApp Framework capabilities, SQL Client driver support, and Visual DataFlex IDE features); database mode (DFDB embedded database with .dat and .tag files vs DataFlex SQL Client connecting to SQL Server, Oracle, PostgreSQL, or MySQL — FIND lock semantics differ at the implementation level in each mode); whether Visual DataFlex Windows forms DDO automation is in use or whether the application relies on manually-coded FIND / UPDATE / RELEASE routines (DDOs automate lock management for interactive UI; manual batch routines must manage locks explicitly); Lock Manager configuration (the DataFlex Lock Manager service must be running for multi-user DFDB applications; retainer scope should include Lock Manager health monitoring and restart procedures); DataFlex WebApp Framework scope if applicable (application server configuration, session lock timeout settings, and web request handler lock management); and data dictionary scope (whether retainer includes .DD data dictionary maintenance, index definition changes, and .dat / .tag schema migrations).

How should DataFlex developer retainer hours be logged?

Log each DataFlex retainer session with the FIND variant used and the lock-management outcome. For FIND lock-not-released bugs: routine name (credit_verify.sl), FIND variant (FIND_EQ CUSTOMER), the branch that bypassed UPDATE / UNLOCK / RELEASE (balance ≤ credit_limit pass branch), the number of records locked (4 customers), the concurrent-hang count before and after fix (4 → 0), the fix (UNLOCK added at top of pass branch), whether UNLOCK or RELEASE was used and why (UNLOCK chosen to preserve buffer for audit logging), and hours (1.5h). For index corruption after Lock Manager restart: table name, .tag file name, symptom (FIND_EQ returned wrong record number), fix (REBUILD-INDEXES), Lock Manager restart procedure documented, hours. For SET_FIELD buffer-overwrite bugs: routine name, the loop structure that caused the overwrite (FIND_EQ / SET_FIELD / FIND_EQ without intervening UPDATE), the number of records that received no update, the fix (UPDATE added after each SET_FIELD before next FIND_EQ), missed-update count before and after, hours.