Skip to content

Commit

Permalink
issue #3614 - replace "updateIfModified" with "forceUpdate"
Browse files Browse the repository at this point in the history
Replaced the old "X-FHIR-UPDATE-IF-MODIFIED" header with a new
"X-FHIR-FORCE-UPDATE" one. The default is still false, but now that
means that no-op updates are now skipped by default.

To force the server to perform a no-op update, users must now explicitly
set this X-FHIR-FORCE-UPDATE header to true.

Signed-off-by: Lee Surprenant <lmsurpre@us.ibm.com>
  • Loading branch information
lmsurpre committed May 3, 2022
1 parent 64f3cd5 commit a35d772
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 57 deletions.
23 changes: 11 additions & 12 deletions docs/src/pages/Conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ The IBM FHIR Server supports headers to modify the behavior of interactions acco

| Interaction | Path | Verb | Header | Description |
| -------------- | ------------ | ------------- | ------------------------- | ----------- |
| update | /[type]/[id] | PUT | If-None-Match | Custom support for conditional create-on-update |
| update | /[type]/[id] | PUT | X-FHIR-UPDATE-IF-MODIFIED | Custom support for conditional update |
| _any_ | _any_ | _any_ | X-FHIR-TENANT-ID | Custom support for multi-tenancy |
| _any_ | _any_ | _any_ | X-FHIR-DSID | Custom support for multiple datasources |
| update | /[type]/[id] | PUT | If-None-Match | Custom support for conditional create-on-update |
| update | /[type]/[id] | PUT | X-FHIR-FORCE-UPDATE | Overrides server optimization for "no-op" updates |
| _any_ | _any_ | _any_ | X-FHIR-TENANT-ID | Custom support for multi-tenancy |
| _any_ | _any_ | _any_ | X-FHIR-DSID | Custom support for multiple datasources |


