October 25, 1985 TO: All interested dBASE and Clipper users FROM: K. E. Saffer RE: Multi-user file access It all started in 1983 while I was writing a financial application for an interior design firm in Springfield, MA. I was using dBASE II on a Columbia MPC running MP/M-68. We had 8 users on line and I had to come up with some sort of file access control methods or the users would most certainly collide with each other sooner or later. Being experienced with LAN systems I possessed a working knowledge of how they control file access. Either on disk or located somewhere in shared memory is a structure called a semaphore table. Each record within this structure has space for a data file name and in the case of record locking schemes a record number. There is also other specialized information that varies from system to system, but we are only interested in the basics. When a user application wishes to open a file in a read/write mode, it would first check the semaphore table to determine if the file and record were free to use. If so, the semaphore table was rapidly updated to show that the file was in use (locked). The changes were made to the file, any dirty buffers were flushed, and the semaphore table was again updated to show that the file was now free to be used (unlocked). Before we progress any further, some facts about dBASE data file access must be known. When you USE a data file, you are in reality opening the file in read-write mode. So long as you do not use any commands that make changes to a data file (EDIT, REINDEX, and the dreaded APPEND BLANK to name a few) the file may be closed without writing any information the the disk drive. This allows multiple users to scan and search though the file all day long without doing any damage. Also, the actual amount of data brought into memory from a file read is rarely a single record. In reality, several records are brought into memory, so say bye-bye to any hope of using record locking techniques in this system (or at least until AT comes out with a workable multi-user database manager). This leaves us with file locking as our only recourse. As the name implies, once a file is locked by an application, no other application can make changes to the file until the original application is through with it. This requires that an application store the changes to be made in local memory variables, then quickly open the file, perform any REPLACEs and close it. If you have been in the habit of allowing a user to keep a file open in USE all the time and are performing @..GETs on the data file fields directly, you will have to make changes in your programming style. I decided to implement a semaphore table on disk as a dBASE database. At first a single database was used to hold all references to locked files, but one day that file became corrupted due to two users trying to lock two different files at the same time. To correct this, I use a small database file as a semaphore table for each database file that requires controlled access. Enough of this! How do I use it ?!?!?!? OK, OK. I know this is boring, but your multi-user users are gonna love you for this. Here is a real-life example. We have a dBASE file called CUSTOMER.DBF, with it's associated index CUSTOMER.NDX. To provide control over the customer file we will create a semaphore file called CUSTLOCK.DBF. Its structure consists of one logical field called LOCKED. The CUSTLOCK data file will have only one record in it, and the LOCKED field will be initialized to .T. The program that manipulates the customer file has three separate sections in it: viewing, editing, and adding records. When the application is viewing the file, the semaphore file is ignored because the view section will never make any changes to it. When editing or adding records, the program will first check the semaphore file to make sure no one else is making changes. If the file is locked, the operator is prompted to wait until it becomes available, otherwise the file is locked and changes made. Then the program will close the customer file, update the semaphore file and proceed about it's business. The trick here is to read the data file information into local memory variables, perform editing on these, then REPLACE the data file fields with the memory variables. This gives the program the opportunity of knowing exactly when it is going to make any changes to the file. We shall maintain control. Make darn sure your program never changes a file without your permission, or you will most certainly corrupt the file and possibly lose your job. The following is an extract of a customer data file manipulation program I wrote for an advanced point-of-sale application used by a local retailer. The system is compiled using Clipper, but all Clipper-specific code has been removed or rewritten to allow operation under dBASE III. Remember that in addition to the data manipulation programs, you must provide a program that will unlock all semaphore files in case the system is accidentally interrupted (i.e. power failure, lamebrain user, etc.) The structures of the files follow: File CUSTOMER.DBF Number of records: 30 Fieldname Type Length Decimals ---------- ---- ------ -------- FIRST_NAME C 20 0 INITIAL C 2 0 LAST_NAME C 20 0 ADDRESS1 C 40 0 ADDRESS2 C 40 0 ADDRESS3 C 40 0 ZIP_CODE C 10 0 File Customer is indexed on UPPER(LAST_NAME) to CUSTOMER.NDX. File CUSTLOCK.DBF Number of records: 1 Fieldname Type Length Decimals ---------- ---- ------ -------- LOCKED L 1 0 The file manipulation program follows: * Program ------:CUSTMAIN.PRG * Author -------:Kevin E. Saffer * Date ---------:October 28, 1985 * Notes --------:Demonstrate methods for multi-users file access control * * This program has been freely placed into the public domain without * any restrictions concerning its use. * establish working environment SET BELL OFF SET COLOR TO +7/0,7/0,0 SET DELETED ON SET DELIMITER ON SET DELIMITER TO '[]' SET EXACT OFF SET HEADINGS OFF SET SAFETY OFF SET TALK OFF * Open the customer file and display heading SELECT A USE Customer.dbf INDEX Customer.ndx CLEAR @ 01,00 SAY "Customer Maintenance" @ 01,67 SAY "Date " + DTOC(DATE()) @ 02,00 SAY "========================================" + ; "========================================" * start main loop DO WHILE .T. SET COLOR TO 7/0,+7/0 * read file fields into local memvars Mfirst = First_name Minit = Initial Mlast = Last_name Maddr1 = Address1 Maddr2 = Address2 Maddr3 = Address3 Mzip = Zip_code * get the record number for use later Mrec_no = RECNO() * display memvars @ 04,00 SAY " Lastname" GET Mlast @ 04,59 SAY "Record number [" + STR(Mrec_no,5,0) + "]" @ 05,00 SAY "Firstname" GET Mfirst @ 06,00 SAY " Initial" GET Minit @ 08,00 SAY " Address" GET Maddr1 @ 09,00 SAY " " GET Maddr2 @ 10,00 SAY " " GET Maddr3 @ 11,00 SAY " Zip Code" GET Mzip CLEAR GETS * prompt operator Choice = " " SET CONFIRM OFF DO WHILE AT(Choice,"PNAEDX") = 0 Choice = " " @ 23,00 @ 23,00 SAY "P)revious, N)ext, A)dd, E)dit, D)elete or eXit?" ; GET Choice PICTURE '!' READ ENDDO DO CASE CASE Choice = "X" * exit program CLEAR QUIT CASE Choice = "P" * previous record SKIP -1 IF BOF() @ 23,00 @ 23,00 SAY "Beginning of file encountered, going to bottom..." GO BOTTOM LOOP ELSE LOOP ENDIF CASE Choice = "N" * next record SKIP IF EOF() @ 23,00 @ 23,00 SAY "End of file encountered, going to top..." GO TOP LOOP ELSE LOOP ENDIF CASE Choice = "A" * add a new record * first, initialize memvars for operator input Mlast = " " Mfirst = " " Minit = " " Maddr1 = " " Maddr2 = " " Maddr3 = " " Mzip = " " * allow operator to edit the memvars Edit_more = " " DO WHILE Edit_more <> "Y" Edit_more = " " @ 04,00 SAY " Lastname" GET Mlast @ 05,00 SAY "Firstname" GET Mfirst @ 06,00 SAY " Initial" GET Minit @ 08,00 SAY " Address" GET Maddr1 @ 09,00 SAY " " GET Maddr2 @ 10,00 SAY " " GET Maddr3 @ 11,00 SAY " Zip Code" GET Mzip @ 23,00 @ 23,00 SAY "Please enter the customer information." READ @ 23,00 @ 23,00 SAY "Is the above information correct? (Y/N)" ; GET Edit_more PICTURE '!' READ ENDDO * prompt operator to add or exit Add_it = " " @ 23,00 @ 23,00 SAY "A)dd this new record or E)xit?" GET Add_it PICTURE '!' READ IF Add_it <> "A" * go back to display routine LOOP ENDIF * lock up customer file, we are still in work area A @ 23,00 @ 23,00 SAY "Adding new record..." USE Custlock.dbf IF Locked SET COLOR TO *15/0,+7/0 @ 23,00 @ 23,00 SAY "Customer file in use, retrying..." SET COLOR TO 7/0,+7/0 ENDIF DO WHILE Locked USE USE Custlock.dbf ENDDO REPLACE Locked WITH .T. USE * the semaphore database now shows the customer file in use, perform * replacements USE Customer.dbf INDEX Customer.ndx APPEND BLANK REPLACE First_name WITH Mfirst REPLACE Last_name WITH Mlast REPLACE Initial WITH Minit REPLACE Address1 WITH Maddr1 REPLACE Address2 WITH Maddr2 REPLACE Address3 WITH Maddr3 REPLACE Zip_code WITH Mzip Mrec_no = RECNO() USE * now, unlock the customer file and get back to the record we just added USE Custlock.dbf REPLACE Locked WITH .F. USE Customer.dbf INDEX Customer.ndx GO Mrec_no LOOP CASE Choice = "E" * editing the current record is just like adding, without initializing * the memvars * allow operator to edit the memvars Edit_more = " " DO WHILE Edit_more <> "Y" Edit_more = " " @ 04,00 SAY " Lastname" GET Mlast @ 05,00 SAY "Firstname" GET Mfirst @ 06,00 SAY " Initial" GET Minit @ 08,00 SAY " Address" GET Maddr1 @ 09,00 SAY " " GET Maddr2 @ 10,00 SAY " " GET Maddr3 @ 11,00 SAY " Zip Code" GET Mzip @ 23,00 @ 23,00 SAY "Please enter any corrections." READ @ 23,00 @ 23,00 SAY "Is the above information correct? (Y/N)" ; GET Edit_more PICTURE '!' READ ENDDO * prompt operator to add or exit Add_it = " " @ 23,00 @ 23,00 SAY "A)dd these changes or E)xit?" GET Add_it PICTURE '!' READ IF Add_it <> "A" * go back to display routine LOOP ENDIF * lock up customer file, we are still in work area A @ 23,00 @ 23,00 SAY "Adding these changes..." USE Custlock.dbf IF Locked SET COLOR TO *15/0,+7/0 @ 23,00 SAY " " @ 23,00 SAY "Customer file in use, retrying..." SET COLOR TO 7/0,+7/0 ENDIF DO WHILE Locked USE USE Custlock.dbf ENDDO REPLACE Locked WITH .T. USE * the semaphore database now shows the customer file in use, perform * replacements USE Customer.dbf INDEX Customer.ndx * get back to the record we want GO Mrec_no REPLACE First_name WITH Mfirst REPLACE Last_name WITH Mlast REPLACE Initial WITH Minit REPLACE Address1 WITH Maddr1 REPLACE Address2 WITH Maddr2 REPLACE Address3 WITH Maddr3 REPLACE Zip_code WITH Mzip Mrec_no = RECNO() USE * now, unlock the customer file and get back to the record we just edited USE Custlock.dbf REPLACE Locked WITH .F. USE Customer.dbf INDEX Customer.ndx GO Mrec_no LOOP CASE Choice = "D" * delete record Kill_it = " " @ 23,00 @ 23,01 SAY "ARE YOU SURE YOU WANT THIS CUSTOMER DELETED? (Y/N)" ; GET Kill_it PICTURE '!' READ IF Kill_it = "Y" @ 23,00 @ 23,01 SAY "Deleting this customer..." Oldrec = RECNO() USE Custlock.dbf IF Locked SET COLOR TO *15/0,+7/0 @ 23,00 @ 23,01 SAY "Customer file in use, retrying..." SET COLOR TO 7/0,+7/0 ENDIF DO WHILE Locked USE USE Custlock.dbf ENDDO REPLACE Locked WITH .T. USE USE Customer.dbf INDEX Customer.ndx GO Mrec_no * check to make sure no one else has deleted this record while we were * locking up the file IF Last_name = Mlast .AND. First_name = Mfirst DELETE USE USE Custlock.dbf REPLACE Locked WITH .F. USE ENDIF USE Customer.dbf INDEX Customer.ndx GO TOP @ 23,00 CLEAR ENDIF ENDCASE ENDDO * end of file CUSTMAIN.PRG The actual program in use has search and print capabilities, a function to unlock the semaphore file, and an indexing routine. The indexing routine is important because the program does not pack the data file when a record is deleted. The operator thinks it's gone because we SET DELETED ON in the beginning of the program. Therefore, once a week the file is re-indexed to remove any deleted records. The program runs relatively slowly in interpetive dBASE, but thats what the Clipper compiler is for. The compiled application goes like lightning. The advantage to having the program take care of the file locking methods is that now the application is not "LAN specific". It can run on nearly any LAN or multi-user system, so long as the system manager give everyone read-write access to the volume where the semaphore files reside. While you are experimenting with the program, see what happens when you try to edit the file when the semaphore database shows the file is locked. Currently, I make the user wait a second or two until the file becomes available. You might give the operator a choice of whether to wait for the file, or exit. The program by itself is only a demonstration of how controlled file sharing may be accomplished and is nowhere near a complete database manipulation program. I leave it up to you to make it perfect. Thank you for your support. KES