diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index 3775d099..f4236ee0 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -201,7 +201,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | Case | Name | Priority | |---|---|---| | 4692 | SuperUser can be deleted | 00 | -| 4693 | FormFieldReference start-up race | 00 | +| 4693 | FormFieldReference start-up race | 00. **Done** (both lazily built lists, `FormFieldReferenceList` and `FormFieldKeys`, are built in a local and published when complete, fields volatile; three restarts each followed at once by the full suite: baseline every time, no "is not valid" in the log) | | 4654 | General notification delivers at most once per 12 hours (circuit breaker) | 00 | | 4694 | Standalone launcher not shutting down PostgreSQL (raven-launcher) | 1 | | 4695 | Shipping the Visual C++ runtime DLLs with the standalone install | 1 | @@ -256,7 +256,7 @@ Found during the port, out of scope. One line each. - **Case 4731 (John, priority 1, after 8.3.0):** a global notification setting "allow email to any address from the API and report scripts" for `notify/direct-smtp`; off for new installs, on for upgraded databases; when off, only a record's saved address. Follows from F3 / case 4723 - `License.DoFetchAsync` (`License.cs:629-661`) rethrows at `x == 3`, so the fifth server (Sockeye) is never tried, and it can return `null` to a caller that dereferences it - Stray `raven-client\ayanova\cypress.json`: the Cypress suite lives in `raven-test\e2e` -- **Race, affects production after every restart:** `FormFieldReference.FormFieldReferenceList` (`biz/FormFieldReference.cs:57-59`) builds a static dictionary lazily with no lock, assigning it before filling it. A concurrent first request sees it half-built and throws `FormFieldReferenceList: is not valid`. Seen twice on .NET 10, each time on the first form-field use after boot: `VendorCrud.CRUD`, then `FormCustom.FormCustomUpdate`. Fix: build the dictionary fully before publishing it (`Lazy` or a static initialiser). Pre-existing, not a port change. John's decision: its own case, fixed with the other cases going into this release, not under the port +- **Race, affects production after every restart:** `FormFieldReference.FormFieldReferenceList` (`biz/FormFieldReference.cs:57-59`) builds a static dictionary lazily with no lock, assigning it before filling it. A concurrent first request sees it half-built and throws `FormFieldReferenceList: is not valid`. Seen twice on .NET 10, each time on the first form-field use after boot: `VendorCrud.CRUD`, then `FormCustom.FormCustomUpdate`. Fix: build the dictionary fully before publishing it (`Lazy` or a static initialiser). Pre-existing, not a port change. John's decision: its own case, fixed with the other cases going into this release, not under the port. **Fixed in case 4693 (2026-10-24)** - `raven-launcher` `Program.cs:98` calls `PGProcess.StandardInput.Close()` but standard input was never redirected, so it throws `InvalidOperationException` at shutdown: the "Postgres shut down" step never runs and the launcher exits with an error when the server stops. Postgres was started detached by `pg_ctl start`, so it probably stays running. Pre-existing; candidate for its own case. **Confirmed in the Sandbox test (8.2.4 and 8.3.0):** after "Shut down server" the launcher crashes with exactly this exception and 7 `postgres` processes stay running. Every standalone upgrade then stops at Setup's "files in use" page listing PostgreSQL Server, and "automatically close the applications" fails ("Setup was unable to automatically close all applications"): the customer must end the processes or restart Windows first. One-line fix (stop PostgreSQL with `pg_ctl stop` instead of writing to its standard input). **John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0** - **Standalone install needs the Visual C++ runtime (pre-existing, same in 8.2.4; John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0):** the bundled PostgreSQL (`local-postgres\bin\pg_ctl.exe`, `postgres.exe`) needs `VCRUNTIME140.dll`, which neither the installer nor `local-postgres` provides. A Windows without it (a new PC, Windows Sandbox) shows "pg_ctl.exe - System Error: VCRUNTIME140.dll was not found" and AyaNova never starts. Confirmed fix: copying `vcruntime140.dll`, `vcruntime140_1.dll` and `msvcp140.dll` (x64, 14.44) into `local-postgres\bin` made PostgreSQL start. Ship those three in `dist\win-x64\postgres-standalone\bin` (app-local, allowed by Microsoft's redistribution terms) rather than running `vc_redist.x64.exe` (it crashed Windows Sandbox mid-install) - Standalone `config.json` (launcher template) has no `AYANOVA_JWT_SECRET`, so every boot logs a warning and generates a random one (logins are lost on restart). Pre-existing; noted from the Sandbox test logs. **John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0** diff --git a/docs/8.0/ayanova/docs/changelog.md b/docs/8.0/ayanova/docs/changelog.md index abd21736..2b04f1f2 100644 --- a/docs/8.0/ayanova/docs/changelog.md +++ b/docs/8.0/ayanova/docs/changelog.md @@ -39,6 +39,7 @@ IN PROGRESS **Fixed** +- Server: fixed occasional errors on the first requests after the server starts. - Server: searching selection lists for text that contains an apostrophe (for example a name like O'Brien) now works. - Server: hardened data table filter handling of number and role filter values. - Server: data table date filters for "greater than or equal", "less than" and "not equal" could include or exclude records on the boundary of the selected date. diff --git a/server/AyaNova/biz/FormFieldReference.cs b/server/AyaNova/biz/FormFieldReference.cs index bd4ea673..b14ff14f 100644 --- a/server/AyaNova/biz/FormFieldReference.cs +++ b/server/AyaNova/biz/FormFieldReference.cs @@ -14,8 +14,9 @@ namespace AyaNova.Biz public static class FormFieldOptionalCustomizableReference { - private static Dictionary> _formFields; - private static List _formFieldKeys = null; + //case 4693: volatile, and published only once it is fully built (see FormFieldReferenceList) + private static volatile Dictionary> _formFields; + private static volatile List _formFieldKeys = null; public static List FormFieldKeys { @@ -23,23 +24,25 @@ namespace AyaNova.Biz { if (_formFieldKeys == null) { - _formFieldKeys = new List(); + //case 4693: built in a local and published when complete, as FormFieldReferenceList + var formFieldKeys = new List(); var values = Enum.GetValues(typeof(AyaType)); foreach (AyaType t in values) { if (t.HasAttribute(typeof(CoreBizObjectAttribute))) { - _formFieldKeys.Add(t.ToString()); + formFieldKeys.Add(t.ToString()); } } //No type / not corebiz form keys: - _formFieldKeys.Add("Contact"); + formFieldKeys.Add("Contact"); //_formFieldKeys.Add(AyaType.ServiceBank.ToString()); - _formFieldKeys.Add(AyaType.QuoteStatus.ToString()); - _formFieldKeys.Add(AyaType.WorkOrderStatus.ToString()); - _formFieldKeys.Add(AyaType.WorkOrderItemStatus.ToString()); - _formFieldKeys.Add(AyaType.WorkOrderItemPriority.ToString()); - _formFieldKeys.Add(AyaType.UnitMeterReading.ToString()); + formFieldKeys.Add(AyaType.QuoteStatus.ToString()); + formFieldKeys.Add(AyaType.WorkOrderStatus.ToString()); + formFieldKeys.Add(AyaType.WorkOrderItemStatus.ToString()); + formFieldKeys.Add(AyaType.WorkOrderItemPriority.ToString()); + formFieldKeys.Add(AyaType.UnitMeterReading.ToString()); + _formFieldKeys = formFieldKeys; } return _formFieldKeys; } @@ -54,9 +57,11 @@ namespace AyaNova.Biz public static List FormFieldReferenceList(string key) { //Initialize the static list here on first retrieval + //case 4693: built in a local and published when complete: requests arriving during the build after a + //restart saw a half-filled list ("is not valid"); two building at once is harmless, the result is the same if (_formFields == null) { - _formFields = new Dictionary>(); + var formFields = new Dictionary>(); /* ***************************** WARNING: Be careful here, if a standard field is hideable and also it's DB SCHEMA is set to NON NULLABLE then the CLIENT end needs to set a default ***************************** Otherwise the hidden field can't be set and the object can't be saved EVER */ @@ -86,7 +91,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "UserCustom14", FieldKey = "UserCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "UserCustom15", FieldKey = "UserCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "UserCustom16", FieldKey = "UserCustom16", IsCustomField = true }); - _formFields.Add(AyaType.User.ToString(), l); + formFields.Add(AyaType.User.ToString(), l); } #endregion @@ -115,7 +120,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "UserCustom14", FieldKey = "UserCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "UserCustom15", FieldKey = "UserCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "UserCustom16", FieldKey = "UserCustom16", IsCustomField = true }); - _formFields.Add("Contact", l); + formFields.Add("Contact", l); } #endregion @@ -171,7 +176,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "CustomerCustom14", FieldKey = "CustomerCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "CustomerCustom15", FieldKey = "CustomerCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "CustomerCustom16", FieldKey = "CustomerCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Customer.ToString(), l); + formFields.Add(AyaType.Customer.ToString(), l); } #endregion @@ -222,7 +227,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "HeadOfficeCustom14", FieldKey = "HeadOfficeCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "HeadOfficeCustom15", FieldKey = "HeadOfficeCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "HeadOfficeCustom16", FieldKey = "HeadOfficeCustom16", IsCustomField = true }); - _formFields.Add(AyaType.HeadOffice.ToString(), l); + formFields.Add(AyaType.HeadOffice.ToString(), l); } #endregion @@ -251,7 +256,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "ContractCustom14", FieldKey = "ContractCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "ContractCustom15", FieldKey = "ContractCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "ContractCustom16", FieldKey = "ContractCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Contract.ToString(), l); + formFields.Add(AyaType.Contract.ToString(), l); } #endregion @@ -295,7 +300,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "LoanUnitCustom14", FieldKey = "LoanUnitCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "LoanUnitCustom15", FieldKey = "LoanUnitCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "LoanUnitCustom16", FieldKey = "LoanUnitCustom16", IsCustomField = true }); - _formFields.Add(AyaType.LoanUnit.ToString(), l); + formFields.Add(AyaType.LoanUnit.ToString(), l); } #endregion @@ -323,7 +328,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "MemoCustom14", FieldKey = "MemoCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "MemoCustom15", FieldKey = "MemoCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "MemoCustom16", FieldKey = "MemoCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Memo.ToString(), l); + formFields.Add(AyaType.Memo.ToString(), l); } #endregion @@ -351,7 +356,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "ReminderCustom14", FieldKey = "ReminderCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "ReminderCustom15", FieldKey = "ReminderCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "ReminderCustom16", FieldKey = "ReminderCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Reminder.ToString(), l); + formFields.Add(AyaType.Reminder.ToString(), l); } #endregion @@ -377,7 +382,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "ReviewCustom14", FieldKey = "ReviewCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "ReviewCustom15", FieldKey = "ReviewCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "ReviewCustom16", FieldKey = "ReviewCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Review.ToString(), l); + formFields.Add(AyaType.Review.ToString(), l); } #endregion @@ -415,7 +420,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "PartCustom14", FieldKey = "PartCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "PartCustom15", FieldKey = "PartCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "PartCustom16", FieldKey = "PartCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Part.ToString(), l); + formFields.Add(AyaType.Part.ToString(), l); } #endregion @@ -445,7 +450,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "PartWarehouseCustom14", FieldKey = "PartWarehouseCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "PartWarehouseCustom15", FieldKey = "PartWarehouseCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "PartWarehouseCustom16", FieldKey = "PartWarehouseCustom16", IsCustomField = true }); - _formFields.Add(AyaType.PartWarehouse.ToString(), l); + formFields.Add(AyaType.PartWarehouse.ToString(), l); } #endregion @@ -474,7 +479,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "PartAssemblyCustom14", FieldKey = "PartAssemblyCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "PartAssemblyCustom15", FieldKey = "PartAssemblyCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "PartAssemblyCustom16", FieldKey = "PartAssemblyCustom16", IsCustomField = true }); - _formFields.Add(AyaType.PartAssembly.ToString(), l); + formFields.Add(AyaType.PartAssembly.ToString(), l); } #endregion @@ -507,7 +512,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "ProjectCustom14", FieldKey = "ProjectCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "ProjectCustom15", FieldKey = "ProjectCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "ProjectCustom16", FieldKey = "ProjectCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Project.ToString(), l); + formFields.Add(AyaType.Project.ToString(), l); } #endregion @@ -555,7 +560,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "PurchaseOrderCustom14", FieldKey = "PurchaseOrderCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "PurchaseOrderCustom15", FieldKey = "PurchaseOrderCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "PurchaseOrderCustom16", FieldKey = "PurchaseOrderCustom16", IsCustomField = true }); - _formFields.Add(AyaType.PurchaseOrder.ToString(), l); + formFields.Add(AyaType.PurchaseOrder.ToString(), l); } #endregion @@ -588,7 +593,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "ServiceRateCustom14", FieldKey = "ServiceRateCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "ServiceRateCustom15", FieldKey = "ServiceRateCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "ServiceRateCustom16", FieldKey = "ServiceRateCustom16", IsCustomField = true }); - _formFields.Add(AyaType.ServiceRate.ToString(), l); + formFields.Add(AyaType.ServiceRate.ToString(), l); } #endregion @@ -619,7 +624,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "TravelRateCustom14", FieldKey = "TravelRateCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "TravelRateCustom15", FieldKey = "TravelRateCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "TravelRateCustom16", FieldKey = "TravelRateCustom16", IsCustomField = true }); - _formFields.Add(AyaType.TravelRate.ToString(), l); + formFields.Add(AyaType.TravelRate.ToString(), l); } #endregion @@ -631,7 +636,7 @@ namespace AyaNova.Biz // l.Add(new FormField { TKey = "TaskGroupNotes", FieldKey = "Notes" }); - // _formFields.Add(AyaType.TaskGroup.ToString(), l); + // formFields.Add(AyaType.TaskGroup.ToString(), l); // } // #endregion @@ -661,7 +666,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "TaxCodeCustom14", FieldKey = "TaxCodeCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "TaxCodeCustom15", FieldKey = "TaxCodeCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "TaxCodeCustom16", FieldKey = "TaxCodeCustom16", IsCustomField = true }); - _formFields.Add(AyaType.TaxCode.ToString(), l); + formFields.Add(AyaType.TaxCode.ToString(), l); } #endregion @@ -671,7 +676,7 @@ namespace AyaNova.Biz // l.Add(new FormField { TKey = "ServiceBankCurrency", FieldKey = "Currency" }); // l.Add(new FormField { TKey = "ServiceBankHours", FieldKey = "Hours" }); // l.Add(new FormField { TKey = "ServiceBankIncidents", FieldKey = "Incidents" }); - // _formFields.Add(AyaType.ServiceBank.ToString(), l); + // formFields.Add(AyaType.ServiceBank.ToString(), l); // } // #endregion @@ -727,7 +732,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "UnitCustom14", FieldKey = "UnitCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "UnitCustom15", FieldKey = "UnitCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "UnitCustom16", FieldKey = "UnitCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Unit.ToString(), l); + formFields.Add(AyaType.Unit.ToString(), l); } #endregion @@ -765,7 +770,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "UnitModelCustom14", FieldKey = "UnitModelCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "UnitModelCustom15", FieldKey = "UnitModelCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "UnitModelCustom16", FieldKey = "UnitModelCustom16", IsCustomField = true }); - _formFields.Add(AyaType.UnitModel.ToString(), l); + formFields.Add(AyaType.UnitModel.ToString(), l); } #endregion @@ -816,7 +821,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "VendorCustom14", FieldKey = "VendorCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "VendorCustom15", FieldKey = "VendorCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "VendorCustom16", FieldKey = "VendorCustom16", IsCustomField = true }); - _formFields.Add(AyaType.Vendor.ToString(), l); + formFields.Add(AyaType.Vendor.ToString(), l); } #endregion @@ -1052,7 +1057,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "LineTotal", FieldKey = "OutsideServiceLineTotalViz", TKeySection = "WorkOrderItemOutsideService", Requireable = false }); - _formFields.Add(AyaType.WorkOrder.ToString(), l); + formFields.Add(AyaType.WorkOrder.ToString(), l); } #endregion @@ -1278,7 +1283,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "TaxBAmt", FieldKey = "OutsideServiceTaxBViz", TKeySection = "WorkOrderItemOutsideService", Requireable = false }); l.Add(new FormField { TKey = "LineTotal", FieldKey = "OutsideServiceLineTotalViz", TKeySection = "WorkOrderItemOutsideService", Requireable = false }); - _formFields.Add(AyaType.Quote.ToString(), l); + formFields.Add(AyaType.Quote.ToString(), l); } #endregion quote @@ -1287,7 +1292,7 @@ namespace AyaNova.Biz { List l = new List(); l.Add(new FormField { TKey = "StatusNotes", FieldKey = "StatusNotes" }); - _formFields.Add(AyaType.QuoteStatus.ToString(), l); + formFields.Add(AyaType.QuoteStatus.ToString(), l); } #endregion @@ -1295,7 +1300,7 @@ namespace AyaNova.Biz { List l = new List(); l.Add(new FormField { TKey = "StatusNotes", FieldKey = "StatusNotes" }); - _formFields.Add(AyaType.WorkOrderStatus.ToString(), l); + formFields.Add(AyaType.WorkOrderStatus.ToString(), l); } #endregion @@ -1303,21 +1308,21 @@ namespace AyaNova.Biz { List l = new List(); l.Add(new FormField { TKey = "WorkOrderItemStatusNotes", FieldKey = "WorkOrderItemStatusNotes" }); - _formFields.Add(AyaType.WorkOrderItemStatus.ToString(), l); + formFields.Add(AyaType.WorkOrderItemStatus.ToString(), l); } #endregion #region WorkOrderItemPriority { List l = new List(); - _formFields.Add(AyaType.WorkOrderItemPriority.ToString(), l); + formFields.Add(AyaType.WorkOrderItemPriority.ToString(), l); } #endregion #region UnitmeterReading { List l = new List(); - _formFields.Add(AyaType.UnitMeterReading.ToString(), l); + formFields.Add(AyaType.UnitMeterReading.ToString(), l); } #endregion @@ -1545,7 +1550,7 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "TaxBAmt", FieldKey = "OutsideServiceTaxBViz", TKeySection = "WorkOrderItemOutsideService", Requireable = false }); l.Add(new FormField { TKey = "LineTotal", FieldKey = "OutsideServiceLineTotalViz", TKeySection = "WorkOrderItemOutsideService", Requireable = false }); - _formFields.Add(AyaType.PM.ToString(), l); + formFields.Add(AyaType.PM.ToString(), l); } #endregion quote @@ -1583,12 +1588,13 @@ namespace AyaNova.Biz l.Add(new FormField { TKey = "CustomerServiceRequestCustom14", FieldKey = "CustomerServiceRequestCustom14", IsCustomField = true }); l.Add(new FormField { TKey = "CustomerServiceRequestCustom15", FieldKey = "CustomerServiceRequestCustom15", IsCustomField = true }); l.Add(new FormField { TKey = "CustomerServiceRequestCustom16", FieldKey = "CustomerServiceRequestCustom16", IsCustomField = true }); - _formFields.Add(AyaType.CustomerServiceRequest.ToString(), l); + formFields.Add(AyaType.CustomerServiceRequest.ToString(), l); } #endregion //****************************************************** + _formFields = formFields; } if (!_formFields.ContainsKey(key))