Every RESOLVED audit line now includes the table alias (filename stem) as a 10-byte right-padded field between the timestamp and the message. This makes it possible to identify which table a log entry refers to without cross-referencing the asked/resolved paths.
New log format:
CONN(6) ENTRY(8) SEQ(8) TIMESTAMP ALIAS(10) RESOLVED="..." ...
Example:
NMWIF0 00000004 00000005 2026-08-21 16:17:26.012 USER_CONFG RESOLVED="C:/data/USER_CONFG.dbf" JAILED ASKED="USER_CONFG" VIA=LOCAL
The alias is right-padded with spaces if shorter than 10 characters,
and truncated from the left if longer. Both server-side (openads.ini
log file) and client-side (_arc32/ace_calls.log) table opens emit
the alias.
When multiple instances share a table via remote connection, Browse() and LastRec() showed only records visible at open time. Records appended by other connections were invisible — causing "phantom records" and stale record counts.
Root cause: Three-layer caching was never invalidated by external writes:
| Layer | Location | Problem |
|---|---|---|
Client rec_count_cached |
AdsGetRecordCount |
Cached forever; only cleared on local writes |
AdsRefreshRecord |
ace_exports.cpp |
Only cleared row_valid, not rec_count_cached |
Server rec_count_ |
GetRecordCount handler |
Returned stale in-memory value |
Fix: AdsRefreshRecord now clears rec_count_cached. The server's
GetRecordCount handler now re-reads the DBF header from disk before
responding. New IDriver::refresh_record_count_from_disk() virtual method
ensures all drivers (CDX, ADT, NTX, Cached) can refresh.
Impact: 30 concurrent instances each appending 5 records → all instances now see all 150 records after repositioning (GoTop/GoBottom/Browse).
The seek_key function used a linear scan through decoded branch
pages — O(N) per seek, degenerating to O(N²) for a full traversal of
20,000+ records. Replaced with a proper B-tree descent using binary
search at each internal node and within leaf pages.
| Operation | Before | After | Speedup |
|---|---|---|---|
| Sequential seek_key | 4,149 µs | 3.2 µs | 1,294x |
| Random seek_key | 4,215 µs | 3.6 µs | 1,162x |
| Soft seek (miss) | 8,318 µs | 2.4 µs | 3,518x |
This fixes the extremely slow GOTO with active CDX order: 47 seconds for 20,000 records → under 0.05 seconds.
OpenADS can now transparently bridge to harbour's hbnetio virtual file system and RPC capabilities. This enables multi-client concurrent access to shared files on a remote hbnetio server (e.g. AWS EC2).
VFS Adapter — maps hb_vf*() operations over OpenADS ITransport:
| hbnetio | VFS Adapter | Description |
|---|---|---|
| hb_vfOpen() | VfsAdapter::open() | Open/create file |
| hb_vfClose() | VfsAdapter::close() | Close file handle |
| hb_vfLock() | VfsAdapter::lock() | Byte-range lock |
| hb_vfUnlock() | VfsAdapter::unlock() | Release lock |
| hb_vfRead() | VfsAdapter::read() | Sequential read |
| hb_vfWrite() | VfsAdapter::write() | Sequential write |
| hb_vfReadAt() | VfsAdapter::read_at() | Positioned read |
| hb_vfWriteAt() | VfsAdapter::write_at() | Positioned write |
| hb_vfSeek() | VfsAdapter::seek() | Set file position |
| hb_vfTrunc() | VfsAdapter::truncate() | Truncate file |
| hb_vfSize() | VfsAdapter::size() | Get file size |
| hb_vfEof() | VfsAdapter::eof() | Test end-of-file |
Distributed Lock Manager — implements the __isLocked() pattern:
- Automatic .lck file creation and management
- Configurable retry with backoff (mirrors hb_idleSleep(2))
- Session-scoped lock tracking with RAII guards
- Bulk release on session disconnect
RPC Bridge — maps hbnetio RPC to OpenADS session dispatch:
| hbnetio Function | RPC Bridge | Description |
|---|---|---|
| netio_FuncExec() | RpcClient::func_exec() | Call function, get result |
| netio_ProcExec() | RpcClient::proc_exec() | Fire-and-forget |
| netio_ProcExecW() | RpcClient::proc_exec_w() | Wait for completion |
| netio_ProcExists() | RpcClient::proc_exists() | Check availability |
| netio_OpenDataStream() | RpcClient::open_data_stream() | Server push |
| netio_GetData() | RpcClient::get_stream_data() | Read stream |
| netio_CloseStream() | RpcClient::close_stream() | Close channel |
Enable: cmake -DOPENADS_WITH_HBNETIO_BRIDGE=ON ..
Wire opcodes 0xF0-0xFC (no conflict with existing opcodes).
contrib/hbnetio_bridge/vfs_adapter.h/.cpp- VFS adaptercontrib/hbnetio_bridge/dist_lock_mgr.h/.cpp- Lock managercontrib/hbnetio_bridge/rpc_bridge.h/.cpp- RPC bridgecontrib/hbnetio_bridge/CMakeLists.txt- Build optioncontrib/hbnetio_bridge/README.md- Documentationdocs/en/hbnetio-bridge.md- Reference guide
Harbour PRG code can now call the oads_*() C functions directly:
| Function | Parameters | Returns |
|---|---|---|
| OADS_FCreate | (hConn, cFile, nAttr) | hFile (0=fail) |
| OADS_FOpen | (hConn, cFile, nMode) | hFile (0=fail) |
| OADS_FClose | (hFile) | lOk |
| OADS_FWrite | (hFile, cData) | nBytesWritten |
| OADS_FRead | (hFile, nLen) | cData |
| OADS_FSeek | (hFile, nOffset, nOrigin) | nPosition |
- oads_imac_test — standalone test against iMac LAN server (all 6 functions)
- abi_oads_file_funcs_test — doctest unit tests (local + remote)
- test_oads_file.prg — Harbour PRG test
Tested against iMac at 192.168.18.184:16262 — 34/36 passed.