Skip to content

Commit 9c32042

Browse files
authored
Merge branch 'main' into fix-app-deploy
2 parents 9654da5 + 9e67748 commit 9c32042

File tree

6 files changed

+226
-59
lines changed

6 files changed

+226
-59
lines changed

openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TypeResourceIT.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88

99
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import java.util.ArrayList;
1011
import java.util.List;
1112
import java.util.Map;
1213
import java.util.UUID;
14+
import java.util.concurrent.CopyOnWriteArrayList;
15+
import java.util.concurrent.CountDownLatch;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.TimeUnit;
1319
import org.junit.jupiter.api.BeforeAll;
1420
import org.junit.jupiter.api.Disabled;
1521
import org.junit.jupiter.api.Test;
@@ -542,6 +548,65 @@ void test_addMultipleHyperlinkCustomProperties(TestNamespace ns) throws Exceptio
542548
assertTrue(hasProp2, "Type should have second hyperlink custom property");
543549
}
544550

551+
@Test
552+
void test_concurrentCustomPropertyAdditions(TestNamespace ns) throws Exception {
553+
OpenMetadataClient client = SdkClients.adminClient();
554+
Type pipelineType = getTypeByName(client, "pipeline");
555+
556+
int threadCount = 5;
557+
List<CustomProperty> properties = new ArrayList<>();
558+
for (int i = 0; i < threadCount; i++) {
559+
CustomProperty prop = new CustomProperty();
560+
prop.setName(ns.prefix("concurrentProp" + i));
561+
prop.setDescription("Concurrent property " + i);
562+
prop.setPropertyType(STRING_TYPE.getEntityReference());
563+
properties.add(prop);
564+
}
565+
566+
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
567+
CountDownLatch startLatch = new CountDownLatch(1);
568+
CountDownLatch doneLatch = new CountDownLatch(threadCount);
569+
List<Exception> errors = new CopyOnWriteArrayList<>();
570+
571+
for (CustomProperty prop : properties) {
572+
executor.submit(
573+
() -> {
574+
try {
575+
startLatch.await();
576+
addCustomProperty(client, pipelineType.getId(), prop);
577+
} catch (InterruptedException e) {
578+
Thread.currentThread().interrupt();
579+
errors.add(e);
580+
} catch (Exception e) {
581+
errors.add(e);
582+
} finally {
583+
doneLatch.countDown();
584+
}
585+
});
586+
}
587+
588+
startLatch.countDown();
589+
assertTrue(doneLatch.await(30, TimeUnit.SECONDS), "All threads should complete within 30s");
590+
executor.shutdown();
591+
592+
assertTrue(errors.isEmpty(), "Concurrent requests should not throw: " + errors);
593+
594+
Type updatedType = getTypeByName(client, "pipeline", "customProperties");
595+
List<String> persistedNames =
596+
updatedType.getCustomProperties() != null
597+
? updatedType.getCustomProperties().stream().map(CustomProperty::getName).toList()
598+
: List.of();
599+
600+
for (CustomProperty prop : properties) {
601+
assertTrue(
602+
persistedNames.contains(prop.getName()),
603+
"Property '"
604+
+ prop.getName()
605+
+ "' was lost due to a concurrent update. Persisted: "
606+
+ persistedNames);
607+
}
608+
}
609+
545610
private static Type createType(OpenMetadataClient client, CreateType createRequest)
546611
throws Exception {
547612
return client
@@ -565,6 +630,16 @@ private static Type getTypeByName(OpenMetadataClient client, String name) throws
565630
return OBJECT_MAPPER.readValue(response, Type.class);
566631
}
567632

