-
Notifications
You must be signed in to change notification settings - Fork 202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: opensearch source secrets refreshment suppport #3437
Changes from all commits
edeb6bf
2661629
9ae948b
2e33eac
6421c50
3256efb
3ba133f
9439296
d5fe312
c8af912
af0c206
ebdc448
94513cf
93b3794
06a3c27
1ba290e
0bd7a0f
ca74d46
a7d7fd0
613770f
c8fc2fe
8f90a0e
43972fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package org.opensearch.dataprepper.plugins.source.opensearch; | ||
|
||
import org.opensearch.dataprepper.model.plugin.PluginComponentRefresher; | ||
|
||
import java.util.Objects; | ||
import java.util.concurrent.locks.ReadWriteLock; | ||
import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
import java.util.function.Function; | ||
|
||
public class ClientRefresher<Client> | ||
implements PluginComponentRefresher<Client, OpenSearchSourceConfiguration> { | ||
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); | ||
private final Function<OpenSearchSourceConfiguration, Client> clientFunction; | ||
private OpenSearchSourceConfiguration existingConfig; | ||
private final Class<Client> clientClass; | ||
private Client currentClient; | ||
|
||
public ClientRefresher(final Class<Client> clientClass, | ||
final Function<OpenSearchSourceConfiguration, Client> clientFunction, | ||
final OpenSearchSourceConfiguration openSearchSourceConfiguration) { | ||
this.clientClass = clientClass; | ||
this.clientFunction = clientFunction; | ||
existingConfig = openSearchSourceConfiguration; | ||
currentClient = clientFunction.apply(openSearchSourceConfiguration); | ||
} | ||
|
||
@Override | ||
public Class<Client> getComponentClass() { | ||
return clientClass; | ||
} | ||
|
||
@Override | ||
public Client get() { | ||
readWriteLock.readLock().lock(); | ||
try { | ||
return currentClient; | ||
} finally { | ||
readWriteLock.readLock().unlock(); | ||
} | ||
} | ||
|
||
@Override | ||
public void update(final OpenSearchSourceConfiguration openSearchSourceConfiguration) { | ||
if (basicAuthChanged(openSearchSourceConfiguration)) { | ||
readWriteLock.writeLock().lock(); | ||
try { | ||
currentClient = clientFunction.apply(openSearchSourceConfiguration); | ||
existingConfig = openSearchSourceConfiguration; | ||
} finally { | ||
readWriteLock.writeLock().unlock(); | ||
} | ||
} | ||
} | ||
|
||
private boolean basicAuthChanged(final OpenSearchSourceConfiguration newConfig) { | ||
return !Objects.equals(existingConfig.getUsername(), newConfig.getUsername()) || | ||
!Objects.equals(existingConfig.getPassword(), newConfig.getPassword()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
import org.opensearch.dataprepper.model.annotations.DataPrepperPluginConstructor; | ||
import org.opensearch.dataprepper.model.buffer.Buffer; | ||
import org.opensearch.dataprepper.model.event.Event; | ||
import org.opensearch.dataprepper.model.plugin.PluginConfigObservable; | ||
import org.opensearch.dataprepper.model.record.Record; | ||
import org.opensearch.dataprepper.model.source.Source; | ||
import org.opensearch.dataprepper.model.source.coordinator.SourceCoordinator; | ||
|
@@ -29,6 +30,7 @@ public class OpenSearchSource implements Source<Record<Event>>, UsesSourceCoordi | |
private final OpenSearchSourceConfiguration openSearchSourceConfiguration; | ||
private final AcknowledgementSetManager acknowledgementSetManager; | ||
private final PluginMetrics pluginMetrics; | ||
private final PluginConfigObservable pluginConfigObservable; | ||
|
||
private SourceCoordinator<OpenSearchIndexProgressState> sourceCoordinator; | ||
private OpenSearchService openSearchService; | ||
|
@@ -37,11 +39,13 @@ public class OpenSearchSource implements Source<Record<Event>>, UsesSourceCoordi | |
public OpenSearchSource(final OpenSearchSourceConfiguration openSearchSourceConfiguration, | ||
final AwsCredentialsSupplier awsCredentialsSupplier, | ||
final AcknowledgementSetManager acknowledgementSetManager, | ||
final PluginMetrics pluginMetrics) { | ||
final PluginMetrics pluginMetrics, | ||
final PluginConfigObservable pluginConfigObservable) { | ||
this.openSearchSourceConfiguration = openSearchSourceConfiguration; | ||
this.awsCredentialsSupplier = awsCredentialsSupplier; | ||
this.acknowledgementSetManager = acknowledgementSetManager; | ||
this.pluginMetrics = pluginMetrics; | ||
this.pluginConfigObservable = pluginConfigObservable; | ||
|
||
openSearchSourceConfiguration.validateAwsConfigWithUsernameAndPassword(); | ||
} | ||
|
@@ -59,7 +63,8 @@ private void startProcess(final OpenSearchSourceConfiguration openSearchSourceCo | |
|
||
final OpenSearchClientFactory openSearchClientFactory = OpenSearchClientFactory.create(awsCredentialsSupplier); | ||
final OpenSearchSourcePluginMetrics openSearchSourcePluginMetrics = OpenSearchSourcePluginMetrics.create(pluginMetrics); | ||
final SearchAccessorStrategy searchAccessorStrategy = SearchAccessorStrategy.create(openSearchSourceConfiguration, openSearchClientFactory); | ||
final SearchAccessorStrategy searchAccessorStrategy = SearchAccessorStrategy.create( | ||
openSearchSourceConfiguration, openSearchClientFactory, pluginConfigObservable); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the code always assume that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is indeed always non-null. If there is no secret usage, the PluginConfigPublisher notifier would never be invoked even though the PluginConfigObservable will still be instantiated and loaded with PluginConfigObserver, i.e. PluginConfigObservable::update will never be invoked. At the client level, this means there is only read access to client although there is indeed a bit overhead from acquiring the read lock. |
||
|
||
final SearchAccessor searchAccessor = searchAccessorStrategy.getSearchAccessor(); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,9 @@ | |
|
||
package org.opensearch.dataprepper.plugins.source.opensearch.worker.client; | ||
|
||
public interface ClusterClientFactory { | ||
Object getClient(); | ||
import org.opensearch.dataprepper.model.plugin.PluginComponentRefresher; | ||
import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; | ||
|
||
public interface ClusterClientFactory<Client> { | ||
PluginComponentRefresher<Client, OpenSearchSourceConfiguration> getClientRefresher(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In line with my comment elsewhere, change this to:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably would not work for OpenSearchIndexPartitionCreationSupplier. Changed into
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately there are some tight coupling in using APIs, e.g. OpenSearchIndexPartitionCreationSupplier needs the SearchAccessor to return more than Supplier. I improved it to be propagating PluginComponentRefresher<Client, OpenSearchConfiguration> |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,8 @@ | |
import org.opensearch.dataprepper.model.event.Event; | ||
import org.opensearch.dataprepper.model.event.EventType; | ||
import org.opensearch.dataprepper.model.event.JacksonEvent; | ||
import org.opensearch.dataprepper.model.plugin.PluginComponentRefresher; | ||
import org.opensearch.dataprepper.plugins.source.opensearch.OpenSearchSourceConfiguration; | ||
import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.IndexNotFoundException; | ||
import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.exceptions.SearchContextLimitException; | ||
import org.opensearch.dataprepper.plugins.source.opensearch.worker.client.model.CreatePointInTimeRequest; | ||
|
@@ -55,18 +57,21 @@ | |
import static org.opensearch.dataprepper.plugins.source.opensearch.worker.client.model.MetadataKeyAttributes.DOCUMENT_ID_METADATA_ATTRIBUTE_NAME; | ||
import static org.opensearch.dataprepper.plugins.source.opensearch.worker.client.model.MetadataKeyAttributes.INDEX_METADATA_ATTRIBUTE_NAME; | ||
|
||
public class ElasticsearchAccessor implements SearchAccessor, ClusterClientFactory { | ||
public class ElasticsearchAccessor implements SearchAccessor, ClusterClientFactory<ElasticsearchClient> { | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchAccessor.class); | ||
|
||
static final String PIT_RESOURCE_LIMIT_ERROR_TYPE = "rejected_execution_exception"; | ||
static final String INDEX_NOT_FOUND_EXCEPTION = "index_not_found_exception"; | ||
|
||
private final ElasticsearchClient elasticsearchClient; | ||
private final PluginComponentRefresher<ElasticsearchClient, OpenSearchSourceConfiguration> | ||
elasticsearchClientRefresher; | ||
private final SearchContextType searchContextType; | ||
|
||
public ElasticsearchAccessor(final ElasticsearchClient elasticsearchClient, final SearchContextType searchContextType) { | ||
this.elasticsearchClient = elasticsearchClient; | ||
public ElasticsearchAccessor(final PluginComponentRefresher<ElasticsearchClient, OpenSearchSourceConfiguration> | ||
elasticsearchClientRefresher, | ||
final SearchContextType searchContextType) { | ||
this.elasticsearchClientRefresher = elasticsearchClientRefresher; | ||
this.searchContextType = searchContextType; | ||
} | ||
|
||
|
@@ -80,7 +85,8 @@ public CreatePointInTimeResponse createPit(final CreatePointInTimeRequest create | |
|
||
OpenPointInTimeResponse openPointInTimeResponse; | ||
try { | ||
openPointInTimeResponse = elasticsearchClient.openPointInTime(OpenPointInTimeRequest.of(request -> request | ||
openPointInTimeResponse = elasticsearchClientRefresher.get() | ||
.openPointInTime(OpenPointInTimeRequest.of(request -> request | ||
.keepAlive(Time.of(time -> time.time(createPointInTimeRequest.getKeepAlive()))) | ||
.index(createPointInTimeRequest.getIndex()))); | ||
} catch (final ElasticsearchException e) { | ||
|
@@ -131,7 +137,8 @@ public SearchWithSearchAfterResults searchWithPit(final SearchPointInTimeRequest | |
@Override | ||
public void deletePit(final DeletePointInTimeRequest deletePointInTimeRequest) { | ||
try { | ||
final ClosePointInTimeResponse closePointInTimeResponse = elasticsearchClient.closePointInTime(ClosePointInTimeRequest.of(request -> request | ||
final ClosePointInTimeResponse closePointInTimeResponse = elasticsearchClientRefresher.get() | ||
.closePointInTime(ClosePointInTimeRequest.of(request -> request | ||
.id(deletePointInTimeRequest.getPitId()))); | ||
if (closePointInTimeResponse.succeeded()) { | ||
LOG.debug("Successfully deleted point in time id {}", deletePointInTimeRequest.getPitId()); | ||
|
@@ -149,7 +156,8 @@ public CreateScrollResponse createScroll(final CreateScrollRequest createScrollR | |
SearchResponse<ObjectNode> searchResponse; | ||
|
||
try { | ||
searchResponse = elasticsearchClient.search(SearchRequest.of(request -> request | ||
searchResponse = elasticsearchClientRefresher.get() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this change is somewhat intrusive to the code, and we don't currently have integration tests for this source, just curious what testing you did to verify no regression? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. I run a smoke test with pipeline:
|
||
.search(SearchRequest.of(request -> request | ||
.scroll(Time.of(time -> time.time(createScrollRequest.getScrollTime()))) | ||
.sort(SortOptions.of(sortOptionsBuilder -> sortOptionsBuilder.doc(ScoreSort.of(scoreSort -> scoreSort.order(SortOrder.Asc))))) | ||
.size(createScrollRequest.getSize()) | ||
|
@@ -182,7 +190,8 @@ public CreateScrollResponse createScroll(final CreateScrollRequest createScrollR | |
public SearchScrollResponse searchWithScroll(final SearchScrollRequest searchScrollRequest) { | ||
SearchResponse<ObjectNode> searchResponse; | ||
try { | ||
searchResponse = elasticsearchClient.scroll(ScrollRequest.of(request -> request | ||
searchResponse = elasticsearchClientRefresher.get() | ||
.scroll(ScrollRequest.of(request -> request | ||
.scrollId(searchScrollRequest.getScrollId()) | ||
.scroll(Time.of(time -> time.time(searchScrollRequest.getScrollTime())))), ObjectNode.class); | ||
} catch (final ElasticsearchException e) { | ||
|
@@ -202,7 +211,8 @@ public SearchScrollResponse searchWithScroll(final SearchScrollRequest searchScr | |
@Override | ||
public void deleteScroll(DeleteScrollRequest deleteScrollRequest) { | ||
try { | ||
final ClearScrollResponse clearScrollResponse = elasticsearchClient.clearScroll(ClearScrollRequest.of(request -> request.scrollId(deleteScrollRequest.getScrollId()))); | ||
final ClearScrollResponse clearScrollResponse = elasticsearchClientRefresher.get() | ||
.clearScroll(ClearScrollRequest.of(request -> request.scrollId(deleteScrollRequest.getScrollId()))); | ||
if (clearScrollResponse.succeeded()) { | ||
LOG.debug("Successfully deleted scroll context with id {}", deleteScrollRequest.getScrollId()); | ||
} else { | ||
|
@@ -236,14 +246,15 @@ public SearchWithSearchAfterResults searchWithoutSearchContext(final NoSearchCon | |
} | ||
|
||
@Override | ||
public Object getClient() { | ||
return elasticsearchClient; | ||
public PluginComponentRefresher<ElasticsearchClient, OpenSearchSourceConfiguration> getClientRefresher() { | ||
return elasticsearchClientRefresher; | ||
} | ||
|
||
private SearchWithSearchAfterResults searchWithSearchAfter(final SearchRequest searchRequest) { | ||
|
||
try { | ||
final SearchResponse<ObjectNode> searchResponse = elasticsearchClient.search(searchRequest, ObjectNode.class); | ||
final SearchResponse<ObjectNode> searchResponse = elasticsearchClientRefresher.get() | ||
.search(searchRequest, ObjectNode.class); | ||
|
||
final List<Event> documents = getDocumentsFromResponse(searchResponse); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these locks needed just in case we had multiple worker threads going at once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. when secrets are configured there are at least two threads accessing this: pipeline worker thread and secret polling thread