Database Configuration¶
Older Versions¶
This section describes the runtime database configuration for FWD v4 and later, which changed substantially from older versions of FWD. For information on configuring the database for older versions, please refer to Database Configuration v3.
Search Algorithm¶
The database configuration can be located in different locations to differentiate the option as global for all servers or specific to a server. The search algorithm used for these options is the Server Search Algorithm.
The database nodes must be located in the global server location /server/default/database/<database_node>/ or in the per-server location:
/server/<server_id>/database/<database_node>/
where <server_id> is the name of the server and <database_node> is the JDBC database name.
Port Services Mappings¶
The port-services section maps a list of names to their TCP port numbers.
<node class="container" name="port_services">
<node class="integer" name="my_first_service">
<node-attribute name="value" value="3333"/>
</node>
<node class="integer" name="my_second_service">
<node-attribute name="value" value="3343"/>
</node>
</node>
This is used only by database connection setup.
Where the 4GL uses the services file to process services names to port mappings, FWD uses this data to create a logical connection to a database using the specified parameters. The 4GL statement which defines this is the CONNECT statement, and it will always result in a call to the ConnectionManager.connect(...)
The CONNECT statement uses a series of parameters among which is :
-S { port-number | service-name }
This specifies the port number or service name used to connect to a FWD server running on the specified host. If the host is specified, then it is required to specify either the port or service name. In the case the argument is not a number the directory will be searched to find the port_services node that has the specified name.
Persistence¶
The persistence node of the directory controls whether persistence (i.e., database) support is active, and whether foreign keys are in use. By default, persistence support is active and foreign key support is disabled. Since it is unusual to change either of these defaults, this node is not present in the directory at all by default.
The active parameter controls whether runtime persistence support is turned on. It would be an uncommon ABL application which does not use a database at all, but persistence support can be disabled. This may be useful, for instance, when running simple test cases which do not use a database. The following setting under the standard/runtime/ directory path disables it:
<node class="container" name="persistence">
<node class="boolean" name="active">
<node-attribute name="value" value="FALSE"/>
</node>
</node>
Note that when persistence is disabled, the FWD server will log a warning at startup.
The foreign-keys parameter controls whether the conversion will calculate and enforce referential integrity using “natural” joins. A natural join is one where two tables are linked by an exactly matching index specification which is defined as unique in at least one of the tables. The default value of foreign-keys parameter is FALSE.
This parameter is deprecated and should not be used. Traditionally, Progress code has not had any concept of referential integrity. This means that normal relational database management system (RDBMS) rules may be and probably are broken in most applications. As one example, in Progress the order of deletes between two tables is not important (it can be in any order), because Progress traditionally has not known about nor enforced any relationship between tables. When adding referential integrity where there was none in the past, such problems can cause a serious deadlocking bug. There are known issues with the current implementation of foreign keys such that many converted Progress applications would not work properly if this is enabled. Some of these problems can likely be resolved in the FWD runtime, but they do still exist at the time of this writing.
Example:
<node class="container" name="persistence">
<node class="boolean" name="foreign-keys">
<node-attribute name="value" value="FALSE"/>
</node>
</node>
Dirty-Share¶
Dirty-Share is a complex feature in FWD to replicate these two aspects in 4GL: intra-session and cross-session visibility of new/changed records described below. To be on par with 4GL behavior, FWD should technically have both "intra-session dirty-share" and "cross-session dirty-share" features enabled.
To implement this feature, FWD manages an in-memory database named "dirty-share database" that stores new/changes records that were not yet flushed or committed, but can be found by queries of the same session (intra-session) or another session (cross-session). In general, this feature kicks in for FIND statements, FOR EACH blocks, queries or other mechanisms that query the database. When such database access is triggered, FWD has the algorithms to resolute the correct records, either from dirty database or physical database.
Due to the design of the "dirty-share database", there are obvious limitations of the support for dirty-share: nested CAN-FIND is not honoring dirty-share, performance is usually affected by the need to constantly check dirty-share database for updates.
Cross-session dirty-share¶
The support for this feature in FWD is partial and is going to be dropped, don't rely on it.
The OpenEdge database has a transaction isolation problem caused by its core design being tightly coupled to index processing. When one session creates a new record and fully updates an index, this updated index is immediately visible to other sessions, even if the record is not fully initialized or it has not yet been committed (the transaction is still open). This means that other sessions can see these new records before they are committed. This is a bad practice. It doesn't fit the modern database transaction isolation policy. It has serious negative security implications. It should not be used. Yet, some 4GL applications intentionally or unintentionally rely upon this behavior to coordinate behavior across sessions. We call this cross-session dirty sharing. We have seen this behavior used in multiple applications to calculate a unique id at the time a record is created. This usage was implemented before OE supported sequences.
dirty-cross-shareflag is used to enable/disable this feature. It is FALSE by default.force-dirty-cross-shareflag it used to force all tables to be subject to cross-session dirty-share mechanism. By default, only tables hinted as "dirty-read" as subject to "cross-session dirty-share" feature if enabled overall. This is FALSE by default.- this flag is ignored if
dirty-cross-shareis FALSE
- this flag is ignored if
dirty-share-global-notificationsflag is used to allow one session to signal another that the "cross-session dirty-share" exposed a new record. With this enabled,FOR EACHqueries of other sessions will start merging results from the physical database with the results in the dirty-share database in order to pick up these exposed new records. This leads to a severe performance downgrade asFOR EACHqueries will pick records from the database one-by-one in order to be preemptive with the functionality of "cross-session dirty-share". This is FALSE by default.- this flag is ignored if
dirty-cross-shareis FALSE
- this flag is ignored if
Intra-session dirty-share¶
Most customers tend to rely on this feature and disabling it usually resulted in very early and obvious issues.
The 4GL exhibits some behavior that allows changes within a session to be visible to separate code (and buffers) elsewhere in the session. When a piece of code inserts or edits database fields that are indexed, those changes are immediately visible to upcoming queries on that fully updated index within the same session, even though some fields were not even set yet. This doesn't break transaction isolation like cross-session dirty sharing, but it allows manipulating partially updated records in-memory before flushing time and eventually WRITE trigger execution. We call this behavior intra-session dirty sharing.
dirty-intra-shareflag is used to enable/disable this feature. It is TRUE by default.force-dirty-intra-shareflag it used to force all tables to be subject to intra-session dirty-share mechanism. By default, only tables hinted as "dirty-read" as subject to "intra-session dirty-share" feature if enabled overall. This is TRUE by default.- this flag is ignored if
dirty-intra-shareis FALSE
- this flag is ignored if
Dirty-share per table¶
In order to mark a table as ready for "cross-session dirty-share", use a <db-name>.schema.hints file to hint that a certain table is "dirty-read":
<hints>
<schema>
<table name="my-table" dirty-read="true">
</table>
</schema>
</hints>
This hint will be used by the conversion to emit:
@Table(name = "myTable", legacy = "my-table", dirtyRead = true)
How to configure dirty-share¶
Dirty-share is configurable from directory.xml file:
<node class="container" name="persistence">
<node class="boolean" name="dirty-cross-share">
<node-attribute name="value" value="FALSE"/>
</node>
<node class="boolean" name="dirty-intra-share">
<node-attribute name="value" value="TRUE"/>
</node>
<node class="boolean" name="force-dirty-cross-share">
<node-attribute name="value" value="FALSE"/>
</node>
<node class="boolean" name="force-dirty-intra-share">
<node-attribute name="value" value="TRUE"/>
</node>
<node class="boolean" name="dirty-share-global-notifications">
<node-attribute name="value" value="FALSE"/>
</node>
</node>
Each option is described in the sections above.
Quick configurations:
- Not having the options at all in
directory.xmlwill lead to a good enough set-up (just like the one in the example):- the intra-session feature is enabled for all tables.
- the cross-session feature is completely disabled: this may generate mismatches with 4GL behavior
- Having all disabled will lead to better performance, yet it will completely stray away from 4GL functionality.
- disabling all these will require a thorough code investigation to remove reliance on dirty-share
- Having all enabled will lead to a full dirty-share feature overhead that is functionally the same as 4GL.
- mind that "cross-session dirty-share" is partially implemented and is going to be unsupported, so this configuration is not recommended.
It is recommended to refactor the legacy code in order to avoid relying on cross-session dirty-share functionality as it is partially supported by FWD run-time and will be dropped.
Examples of dirty-share usage¶
- Record ids that are (uniquely) assigned similarly how a sequence will do. Usually this happens in WRITE triggers and relies on cross-session dirty-share (identifying the absolute last id across all sessions, even if not committed).
- These should be refactored to use sequences.
- Mutex implementations that use a database table that is updated inside a transaction, but not committed. This relies on cross-session dirty-share to properly create / find / release mutexes without actually committing.
- These should be refactored in Direct Java Code or find another way to make the mutexes visible cross-session without relying on dirty-share.
- Such mutex work can be deferred to an appserver that has its own transaction and DB connection. This way, it can actually commit without spoiling the full transaction of the original business logic.
Batch Results size¶
Since FWD trunk revision 16488, a new feature was added: fetching the cached records in batches when the result set gets invalidated.
Example how to configure:
<node class="container" name="server">
<node class="container" name="default">
<node class="container" name="runtime">
...
<node class="integer" name="batchResultsSize">
<node-attribute name="value" value="100"/>
</node>
...
<node>
</node>
</node>
This configuration is optional, with default value set to 100.
How to adjust this parameter:- if you encounter high database latency (iterating through a table with high number of records), try using a higher batch size.
- if your
batchResultsSizevalue is too high, your memory andSessioncache will fill up, which will slow down your query performance. - if you want the performance prior revision 16488, use
batchResultsSize1.
Session Lifespan¶
This is an option that can be configured in directory.xml, it allows for any session to be reclaimed in a configurable time period after not being used.
It can be defined in the persistence container in the following way:
<node class="container" name="persistence">
<node class="integer" name="session-lifespan">
<node-attribute name="value" value="1000"/>
</node>
</node>
The attribute value is measured in milliseconds. In the example above, a session can be reclaimed only if it didn't exceed 1 second of inactivity.
Each persistence context will have aSessionFactory instance associated which will manage the create/mark/reclaim/expire process of a session:
- When a session is created, it will check for any session that can be reclaimed. It will create one if none can be reclaimed or return the existent reclaimable session.
- When a session is not used anymore and needs to be closed, it is marked as reclaimable instead.
- A session expires when the time period in which it can be claimed is exceeded. In this case, the session is closed.
All SessionFactory instances are registered to SessionFactory.SessionCloseThread which manages the closing of the sessions that exceed the time period in which they can be reclaimed.
- If no option is set, the lifespan will be set to a default value (1 second).
- If
0is used as an option, the session will go through the reclaim process indefinitely. The managing thread will not start, but theSessionFactoryinstances will still be registered and deregistered. In this way, when the context is closed, theSessionFactorywill be deregistered and any reclaimable session will be closed. - If a negative value is provided, session reclaiming will be disabled.
- If a positive value is provided, it will represent the maximum time in which the session can be reclaimed.
Database Instances¶
Each database instance which the application can use must be configured via a database container, which at a high level looks like this:
<node class="container" name="database">
<node class="container" name="my_database">
<node class="container" name="p2j">
<!-- general settings -->
...
</node>
<node class="container" name="orm">
<!-- ORM (Object-to-Relational-Mapping) settings -->
...
</node>
</node>
The name attribute in the second level container above with the value my_database corresponds with the physical database name in the legacy environment. This can be (but is not necessarily) different from the schema name and database instance name described in the sections below.
Each of the XML comments above are placeholders for nested containers with specific settings, possibly in more deeply nested containers. These settings are described in the various sections below.
P2J Section¶
The p2j container holds general purpose FWD database runtime settings:
schema(String) the name of the database's schema; typically, this is the name of the schema that was exported from the 4GL Data Dictionary. This can be, but it does not need to be, the same name as the name configured in thedatabasecontainer.load_at_startup(boolean) determines whether the database is auto-connected (in the 4GL sense, not the JDBC sense) at server startup. If not auto-connected, it is assumed the original 4GL code used a CONNECT statement when this database needed to be connected. [TODO: this setting is obsolete; document newdatabase-connectionsconfiguration]embedded-collation(String) this is used to set collation rules. The default value isen_US@iso88591_fwd_basic. See thestaticinitializer block ofFwdCollatorProviderfor other collations provided in FWD. This is used to set collation rules for H2 dialect. The other dialects are not affected.
Example:
<node class="container" name="database">
<node class="container" name="my_database">
...
<node class="container" name="p2j">
<node class="string" name="schema">
<node-attribute name="value" value="my_schema"/>
</node>
<node class="boolean" name="load_at_startup">
<node-attribute name="value" value="TRUE"/>
</node>
<node class="string" name="embedded-collation">
<node-attribute name="value" value="nl_NL@cp1252_fwd_basic"/>
</node>
</node>
...
ORM Section¶
The ORM section contains settings which allow FWD to map its persistence framework Java objects, which represent relational database constructs (e.g., tables, records, etc.), to the corresponding constructs in the backing database.
Dialect¶
The dialect container is used to specify the database dialect in use.
The levels of support for SQL standards among different database vendors vary, and each vendor may define extensions and features not specified by standards. To account for these differences in syntax, FWD implements an abstraction layer which requires the type of database dialect in use to be specified.
As of FWD version 4, the following dialects are provided:
| Database | FWD Dialect Name | Notes |
|---|---|---|
| PostgreSQL | com.goldencode.p2j.persist.dialect.P2JPostgreSQLDialect |
Recommended for production use |
| MariaDB | com.goldencode.p2j.persist.dialect.MariaDbLenientDialect |
Recommended for production use |
| SQL Server | com.goldencode.p2j.persist.dialect.SQLServerDialect |
Development was suspended a few years so the support is incomplete. We started working on this and expect to get it on par with the other, soon. |
| H2 | com.goldencode.p2j.persist.dialect.H2Dialect |
Used heavily for testing and for FWD's temporary table support; not recommended for external database production use |
<node class="container" name="database">
<node class="container" name="my_database">
<node class="container" name="orm">
...
<node class="string" name="dialect">
<node-attribute name="value" value="com.goldencode.p2j.persist.dialect.P2JPostgreSQLDialect"/>
</node>
...
Connection¶
The connection container is used to set up a JDBC connection to the permanent database as configured in the specified directory. The common path the connection directory nodes have in common is:
databaseDirNodePath + my_database + "/orm/connection/"
where databaseDirNodePath is the full database container path from the directory file and the my_database variable represents the name of the database.
The attributes of the connection container are:
driver_class: (String) the Java Driver class used to create the JDBC Connectionusername: (String) the user name used to connect to the database.password: (String) the password used to authenticate the user when connecting to the data baseurl: (String) the jdbc url address of the database to which the connection is made. Note that the database name used in this URL does not need to match the physical database name configured in thedatabasecontainer; it is the name of the relational database instance to which JDBC connections will be made. For MariaDB, it is highly recommended to adduseServerPrepStmts=trueoption in the URL in order to gain some performance improvements.prepareThreshold(optional/Integer) : The threshold number of times a statement is called for which the query will be used as a named query, saving the execution plan at the database server (PostgreSQL only).
Example:
<node class="container" name="database">
<node class="container" name="my_database">
<node class="container" name="orm">
...
<node class="container" name="connection">
<node class="string" name="driver_class">
<node-attribute name="value" value="org.postgresql.Driver"/>
</node>
<node class="string" name="username">
<node-attribute name="value" value="my_db_username"/>
</node>
<node class="string" name="password">
<node-attribute name="value" value="my_db_users_password_in_clear_text"/>
</node>
<node class="string" name="url">
<node-attribute name="value" value="jdbc:postgresql://localhost/my_database_instance"/>
</node>
<node class="integer" name="prepareThreshold">
<node-attribute name="value" value="1"/>
</node>
</node>
...
C3P0¶
The c3p0 section can be used to provide initialization data for the c3p0 database connection pool.
c3p0 is an easy-to-use library for making traditional JDBC drivers "enterprise-ready" by augmenting them with functionality defined by the jdbc3 spec and the optional extensions to jdbc2. In particular, c3p0 provides several useful services:
- Classes which adapt traditional
DriverManager-based JDBC drivers to the newjavax.sql.DataSourcescheme for acquiring database Connections. - Transparent pooling of Connection and
PreparedStatementsbehindDataSourceswhich can "wrap" around traditional drivers or arbitrary unpooledDataSources.
The attributes of the c3p0 container are:
minPoolSize(Integer) the minimum number of connections a pool will maintain at any given time.maxPoolSize(Integer) the maximum number of connections the pool will maintain at any given time. Be sure your database configuration allows at least this many connections (it is a good idea to reserve a few extra connections which the pool will not use, for database administration).aquireIncrement(Integer) the number of connections at a time c3p0 will try to acquire when the pool is exhausted.maxIdleTime(Integer) the number of seconds a pooled connection will be available before it is released. Set to 0 for connections that do not expire.maxStatementsPerConnection(Integer) the maximum numberPreparedStatementsaDataSourcewill cache per JDBC connection. The pool will destroy the least-recently-usedPreparedStatementwhen it hits this limit. Statement pooling is optional and may help or hurt performance, depending on the particular application. Remove this parameter or set it to 0 to disable statement pooling. This option SHOULD be disabled if MariaDB driver anduseServerPrepStmtsoption are used.
Please see also https://www.mchange.com/projects/c3p0/#configuration for more details on the above configuration options, which are passed through to c3p0.
Example (actual values should be determined by the needs of a particular application; those which affect performance should be determined through testing/profiling):
<node class="container" name="database">
<node class="container" name="my_database">
<node class="container" name="orm">
...
<node class="container" name="c3p0">
<node class="integer" name="minPoolSize">
<node-attribute name="value" value="20"/>
</node>
<node class="integer" name="maxPoolSize">
<node-attribute name="value" value="400"/>
</node>
<node class="integer" name="acquireIncrement">
<node-attribute name="value" value="20"/>
</node>
<node class="integer" name="maxIdleTime">
<node-attribute name="value" value="600"/>
</node>
<node class="integer" name="maxStatementsPerConnection">
<node-attribute name="value" value="100"/>
</node>
</node>
...
C3P0 and PGsql configuration for PASOE¶
Definitions & Formulas:
maxConnectionsPerAgent= number of worker threads serving requestsbatchProcesses= number of batch processes per tenant
Max pool size per tenant:
maxPoolSize = maxConnectionsPerAgent + batchProcesses
Max pool sizes for the 3 databases:- Landlord DB: fixed
maxPoolSize= 15 (fixed) - Tenant DB:
maxPoolSizeas above - Tenant_shared DB:
maxPoolSizeas above
- Landlord DB: fixed
max_connections=<landlord maxPoolSize> + N × <tenant maxPoolSize> + <tenant_shared maxPoolSize> + <superuser connections>
Where:N= number of tenantssuperuser connections= 10
With the current directory configuration:
maxConnectionsPerAgent = 16batchProcesses = 4 → maxPoolSize = 16 + 4 = 20
landlord maxPoolSize = 15
tenant_shared maxPoolSize = 20
superuser connections = 10
One tenant:max_connections = 15 + 20 + 20 + 10 = 65
| Tenants (N) | Landlord maxPoolSize | Tenant maxPoolSize | Tenant_shared maxPoolSize | Superuser conns | PostgreSQL max_connections |
|---|---|---|---|---|---|
| 1 | 15 | 20 | 20 | 10 | 15 + 1*20 + 20 + 10 = 65 |
| 10 | 15 | 20 | 20 | 10 | 15 + 10*20 + 20 + 10 = 245 |
| 20 | 15 | 20 | 20 | 10 | 15 + 20*20 + 20 + 10 = 445 |
| 30 | 15 | 20 | 20 | 10 | 15 + 30*20 + 20 + 10 = 645 |
Tenants can be added dynamically, but max_connections per pgsql cluster cannot be changed on the fly, max_connections needs to be scaled accordingly to the anticipated number of tenants.
Strategies for Allocating JDBC Connections¶
We saw earlier that the values specified in directory.xml at database/<db-name>/orm represent the values for the shared database and, at the same time, the default values for all tenants of that database if not explicitly configured in landlord database via REST requests.
Probably the most important parameter for the connection pool is the maxPoolSize which sets the upper limit for the number of connection to a specific physical database.
In previous section a table with simple arithmetic is presented for a single cluster setup. However, as shown below, the setup can be extended to multiple clusters, as well.
- From known necessities
Let's assume the customer has a good experience with the application and is aware of the number of connection needed. Let's considermaxPoolSize = 20(as in cited document). We compute the minimal value for the value ofmax_connectionsstored in cluster'spostgresql.conffile as follows:max_connections >= 10 (superuser) + 15 (landlord) + 20 x (N + 1) (shared and tenant physical databases)
Pros and cons of this strategy:+easy to configure when the application requirements are known and the number of tenants is also known from beginning;-adding a tenant requiresmax_connectionsto be recomputed and PSQL to be restarted;*alternatively, when adding a new tenant, it can be stored in a new cluster.
- Hard limits
In casemax_connectionsis fixed and cannot be changed, the maximum number of connections (maxPoolSize) will have to be computed using the following formula:maxPoolSize = floor((max_connections - 10 (superuser) - 15 (landlord)) / (N + 1) (shared and tenant physical databases))
Pros and cons of this strategy:+useful if setting up the server in strict conditions;-ifNis large,maxPoolSizemay become too small to be useful*in that case, some of the tenant physical databases can be moved to another cluster
- Dynamic allocation of tenants on multiple clusters
From the bullets above marked with*, we can encounter cases when using a single cluster is not feasible. Using additional clusters (each with its ownmax_connectionspossible connections) allow the system to be naturally expanded. Physical databases can be stored to other clusters than the one which hosts the shared database. If not specified otherwise, the data pools for these databases will inherit themaxPoolSizeof the shared database. When computing themax_connectionsfor a new cluster which will hold an additionalMtenants we use:max_connections2 >= 10 (superuser) + maxPoolSize x M (tenant physical databases)
Notice that, even if the superuser connections are mandatory, the reservation of 15 connections forlandlordandmaxPoolSizefor shared database are not accounted. At this moment, the FWD already hasN + Mtenants. This operation can be extended to any number of clusters, not mandatory on same machine. - Fine-tuning connections
As noted initially, all the above examples use themaxPoolSizeset indatabase/<db-name>/orm. None of the tenants were configured with a differentmaxPoolSizewhen they were created / modified using tenant REST admin (or directly inlandlorddatabase, before re-starting the FWD server).
ThemaxPoolSize(but also the other C3P0 parameters) can be individually configured for each tenant database. Let's assume we want to create a new private database which is expected to require a larger number of connections than declared indirectory.xmlfor the shared database. We use the following request:
When running, the data source forPOST https://localhost:7443/admin/tenants/fwd_ten404/db { "physicalName": "fwd_ten404", "logicalName": "fwd", "url": "jdbc:postgresql://localhost:5430/fwd_ten404", "username": "cool-name", "password": "*****", "minPoolSize": 2, "maxPoolSize": 42 }fwd_ten404database will allocate a minimum of 2 connections but will be able to grow up to 42, even if the directory settings forfwddatabase specified 10 and 20 respectively. Also, this seems to be a different cluster (using port 4530) than the default used by shared/logical database.
In this general case, computingmax_connectionsbecomes a bit more complex. Formally:max_connections >= 10 (superuser) + 15 (landlord) + maxPoolSize (shared) + Σ(i in 1 .. N of maxPoolSize_i)
for the primary cluster (the one storing the shared andlandlorddatabase), andmax_connections >= 10 (superuser) + Σ(i in 1 .. M of maxPoolSize_i)
for secondary clusters. We have:/ maxPoolSize of shared database, if the value was not specified individually
maxPoolSize_i = <
\ specific maxPoolSize, if specified in REST request
Particular case: if each tenant has its own database cluster, then the formulae above get simplified (N = 0 (assuming no tenant is stored with the shared database), M = 1, etc), as follows:
max_connections >= 10 (superuser) + 15 (landlord) + maxPoolSize (shared)for main cluster, and
max_connections_k >= 10 (superuser) + maxPoolSize_kfor each secondary cluster holding the private database of tenant
k. If the maxPoolSize is not specified at creation of later set, the default size from the shared database is inherited. Otherwise, maxPoolSize for tenant_k can be set in the REST request as:maxPoolSize_k = max_connections_k - 10 (superuser)assuming there are no other active databases on respective cluster. If there are, the formula will have to be adjusted proportionally.
JDBC¶
This section is used for JDBC additional settings:
- fetch_size (Integer) represents the number of rows physically retrieved from the database at one time by the JDBC driver as you scroll through a query's scrollable result set. [TODO: this is a global parameter; move this documentation accordingly]
Example:<node class="container" name="database"> <node class="container" name="my_database"> <node class="container" name="orm"> ... <node class="container" name="jdbc"> <node class="integer" name="fetch_size"> <node-attribute name="value" value="1024"/> </node> </node> ...
SQL Logging¶
It can be useful in limited situations to log all SQL statements which are executed by FWD for a particular database. The verbose nature of this logging can result in a very noise server log, and performance of an application may be adversely affected with SQL logging enabled. Therefore, this setting is only intended for diagnostic/debug use and it is recommended that it should be disabled for normal production use.
This setting is configured directly under the orm container.
- show_sql ( boolean) used to set enable the logging of all the generated SQL statements. The default value is FALSE.
Example:<node class="container" name="database"> <node class="container" name="my_database"> <node class="container" name="orm"> ... <node class="boolean" name="show_sql"> <node-attribute name="value" value="TRUE"/> </node> ...
Session hook¶
A hook that can be executed each time a FWD session is established (in Session() constructor) and also when the connection is closed (in Session.close()).
The custom hook must implement com.goldencode.p2j.persist.orm.SessionHook and override onSessionClaim() and onSessionClose().
To be noted that onSessionClaim was initially named onSessionOpen, so renaming the method in the implementations of the SessionHook is necessary.
This setting is configured directly under the orm container.
- session_hook ( String) The name of the custom implementation of
SessionHookinterface.
Example:<node class="container" name="database"> <node class="container" name="my_database"> <node class="container" name="orm"> ... <node class="string" name="session_hook"> <node-attribute name="value" value="_path_to_the_implementation_"/> </node> ...
Configuring session parameters¶
FWD allows to config database session parameters across all supported dialects, which sets the parameters when acquiring the connection.
The parameters are read from the startup-config node of a database in the directory and applied by the dialect's connection customizer as a session-scoped SET command every time a pooled connection is acquired.
Because the parameters are applied at session scope, each entry must name a variable/option that the database accepts as a per-session SET; server-global or persistent settings are out of scope for this mechanism.
- PostgreSQL
<node class="container" name="database"> <node class="container" name="my_database"> ... <node class="container" name="p2j"> ... <node class="strings" name="startup-config"> <node-attribute name="values" value="synchronous_commit = off"/> </node> - MariaDB
<node class="container" name="database"> <node class="container" name="my_database"> ... <node class="container" name="p2j"> ... <node class="strings" name="startup-config"> <node-attribute name="values" value="sort_buffer_size = 1048576"/> <node-attribute name="values" value="time_zone = '+00:00'"/> <node-attribute name="values" value="foreign_key_checks = 0"/> </node> - SQLServer
<node class="container" name="database"> <node class="container" name="my_database"> ... <node class="container" name="p2j"> ... <node class="strings" name="startup-config"> <node-attribute name="values" value="STATISTICS IO ON"/> </node> - H2
<node class="container" name="database"> <node class="container" name="my_database"> ... <node class="container" name="p2j"> ... <node class="strings" name="startup-config"> <node-attribute name="values" value="WRITE_DELAY 0"/> </node>
Implications of commit synchronization (durability vs. performance)¶
The most impactful session parameter is the one that controls commit durability - whether the database waits for the transaction log to be physically flushed to stable storage before reporting a COMMIT as successful. Each dialect exposes a different knob for this, but the trade-off is the same:
- Synchronous commit (durable) -
COMMITreturns only after the log record reaches disk. A committed transaction is guaranteed to survive an operating-system or hardware crash. This is slower because every commit incurs a diskfsync. - Asynchronous / delayed commit (relaxed) -
COMMITreturns as soon as the log record is buffered, before it is flushed. Throughput increases and commit latency drops sharply, but a crash can lose the most recent committed transactions (those still sitting in the buffer).
Important: relaxing commit synchronization never corrupts the database and never produces partial/torn transactions. The database always recovers to a consistent state; the only risk is losing a small window of the latest committed transactions after an abrupt crash. Normal shutdowns lose nothing.
Default values per dialect¶
The following table lists the parameter that governs commit durability in each dialect and its out-of-the-box default.
| Dialect | Parameter | Default | Meaning of the default |
|---|---|---|---|
| PostgreSQL | synchronous_commit |
on |
Fully durable - COMMIT waits for the WAL record to be flushed to disk. |
| MariaDB | innodb_flush_log_at_trx_commit |
1 |
Fully durable - the redo log is written and fsync-ed at every COMMIT. |
| SQL Server | DELAYED_DURABILITY |
DISABLED |
Fully durable - COMMIT waits for the transaction-log flush. (Database-level setting; see the note below.) |
| H2 | WRITE_DELAY |
500 (ms) |
Not fully durable - changes are flushed at most every 500 ms, so a crash can lose up to ~0.5 s of committed work. |
- On PostgreSQL,
synchronous_commit = offcan lose transactions committed within roughly the last3 x wal_writer_delay(~ 0.6 s with the defaultwal_writer_delay = 200ms). Valueslocal,remote_writeandremote_applytune behaviour for replication. Note that FWD overrides this default tooff- see "FWD default for PostgreSQL" below. - On MariaDB,
innodb_flush_log_at_trx_commit = 0or2relaxes durability to roughly one flush per second (up to ~1 s of loss). This variable is session-settable, so it works withstartup-config. - On SQL Server, delayed durability is a database-level option (
ALTER DATABASE ... SET DELAYED_DURABILITY) or a per-transactionCOMMITclause; there is no session-scopedSETequivalent, so it cannot be relaxed throughstartup-config. It stays fully durable unless changed at the database level. - On H2,
WRITE_DELAY 0forces an immediate flush on every commit (fully durable, slower); the default of 500 ms favours throughput over durability.
OpenEdge (Progress) default¶
OpenEdge's commit durability is governed by the Delayed BI File Write (-Mf) startup parameter, which controls how long the engine may delay flushing the last before-image (BI) record - the write that makes a committed transaction durable. Its default depends on the database mode:
| OpenEdge mode | -Mf default | Durability of the default |
|---|---|---|
| Single-user | -Mf 0 |
Fully durable - the last BI record is flushed immediately at commit (equivalent to synchronous_commit = on / WRITE_DELAY 0). |
| Multi-user | -Mf 3 |
Relaxed - the last BI record may be held up to ~3 seconds (a group-commit optimization), so an abrupt crash can lose the most-recent committed transactions within that window. |
(Full crash protection can also be disabled entirely as an explicit opt-in, e.g. the -i "no integrity / no crash protection" mode used for bulk loads.)
- To match an OpenEdge single-user source (fully durable), keep the durable defaults -
synchronous_commit = on(PostgreSQL),innodb_flush_log_at_trx_commit = 1(MariaDB) - and setWRITE_DELAY 0on H2, whose 500 ms default is otherwise non-durable. - To match an OpenEdge multi-user source, note that the database defaults on PostgreSQL, MariaDB and SQL Server are actually stricter (fully durable) than OpenEdge's ~ 3 s delayed-BI window. Applications that want the original throughput characteristics may deliberately relax durability to approximate
-Mf 3- e.g.synchronous_commit = offon PostgreSQL (~ 0.6 s window) - accepting the same "lose the last moments of committed work on a crash" behaviour that OpenEdge multi-user already had by default. (H2's 500 msWRITE_DELAYdefault already sits in this relaxed range.)
FWD default for PostgreSQL¶
For PostgreSQL, FWD's connection customizer applies synchronous_commit = off on every acquired connection by default - you do not need to configure it. The default is suppressed only when the database's startup-config sets synchronous_commit explicitly (to any value), in which case the configured value wins.
This deliberately makes FWD's PostgreSQL default less durable than stock PostgreSQL (on): it favours throughput and more closely matches the delayed-BI-write behaviour of a default multi-user OpenEdge database (-Mf 3). To restore full durability, add an explicit override to startup-config:
<node class="strings" name="startup-config">
<node-attribute name="values" value="synchronous_commit = on"/>
</node>
The other dialects (MariaDB, SQL Server, H2) keep their own native defaults; FWD applies no implicit durability override for them.
Multi-tenant databases¶
In a multi-tenant deployment each tenant's physical database has its own connection pool, so the session-scoped SET statements (and the PostgreSQL synchronous_commit default) are applied per tenant connection and never leak between tenants.
startup-config is defined on the primary (default-tenant) database and is inherited uniformly by every tenant of that logical database - there is no per-tenant startup-config override. Consequences:
- All tenants of a logical database receive the same
startup-configstatements. - The PostgreSQL
synchronous_commitdefault is therefore all-or-nothing across tenants: either every tenant runs withoff, or - if the primary'sstartup-configsetssynchronous_commit- every tenant uses that configured value. A single tenant cannot be given a durability setting different from the others.
Example Configuration¶
Here is a full example of a permanent database configuration:
<node class="container" name="database">
<node class="container" name="my_database">
<node class="container" name="p2j">
<node class="string" name="schema">
<node-attribute name="value" value="my_schema"/>
</node>
<node class="boolean" name="load_at_startup">
<node-attribute name="value" value="TRUE"/>
</node>
<node class="string" name="embedded-collation">
<node-attribute name="value" value="en_US_P2J"/>
</node>
<node class="strings" name="startup-config">
<node-attribute name="values" value="synchronous_commit = off"/>
</node>
</node>
<node class="container" name="orm">
<node class="string" name="dialect">
<node-attribute name="value" value="com.goldencode.p2j.persist.dialect.P2JPostgreSQLDialect"/>
</node>
<node class="container" name="connection">
<node class="string" name="driver_class">
<node-attribute name="value" value="org.postgresql.Driver"/>
</node>
<node class="string" name="url">
<node-attribute name="value" value="jdbc:postgresql://localhost:5433/my_database_instance"/>
</node>
<node class="string" name="username">
<node-attribute name="value" value="fwd_user"/>
</node>
<node class="string" name="password">
<node-attribute name="value" value="user"/>
</node>
<node class="integer" name="prepareThreshold">
<node-attribute name="value" value="1"/>
</node>
</node>
<node class="boolean" name="show_sql">
<node-attribute name="value" value="FALSE"/>
</node>
<node class="container" name="c3p0">
<node class="integer" name="minPoolSize">
<node-attribute name="value" value="10"/>
</node>
<node class="integer" name="maxPoolSize">
<node-attribute name="value" value="50"/>
</node>
<node class="integer" name="acquireIncrement">
<node-attribute name="value" value="5"/>
</node>
<node class="integer" name="maxIdleTime">
<node-attribute name="value" value="900"/>
</node>
<node class="integer" name="maxStatementsPerConnection">
<node-attribute name="value" value="100"/>
</node>
</node>
<node class="container" name="jdbc">
<node class="integer" name="fetch_size">
<node-attribute name="value" value="1024"/>
</node>
</node>
</node>
</node>
</node>
To reiterate, the legacy physical database name (in this case, my_database) does not need to match the legacy schema name (in this case my_schema), and neither needs to match the relational database instance name (in this case, my_database_instance).
The legacy physical database name and the legacy schema name are used by the FWD server to logically identify a relational database configuration, mapping it to legacy database references in an application. The relational database can have any arbitrary name; the JDBC URL specified in the directory's database configuration for a particular legacy database tells FWD how to connect to that resource.
Reference¶
With the exception of the port_services section, all of the configuration values below must be located in a <db_path>/database/<db_name>/ container. The port_services section must be placed directly under /server/default/ or in server/<server_id>/.
Here is the list of the database options and their path which can be configured within the directory:
| Option ID | Data Type | Default Value | Required | Details |
|---|---|---|---|---|
port_services/<service_name> |
Integer | N/A | No | FWD uses this data to create a logical connection (in the 4GL sense) to a database using the specified parameters. It can also be specified as parameters to server startup if not given inside directory. |
p2j/embedded-collation |
String | en_US@iso88591_fwd_basic |
No | This is used to set collation rules for H2 dialect. The other dialects are not affected. |
p2j/load_at_startup |
Boolean | false |
No | This is used to determine auto-connect status, in the 4GL connection sense (not the JDBC connection sense). |
p2j/mutable |
Boolean | false |
No | Determine whether a database's schema is programmatically mutable, which is set by configuration. |
p2j/schema |
String | <database-name> |
No | The name of the schema associated with this database, as exported from the 4GL Data Dictionary. |
p2j/use_java_udfs |
Boolean | false |
No | Decide whether to use Java or SQL UDFs. Java UDFs are now deprecated and the recommended value to use is false. |
orm/connection/driver_class |
String | N/A | Yes | The Java Driver class used to create the JDBC connection. |
orm/connection/username |
String | N/A | Yes | The username used to connect to the database. |
orm/connection/password |
String | N/A | Yes | The password used to connect to the database. |
orm/connection/url |
String | N/A | Yes | The JDBC URL (formatted according to the database vendor's requirements) used by JDBC to connect to the database instance. |
orm/connection/prepareThreshold |
Integer | 3* |
No | The threshold number of times a statement is called for which the query will be used as a named query saving the execution plan at the database. * Only used by PostreSQL (defaults to 3). |
orm/c3p0/minPoolSize |
Integer | 3 |
No | The minimum number of connections the pool will maintain at any given time. |
orm/c3p0/maxPoolSize |
Integer | 15 |
No | The maximum number of connections the pool will maintain at any given time. |
orm/c3p0/maxIdleTime |
Integer | 0 |
No | The number of seconds a pooled connection will be available before it is released. Set to 0 or omit for connections that do not expire. |
orm/c3p0/maxStatementsPerConnection |
Integer | 0 |
No | The total number PreparedStatements a DataSource will cache per pooled connection.The pool will destroy the least-recently-used PreparedStatement when it hits this limit. Set to 0 or omit to disable prepared statement caching. |
orm/c3p0/acquireIncrement |
Integer | 3 |
No | This parameter is used to determine how many connections at a time c3p0 will try to acquire when the pool is exhausted. |
orm/jdbc/fetch_size |
Integer | 256 / 1024 |
No | Represents the number of rows physically retrieved from the database at one time by the JDBC driver as you scroll through a query ResultSet with next(). |
orm/show_sql |
Boolean | false |
No | Enable/disable the logging of all the generated SQL statements to the server log. |
orm/dialect |
String | N/A | Yes | The Java class that which defines database dialect-specific behavior and syntax. |
© 2004-2021 Golden Code Development Corporation. ALL RIGHTS RESERVED.