In addition to the content negotiation headers required in the FHIR specification, the IBM FHIR Server supports two client preferences via the `Prefer` header:
Expand All @@ -52,15 +52,13 @@ In `lenient` mode, the client must [check the self uri](https://www.hl7.org/fhir

Note: In addition to controlling whether or not the server returns an error for unexpected search parameters, the handling preference is also used to control whether or not the server will return an error for unexpected elements in the JSON representation of a Resource as defined at https://www.hl7.org/fhir/json.html.

The IBM FHIR Server supports a custom header, `X-FHIR-UPDATE-IF-MODIFIED`, for clients to opt in to a specific update optimization. See Section 5.2. Conditional Update of the [Performance Guide](guides/FHIRPerformanceGuide) for more information.
The IBM FHIR Server supports conditional create-on-update using the `If-None-Match` header. This IBM FHIR Server-specific feature allows clients to use a `PUT` (update) interaction which behaves as follows:

The IBM FHIR Server also supports conditional create-on-update using the `If-None-Match` header. This IBM FHIR Server-specific feature allows clients to use a `PUT` (update) interaction which behaves as follows:
1. If the resource does not yet exist, create the resource and return `201 Created`;
2. If the resource does exist, do nothing and return `412 Precondition Failed` (default behavior);
3. If the resource does exist and the fhir-server-config element `fhirServer/core/ifNoneMatchReturnsNotModified` is set to `true`, do nothing and return `304 Not Modified`.

1. `If-None-Match: "*"`: If the resource does not yet exist, create the resource and return `201 Created`;
2. `If-None-Match: "*"`: If the resource does exist, do nothing and return `412 Precondition Failed` (default behavior);
3. `If-None-Match: "*"`: If the resource does exist and the fhir-server-config element `fhirServer/core/ifNoneMatchReturnsNotModified` is set to `true`, do nothing and return `304 Not Modified`.

The only supported value for If-None-Match conditional create-on-update is `"*"`. This feature can also be used for `PUT` requests in transaction or batch bundles by specifying the `ifNoneMatch` field similarly in the request element:
The only supported value for the If-None-Match header for update interactions is `"*"`. This feature can also be used for `PUT` requests in transaction or batch bundles by specifying the `ifNoneMatch` field similarly in the request element:
```
{
"resourceType" : "Bundle",
Expand All @@ -85,7 +83,6 @@ The only supported value for If-None-Match conditional create-on-update is `"*"`
}
]
}
```

If a match is found and the fhir-server-config element `fhirServer/core/ifNoneMatchReturnsNotModified` is not configured or is set to `false`, the condition is treated as an error which will cause transaction bundles to fail, returning a status of `400` (Bad Request). For batch bundles, the entry response status will be `412` (Precondition Failed).
Expand All @@ -109,6 +106,8 @@ If a match is found and the fhir-server-config element `fhirServer/core/ifNoneMa
}
```

The server also implements an optimization for updates that do not change the resource contents. See Section 5.2. Conditional Update of the [Performance Guide](guides/FHIRPerformanceGuide#52-conditional-update) for more information.

Finally, the IBM FHIR Server supports multi-tenancy through custom headers as defined at https://ibm.github.io/FHIR/guides/FHIRServerUsersGuide#49-multi-tenancy. By default, the server will look for a tenantId in a `X-FHIR-TENANT-ID` header and a datastoreId in the `X-FHIR-DSID` header, and use `default` for either one if the headers are not present.

### General parameters
Expand Down
7 changes: 5 additions & 2 deletions docs/src/pages/guides/FHIRPerformanceGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,11 +484,11 @@ Avoiding these unnecessary updates is important for two reasons:
1. ingestion performance (each update performs work in the database)
2. database size (each version of each resource is stored in the database)

The HL7 FHIR specification includes experimental support for both [conditional create](https://www.hl7.org/fhir/R4/http.html#ccreate) and [conditional update](https://www.hl7.org/fhir/R4/http.html#cond-update) and the IBM FHIR server implements each of these. However, this approach suffers multiple issues:
The HL7 FHIR specification defines experimental support for both [conditional create](https://www.hl7.org/fhir/R4/http.html#ccreate) and [conditional update](https://www.hl7.org/fhir/R4/http.html#cond-update) and the IBM FHIR server implements each of these. However, this approach suffers multiple issues:
1. each update must perform a search which can be more costly than simply performing read before the update
2. conditional requests require intricate locking techniques to avoid race conditions and the currently-implemented approach has [significant limitations](https://github.com/IBM/FHIR/issues/2051)

Instead, IBM FHIR Server version 4.7.1 introduces support for a server-enabled optimization to avoid performing unnecessary updates. When users pass the `X-FHIR-UPDATE-IF-MODIFIED` header with a value of `true`, the server will perform a comparison of the resource contents from the update with the contents of the resource in the database.
Instead, IBM FHIR Server version 4.7.1 introduces support for a server-enabled optimization to avoid performing unnecessary updates. The server will only create a new resource version if the resource contents from the update differ from the contents of the resource in the database.

Two resources will be considered equivalent based on the following criteria:
* whitespace between the resource elements (both XML and JSON) is ignored
Expand All @@ -498,6 +498,9 @@ Two resources will be considered equivalent based on the following criteria:
When the update is skipped, the response will contain a Location header that points to the *existing* resource version (e.g. `[base]/Patient/1234/_history/1`) instead of a newly created instance of this resource (`[base]/Patient/1234/_history/2`) and the response body will be sent according to the client's [return preference](https://www.hl7.org/fhir/R4/http.html#ops).
If the client indicates a return preference of OperationOutcome and the update is skipped on the server, the response will contain an informational issue to indicate this case.

To opt out of this optimization, users can force the server to perform an update by setting an
HTTP header named `X-FHIR-FORCE-UPDATE` to "true".

# 6. Client Access Scenarios

The IBM FHIR Server translates a FHIR search request into a SQL query. The database performs query optimization to generate what it thinks is the most efficient execution plan before running the query. This optimization depends on the database having good statistics (and a clever algorithm) to make the right choice. When this goes wrong, the result is a slow response which can also end up consuming significant resources which impact the capacity of the system as a whole.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/guides/FHIRServerUsersGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2764,7 +2764,7 @@ IBM FHIR Server Supports the following custom HTTP Headers:
|`X-FHIR-TENANT-ID`|Specifies which tenant config should be used for the request. Default is `default`. The header name can be overridden via config property `fhirServer/core/tenantIdHeaderName`.|
|`X-FHIR-DSID`|Specifies which datastore config should be used for the request. Default is `default`. The header name can be overridden via config property `fhirServer/core/dataSourceIdHeaderName`.|
|`X-FHIR-FORWARDED-URL`|The original (user-facing) request URL; used for constructing absolute URLs within the server response. Only enabled when explicitly configured in the default fhir-server-config.json. If either the config property or the header itself is missing, the server will use the actual request URL. The header name can be overridden via config property `fhirServer/core/originalRequestUriHeaderName`. Note that `fhirServer/core/externalBaseUrl` overrides the `X-FHIR-FORWARDED-URL` and is used to construct the absolute URL. Also note that the base URL's value must not include a path segment that matches any FHIR resource type name (case-sensitive). For example, "https://example.com" or "https://example.com/my/patient/api" are fine, but "https://example.com/my/Patient/api" is not.|
|`X-FHIR-UPDATE-IF-MODIFIED`|When set to true, for update and patch requests, the server will perform a resource comparison and only perform the update if the contents of the resource have changed. For all other values, the update will be executed as normal.|
|`X-FHIR-FORCE-UPDATE`|By default, when the server receives an update request, it performs a resource comparison and only performs the update if the contents of the resource have changed. Set this header to true to skip that check and force the update.|

# 6 Related topics
For more information about topics related to configuring a FHIR server, see the following documentation:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class FHIRConstants {

public static final String ELEMENTS = "_elements";

public static final String UPDATE_IF_MODIFIED_HEADER = "X-FHIR-UPDATE-IF-MODIFIED";
public static final String FORCE_UPDATE_HEADER = "X-FHIR-FORCE-UPDATE";

public static final String EXT_BASE = "http://ibm.com/fhir/extension/";
public static final String TAG_BASE = "http://ibm.com/fhir/tag/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ public class BundleTest extends FHIRServerTestBase {
private static final String PREFER_HEADER_RETURN_REPRESENTATION = "return=representation";
private static final String PREFER_HEADER_NAME = "Prefer";

private static final String UPDATE_IF_MODIFIED_HEADER_NAME = "X-FHIR-UPDATE-IF-MODIFIED";

/**
* Retrieve the server's conformance statement to determine the status of
* certain runtime options.
Expand Down Expand Up @@ -2627,8 +2625,7 @@ public void testTransactionBundleWithSkippableUpdates() throws Exception {

// Process bundle
FHIRRequestHeader returnPref = FHIRRequestHeader.header(PREFER_HEADER_NAME, PREFER_HEADER_RETURN_REPRESENTATION);
FHIRRequestHeader updateOnlyIfModified = FHIRRequestHeader.header(UPDATE_IF_MODIFIED_HEADER_NAME, true);
FHIRResponse response = client.transaction(requestBundle, returnPref, updateOnlyIfModified);
FHIRResponse response = client.transaction(requestBundle, returnPref);
assertNotNull(response);
assertResponse(response.getResponse(), Response.Status.OK.getStatusCode());
Bundle responseBundle = response.getResource(Bundle.class);
Expand Down Expand Up @@ -2658,7 +2655,7 @@ public void testTransactionBundleWithSkippableUpdates() throws Exception {
.build();

// Process bundle
FHIRResponse response2 = client.transaction(requestBundle, returnPref, updateOnlyIfModified);
FHIRResponse response2 = client.transaction(requestBundle, returnPref);
assertNotNull(response2);
assertResponse(response2.getResponse(), Response.Status.OK.getStatusCode());
Bundle responseBundle2 = response2.getResource(Bundle.class);
Expand Down Expand Up @@ -2706,7 +2703,7 @@ public void testTransactionBundleWithSkippableUpdates() throws Exception {
.build();

// Process the bundle request and check that the patch was skipped
FHIRResponse response3 = client.transaction(requestBundle, returnPref, updateOnlyIfModified);
FHIRResponse response3 = client.transaction(requestBundle, returnPref);
assertNotNull(response3);
assertResponse(response3.getResponse(), Response.Status.OK.getStatusCode());
Bundle responseBundle3 = response3.getResource(Bundle.class);
Expand Down
Loading

0 comments on commit a35d772

Please sign in to comment.