633+
private static Type getTypeByName(OpenMetadataClient client, String name, String fields)
634+
throws Exception {
635+
String response =
636+
client
637+
.getHttpClient()
638+
.executeForString(
639+
HttpMethod.GET, "/v1/metadata/types/name/" + name + "?fields=" + fields, null);
640+
return OBJECT_MAPPER.readValue(response, Type.class);
641+
}
642+
568643
private static TypeList listTypes(OpenMetadataClient client) throws Exception {
569644
String response =
570645
client.getHttpClient().executeForString(HttpMethod.GET, "/v1/metadata/types", null);

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TypeRepository.java

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Map;
3434
import java.util.Set;
3535
import java.util.UUID;
36+
import java.util.concurrent.ConcurrentHashMap;
3637
import java.util.stream.Collectors;
3738
import lombok.extern.slf4j.Slf4j;
3839
import org.apache.commons.lang3.tuple.Triple;
@@ -64,6 +65,8 @@
6465
public class TypeRepository extends EntityRepository<Type> {
6566
private static final String UPDATE_FIELDS = "customProperties";
6667
private static final String PATCH_FIELDS = "customProperties";
68+
private static final ConcurrentHashMap<UUID, Object> TYPE_PROPERTY_LOCKS =
69+
new ConcurrentHashMap<>();
6770

6871
public TypeRepository() {
6972
super(
@@ -162,31 +165,34 @@ public void postUpdate(Type original, Type updated) {
162165

163166
public PutResponse<Type> addCustomProperty(
164167
UriInfo uriInfo, String updatedBy, UUID id, CustomProperty property) {
165-
Type type = find(id, Include.NON_DELETED);
166-
property.setPropertyType(
167-
Entity.getEntityReferenceById(
168-
Entity.TYPE, property.getPropertyType().getId(), NON_DELETED));
169-
validateProperty(property);
170-
if (type.getCategory().equals(Category.Field)) {
171-
throw new IllegalArgumentException(
172-
"Only entity types can be extended and field types can't be extended");
173-
}
174-
setFieldsInternal(type, putFields);
168+
Object lock = TYPE_PROPERTY_LOCKS.computeIfAbsent(id, k -> new Object());
169+
synchronized (lock) {
170+
Type type = find(id, Include.NON_DELETED);
171+
property.setPropertyType(
172+
Entity.getEntityReferenceById(
173+
Entity.TYPE, property.getPropertyType().getId(), NON_DELETED));
174+
validateProperty(property);
175+
if (type.getCategory().equals(Category.Field)) {
176+
throw new IllegalArgumentException(
177+
"Only entity types can be extended and field types can't be extended");
178+
}
179+
setFieldsInternal(type, putFields);
175180

176-
find(property.getPropertyType().getId(), NON_DELETED); // Validate customProperty type exists
181+
find(property.getPropertyType().getId(), NON_DELETED); // Validate customProperty type exists
177182

178-
// If property already exists, then update it. Else add the new property.
179-
List<CustomProperty> updatedProperties = new ArrayList<>(List.of(property));
180-
for (CustomProperty existing : type.getCustomProperties()) {
181-
if (!existing.getName().equals(property.getName())) {
182-
updatedProperties.add(existing);
183+
// If property already exists, then update it. Else add the new property.
184+
List<CustomProperty> updatedProperties = new ArrayList<>(List.of(property));
185+
for (CustomProperty existing : type.getCustomProperties()) {
186+
if (!existing.getName().equals(property.getName())) {
187+
updatedProperties.add(existing);
188+
}
183189
}
184-
}
185190

186-
type.setCustomProperties(updatedProperties);
187-
type.setUpdatedBy(updatedBy);
188-
type.setUpdatedAt(System.currentTimeMillis());
189-
return createOrUpdate(uriInfo, type, updatedBy);
191+
type.setCustomProperties(updatedProperties);
192+
type.setUpdatedBy(updatedBy);
193+
type.setUpdatedAt(System.currentTimeMillis());
194+
return createOrUpdate(uriInfo, type, updatedBy);
195+
}
190196
}
191197

192198
private List<CustomProperty> getCustomProperties(Type type) {

openmetadata-ui/src/main/resources/ui/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
"cookie-storage": "^6.1.0",
9595
"cronstrue": "^2.53.0",
9696
"crypto-random-string-with-promisify-polyfill": "^5.0.0",
97-
"diff": "^5.0.0",
97+
"diff": "^5.2.2",
9898
"dompurify": "^3.2.4",
9999
"elkjs": "^0.9.3",
100100
"eventemitter3": "^5.0.1",
@@ -113,7 +113,7 @@
113113
"notistack": "^3.0.2",
114114
"oidc-client": "^1.11.5",
115115
"process": "^0.11.10",
116-
"qs": "6.14.1",
116+
"qs": "6.14.2",
117117
"quill-mention": "^6.0.1",
118118
"quilljs-markdown": "^1.2.0",
119119
"rapidoc": "^9.3.8",
@@ -264,6 +264,7 @@
264264
"tar-fs": "2.1.4",
265265
"js-yaml": "4.1.1",
266266
"lodash": ">=4.17.23",
267-
"lodash-es": ">=4.17.23"
267+
"lodash-es": ">=4.17.23",
268+
"markdown-it": ">=14.1.1"
268269
}
269270
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# SFTP
2+
3+
In this section, we provide guides and references to use the SFTP connector.
4+
5+
## Requirements
6+
7+
To extract metadata from an SFTP server, the user needs to have read access to the directories and files to be catalogued.
8+
9+
You can find further information on the SFTP connector in the <a href="https://docs.open-metadata.org/connectors/drive/sftp" target="_blank">docs</a>.
10+
11+
## Connection Details
12+
13+
$$section
14+
### Host $(id="host")
15+
SFTP server hostname or IP address (e.g., `sftp.example.com` or `192.168.1.100`).
16+
$$
17+
18+
$$section
19+
### Port $(id="port")
20+
SFTP server port number. Defaults to `22`.
21+
$$
22+
23+
$$section
24+
### Authentication Type $(id="authType")
25+
Authentication method to connect to the SFTP server. Choose between:
26+
- **Username/Password**: Authenticate using a username and password.
27+
- **Private Key**: Authenticate using an SSH private key in PEM format. Supports RSA, Ed25519, ECDSA, and DSS keys.
28+
$$
29+
30+
$$section
31+
### Username $(id="username")
32+
SFTP username used for authentication.
33+
$$
34+
35+
$$section
36+
### Password $(id="password")
37+
Password for username/password authentication.
38+
$$
39+
40+
$$section
41+
### Private Key $(id="privateKey")
42+
SSH private key content in PEM format for key-based authentication. Supports RSA, Ed25519, ECDSA, and DSS keys.
43+
$$
44+
45+
$$section
46+
### Private Key Passphrase $(id="privateKeyPassphrase")
47+
Passphrase to decrypt the private key, if the key is encrypted. Leave blank if the key has no passphrase.
48+
$$
49+
50+
$$section
51+
### Root Directories $(id="rootDirectories")
52+
List of root directories to scan for files and subdirectories. Defaults to `/` (the user's home directory). Multiple directories can be specified to scope the ingestion to specific paths on the server.
53+
$$
54+
55+
$$section
56+
### Connection Options $(id="connectionOptions")
57+
Additional connection options to build the URL that can be sent to the service during the connection.
58+
$$
59+
60+
$$section
61+
### Connection Arguments $(id="connectionArguments")
62+
Additional connection arguments such as security or protocol configs that can be sent to the service during connection.
63+
$$
64+
65+
$$section
66+
### Directory Filter Pattern $(id="directoryFilterPattern")
67+
Regex to only include/exclude directories that match the pattern.
68+
$$
69+
70+
$$section
71+
### File Filter Pattern $(id="fileFilterPattern")
72+
Regex to only include/exclude files that match the pattern.
73+
$$
74+
75+
$$section
76+
### Structured Data Files Only $(id="structuredDataFilesOnly")
77+
When enabled, only catalog structured data files (CSV, TSV) that can have schema extracted. Non-structured files like images, PDFs, and videos will be skipped. Defaults to `false`.
78+
$$
79+
80+
$$section
81+
### Extract Sample Data $(id="extractSampleData")
82+
When enabled, extract sample data from structured files (CSV, TSV). Disabled by default to avoid performance overhead.
83+
$$

openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,8 @@ export const BETA_SERVICES = [
461461
DatabaseServiceType.MicrosoftFabric,
462462
PipelineServiceType.MicrosoftFabricPipeline,
463463
DatabaseServiceType.BurstIQ,
464+
DatabaseServiceType.StarRocks,
465+
DriveServiceType.SFTP,
464466
];
465467

466468
export const TEST_CONNECTION_INITIAL_MESSAGE =

0 commit comments

Comments
 (0)