Fixes #1901 - Add support for storing last successful event timestamp to resume sending events on server shutdown (#1938)

This commit is contained in:
Suresh Srinivas 2021-12-27 13:36:43 -08:00 committed by GitHub
parent 5f479fb1c6
commit 4863e02a68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 169 additions and 56 deletions

View File

@ -520,7 +520,8 @@ public abstract class EntityRepository<T> {
newVersion = Math.round((oldVersion + 0.1) * 10.0) / 10.0;
}
LOG.info(
"{}->{} - Fields added {}, updated {}, deleted {}",
"{} {}->{} - Fields added {}, updated {}, deleted {}",
original.getId(),
oldVersion,
newVersion,
changeDescription.getFieldsAdded(),
@ -531,7 +532,7 @@ public abstract class EntityRepository<T> {
return !newVersion.equals(oldVersion);
}
private boolean fieldsChanged() {
public boolean fieldsChanged() {
return !changeDescription.getFieldsAdded().isEmpty()
|| !changeDescription.getFieldsUpdated().isEmpty()
|| !changeDescription.getFieldsDeleted().isEmpty();

View File

@ -119,7 +119,8 @@ public class WebhookRepository extends EntityRepository<Webhook> {
}
public void addWebhookPublisher(Webhook webhook) {
if (!webhook.getEnabled()) { // Only add webhook that is enabled
if (!webhook.getEnabled()) { // Only add webhook that is enabled for publishing events
webhook.setStatus(Status.NOT_STARTED);
return;
}
WebhookPublisher publisher = new WebhookPublisher(webhook);
@ -130,7 +131,7 @@ public class WebhookRepository extends EntityRepository<Webhook> {
}
public void updateWebhookPublisher(Webhook webhook) throws InterruptedException {
if (webhook.getEnabled()) { // Only add webhook that is enabled
if (webhook.getEnabled()) { // Only add webhook that is enabled for publishing
// If there was a previous webhook either in disabled state or stopped due
// to errors, update it and restart publishing
WebhookPublisher previousPublisher = getPublisher(webhook.getId());
@ -142,7 +143,7 @@ public class WebhookRepository extends EntityRepository<Webhook> {
// Update the existing publisher
Status status = previousPublisher.getWebhook().getStatus();
previousPublisher.updateWebhook(webhook);
if (status != Status.SUCCESS && status != Status.AWAITING_RETRY) {
if (status != Status.STARTED && status != Status.AWAITING_RETRY) {
// Restart the previously stopped publisher (in states notStarted, error, retryLimitReached)
BatchEventProcessor<ChangeEventHolder> processor = EventPubSub.addEventHandler(previousPublisher);
previousPublisher.setProcessor(processor);
@ -160,7 +161,7 @@ public class WebhookRepository extends EntityRepository<Webhook> {
publisher.getProcessor().halt();
publisher.awaitShutdown();
EventPubSub.removeProcessor(publisher.getProcessor());
LOG.info("Webhook publisher deleted {}", publisher.getWebhook());
LOG.info("Webhook publisher deleted for {}", publisher.getWebhook().getName());
}
webhookPublisherMap.remove(id);
}
@ -367,9 +368,9 @@ public class WebhookRepository extends EntityRepository<Webhook> {
// 2xx response means call back is successful
if (response.getStatus() >= 200 && response.getStatus() < 300) { // All 2xx responses
batch.clear();
if (webhook.getStatus() != Status.SUCCESS) {
webhook.getFailureDetails().setLastSuccessfulAt(changeEventHolder.get().getDateTime().getTime());
setStatus(Status.SUCCESS, null, null, null, null);
webhook.getFailureDetails().setLastSuccessfulAt(changeEventHolder.get().getDateTime().getTime());
if (webhook.getStatus() != Status.STARTED) {
setStatus(Status.STARTED, null, null, null, null);
}
// 3xx response/redirection is not allowed for callback. Set the webhook state as in error
} else if (response.getStatus() >= 300 && response.getStatus() < 400) {
@ -495,16 +496,31 @@ public class WebhookRepository extends EntityRepository<Webhook> {
@Override
public void entitySpecificUpdate() throws IOException {
recordChange("enabled", original.getEntity().getEnabled(), updated.getEntity().getEnabled());
recordChange("status", original.getEntity().getStatus(), updated.getEntity().getStatus());
recordChange("endPoint", original.getEntity().getEndpoint(), updated.getEntity().getEndpoint());
recordChange("batchSize", original.getEntity().getBatchSize(), updated.getEntity().getBatchSize());
recordChange(
"failureDetails",
original.getEntity().getFailureDetails(),
updated.getEntity().getFailureDetails(),
true,
failureDetailsMatch);
Webhook origWebhook = original.getEntity();
Webhook updatedWebhook = updated.getEntity();
recordChange("enabled", origWebhook.getEnabled(), updatedWebhook.getEnabled());
recordChange("status", origWebhook.getStatus(), updatedWebhook.getStatus());
recordChange("endPoint", origWebhook.getEndpoint(), updatedWebhook.getEndpoint());
recordChange("batchSize", origWebhook.getBatchSize(), updatedWebhook.getBatchSize());
if (fieldsChanged()) {
// If updating the other fields, opportunistically use it to capture failure details
WebhookPublisher publisher = WebhookRepository.this.getPublisher(origWebhook.getId());
if (publisher != null && updatedWebhook != publisher.getWebhook()) {
updatedWebhook
.withStatus(publisher.getWebhook().getStatus())
.withFailureDetails(publisher.getWebhook().getFailureDetails());
if (updatedWebhook.getEnabled() == false) {
updatedWebhook.setStatus(Status.NOT_STARTED);
}
}
recordChange(
"failureDetails",
origWebhook.getFailureDetails(),
updatedWebhook.getFailureDetails(),
true,
failureDetailsMatch);
}
}
}
}

View File

@ -234,7 +234,7 @@ public class WebhookResource {
throws IOException {
SecurityUtil.checkAdminOrBotRole(authorizer, securityContext);
Webhook webhook = getWebhook(securityContext, create);
webhook.setStatus(webhook.getEnabled() ? Status.SUCCESS : Status.NOT_STARTED);
webhook.setStatus(webhook.getEnabled() ? Status.STARTED : Status.NOT_STARTED);
webhook = dao.create(uriInfo, webhook);
dao.addWebhookPublisher(webhook);
return Response.created(webhook.getHref()).entity(webhook).build();
@ -259,7 +259,7 @@ public class WebhookResource {
// SecurityUtil.checkAdminOrBotRole(authorizer, securityContext);
// Table table = getTable(securityContext, create);
Webhook webhook = getWebhook(securityContext, create);
webhook.setStatus(webhook.getEnabled() ? Status.SUCCESS : Status.NOT_STARTED);
webhook.setStatus(webhook.getEnabled() ? Status.STARTED : Status.NOT_STARTED);
PutResponse<Webhook> putResponse = dao.createOrUpdate(uriInfo, webhook);
dao.updateWebhookPublisher(webhook);
return putResponse.toResponse();

View File

@ -109,9 +109,9 @@ public final class EntityUtil {
public static final BiPredicate<MlFeature, MlFeature> mlFeatureMatch = MlFeature::equals;
public static final BiPredicate<MlHyperParameter, MlHyperParameter> mlHyperParameterMatch = MlHyperParameter::equals;
public static final BiPredicate<FailureDetails, FailureDetails> failureDetailsMatch =
(failureDetails1, failureDetails2) ->
failureDetails1.getLastFailedAt().equals(failureDetails2.getLastFailedAt()) &&
failureDetails1.getLastSuccessfulAt().equals(failureDetails2.getLastSuccessfulAt());
(failureDetails1, failureDetails2) ->
Objects.equals(failureDetails2.getLastFailedAt(), failureDetails1.getLastFailedAt())
&& Objects.equals(failureDetails2.getLastSuccessfulAt(), failureDetails1.getLastSuccessfulAt());
private EntityUtil() {}

View File

@ -60,16 +60,15 @@
"type": "string"
},
"status": {
"description": "Status is `notStarted`, when webhook was created with `enabled` set to false and it never started publishing events. Status is `success` on 200 response to notifications. Status is `error` on bad callback URL, connection failures, and `1xx`, `3xx` response. Status is `awaitingRetry` when previous attempt at delivery timed out or received `4xx`, `5xx` response. Status is `retryLimitReached` after all retries fail.",
"description": "Status is `notStarted`, when webhook was created with `enabled` set to false and it never started publishing events. Status is `started` when webhook is normally functioning and 200 OK response was received for callback notification. Status is `failed` on bad callback URL, connection failures, `1xx`, and `3xx` response was received for callback notification. Status is `awaitingRetry` when previous attempt at callback timed out or received `4xx`, `5xx` response. Status is `retryLimitReached` after all retries fail.",
"type": "string",
"enum": [
"notStarted",
"success",
"started",
"failed",
"awaitingRetry",
"retryLimitReached"
],
"default": "success"
]
},
"failureDetails": {
"description": "Failure details are set only when `status` is not `success`",

View File

@ -31,6 +31,7 @@ public class WebhookCallbackResource {
public static final Logger LOG = LoggerFactory.getLogger(WebhookCallbackResource.class);
private final AtomicInteger counter = new AtomicInteger();
private volatile long counterStartTime;
private volatile long counterLatestTime;
private final ConcurrentLinkedQueue<ChangeEvent> changeEvents = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<ChangeEvent> changeEventsSlowServer = new ConcurrentLinkedQueue<>();
@ -60,14 +61,27 @@ public class WebhookCallbackResource {
if (counter.get() == 0) {
counterStartTime = events.getData().get(0).getDateTime().getTime();
}
counterLatestTime = events.getData().get(0).getDateTime().getTime();
counter.incrementAndGet();
LOG.info("callback /counter received event. Current count {}", counter.get());
return Response.ok().build();
}
public int getCount() { return counter.get(); }
public void resetCount() { counter.set(0); }
public long getCountStartTime() { return counterStartTime; }
public int getCount() {
return counter.get();
}
public void resetCount() {
counter.set(0);
}
public long getCounterStartTime() {
return counterStartTime;
}
public long getCounterLatestTime() {
return counterLatestTime;
}
/** Webhook endpoint that immediately responds to callback. The events received are ignored */
@POST

View File

@ -30,7 +30,6 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import javax.ws.rs.core.Response;
import org.apache.http.client.HttpResponseException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.events.CreateWebhook;
import org.openmetadata.catalog.jdbi3.WebhookRepository.WebhookEntityInterface;
@ -46,6 +45,7 @@ import org.openmetadata.catalog.type.FieldChange;
import org.openmetadata.catalog.type.Webhook;
import org.openmetadata.catalog.type.Webhook.Status;
import org.openmetadata.catalog.util.EntityInterface;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.TestUtils;
import org.openmetadata.catalog.util.TestUtils.UpdateType;
@ -65,57 +65,71 @@ public class WebhookResourceTest extends EntityResourceTest<Webhook> {
@Test
public void post_webhookEnabledStateChange() throws URISyntaxException, IOException, InterruptedException {
// Disabled webhook will not start webhook publisher
//
// Create webhook in disabled state. It will not start webhook publisher
//
LOG.info("creating webhook in disabled state");
webhookCallbackResource.resetCount();
String uri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/counter";
CreateWebhook create =
createRequest("disabledWebhook", "", "", null).withEnabled(false).withEndpoint(URI.create(uri));
CreateWebhook create = createRequest("counter", "", "", null).withEnabled(false).withEndpoint(URI.create(uri));
Webhook webhook = createAndCheckEntity(create, adminAuthHeaders());
assertEquals(Status.NOT_STARTED, webhook.getStatus());
Webhook getWebhook = getEntity(webhook.getId(), adminAuthHeaders());
assertEquals(Status.NOT_STARTED, getWebhook.getStatus());
assertEquals(0, webhookCallbackResource.getCount());
//
// Now enable the webhook
//
LOG.info("Enabling webhook");
int counter = webhookCallbackResource.getCount();
ChangeDescription change = getChangeDescription(webhook.getVersion());
change.getFieldsUpdated().add(new FieldChange().withName("enabled").withOldValue(false).withNewValue(true));
change
.getFieldsUpdated()
.add(new FieldChange().withName("status").withOldValue(Status.NOT_STARTED).withNewValue(Status.SUCCESS));
change
.getFieldsUpdated()
.add(new FieldChange().withName("batchSize").withOldValue(10).withNewValue(50));
.add(new FieldChange().withName("status").withOldValue(Status.NOT_STARTED).withNewValue(Status.STARTED));
change.getFieldsUpdated().add(new FieldChange().withName("batchSize").withOldValue(10).withNewValue(50));
create.withEnabled(true).withBatchSize(50);
webhook = updateAndCheckEntity(create, Response.Status.OK, adminAuthHeaders(), UpdateType.MINOR_UPDATE, change);
assertEquals(Status.SUCCESS, webhook.getStatus());
assertEquals(Status.STARTED, webhook.getStatus());
getWebhook = getEntity(webhook.getId(), adminAuthHeaders());
assertEquals(Status.SUCCESS, getWebhook.getStatus());
assertEquals(Status.STARTED, getWebhook.getStatus());
// Change event for webhook enabling is received to ensure the call back has started
// Ensure the call back notification has started
int iterations = 0;
while (webhookCallbackResource.getCount() <= counter && iterations < 100) {
Thread.sleep(10);
iterations++;
}
assertEquals(counter + 1, webhookCallbackResource.getCount());
long lastSuccessfulEventTime = webhookCallbackResource.getCounterLatestTime();
FailureDetails failureDetails = new FailureDetails().withLastSuccessfulAt(lastSuccessfulEventTime);
// Disable the webhook and ensure it is disabled
//
// Disable the webhook and ensure notification is disabled
//
LOG.info("Disabling webhook");
create.withEnabled(false);
change = getChangeDescription(webhook.getVersion());
change = getChangeDescription(getWebhook.getVersion());
change
.getFieldsAdded()
.add(new FieldChange().withName("failureDetails").withNewValue(JsonUtils.pojoToJson(failureDetails)));
change.getFieldsUpdated().add(new FieldChange().withName("enabled").withOldValue(true).withNewValue(false));
change
.getFieldsUpdated()
.add(new FieldChange().withName("status").withOldValue(Status.SUCCESS).withNewValue(Status.NOT_STARTED));
.add(new FieldChange().withName("status").withOldValue(Status.STARTED).withNewValue(Status.NOT_STARTED));
// Disabled webhook state is NOT_STARTED
getWebhook = updateAndCheckEntity(create, Response.Status.OK, adminAuthHeaders(), UpdateType.MINOR_UPDATE, change);
assertEquals(Status.NOT_STARTED, getWebhook.getStatus());
// Disabled webhook state also records last successful time when event was sent
getWebhook = getEntity(webhook.getId(), adminAuthHeaders());
assertEquals(Status.NOT_STARTED, getWebhook.getStatus());
assertEquals(webhookCallbackResource.getCounterStartTime(), getWebhook.getFailureDetails().getLastSuccessfulAt());
// Ensure callback is disabled and no further events are received
// Ensure callback back notification is disabled with no new events
iterations = 0;
while (iterations < 100) {
Thread.sleep(10);
@ -129,7 +143,7 @@ public class WebhookResourceTest extends EntityResourceTest<Webhook> {
@Test
public void put_updateEndpointURL() throws URISyntaxException, IOException, InterruptedException {
CreateWebhook create =
createRequest("replaceURL", "", "", null).withEndpoint(URI.create("http://invalidUnknowHost"));
createRequest("counter", "", "", null).withEnabled(true).withEndpoint(URI.create("http://invalidUnknowHost"));
Webhook webhook = createAndCheckEntity(create, adminAuthHeaders());
// Wait for webhook to be marked as failed
@ -149,7 +163,6 @@ public class WebhookResourceTest extends EntityResourceTest<Webhook> {
String baseUri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook/counter";
create = create.withEndpoint(URI.create(baseUri));
ChangeDescription change = getChangeDescription(getWebhook.getVersion());
change.getFieldsDeleted().add(new FieldChange().withName("failureDetails").withOldValue(failureDetails));
change
.getFieldsUpdated()
.add(
@ -159,9 +172,8 @@ public class WebhookResourceTest extends EntityResourceTest<Webhook> {
.withNewValue(create.getEndpoint()));
change
.getFieldsUpdated()
.add(new FieldChange().withName("status").withOldValue(Status.FAILED).withNewValue(Status.SUCCESS));
.add(new FieldChange().withName("status").withOldValue(Status.FAILED).withNewValue(Status.STARTED));
webhook = updateAndCheckEntity(create, Response.Status.OK, adminAuthHeaders(), UpdateType.MINOR_UPDATE, change);
assertEquals(Status.SUCCESS, webhook.getStatus());
deleteEntity(webhook.getId(), adminAuthHeaders());
}
@ -175,7 +187,8 @@ public class WebhookResourceTest extends EntityResourceTest<Webhook> {
.withDescription(description)
.withEventFilters(ALL_EVENTS_FILTER)
.withEndpoint(URI.create(uri))
.withBatchSize(100);
.withBatchSize(100)
.withEnabled(false);
}
@Override
@ -218,9 +231,7 @@ public class WebhookResourceTest extends EntityResourceTest<Webhook> {
public void startWebhookSubscription() throws IOException, URISyntaxException {
// Valid webhook callback
String baseUri = "http://localhost:" + APP.getLocalPort() + "/api/v1/test/webhook";
CreateWebhook createWebhook =
createRequest("validWebhook", "validWebhook", "", null).withEndpoint(URI.create(baseUri));
createEntity(createWebhook, adminAuthHeaders());
createWebhook("validWebhook", baseUri);
}
/** Start webhook subscription for given entity and various event types */
@ -323,13 +334,13 @@ public class WebhookResourceTest extends EntityResourceTest<Webhook> {
public Webhook createWebhook(String name, String uri, List<EventFilter> filters)
throws URISyntaxException, IOException {
CreateWebhook createWebhook =
createRequest(name, "", "", null).withEndpoint(URI.create(uri)).withEventFilters(filters);
createRequest(name, "", "", null).withEndpoint(URI.create(uri)).withEventFilters(filters).withEnabled(true);
return createAndCheckEntity(createWebhook, adminAuthHeaders());
}
public void assertWebhookStatusSuccess(String name) throws HttpResponseException {
Webhook webhook = getEntityByName(name, "", adminAuthHeaders());
assertEquals(Status.SUCCESS, webhook.getStatus());
assertEquals(Status.STARTED, webhook.getStatus());
assertNull(webhook.getFailureDetails());
}

View File

@ -3811,6 +3811,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@ -5605,6 +5610,21 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
find-versions@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965"
integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==
dependencies:
semver-regex "^3.1.2"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@ -6345,6 +6365,22 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
husky@^4.3.0:
version "4.3.8"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d"
integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==
dependencies:
chalk "^4.0.0"
ci-info "^2.0.0"
compare-versions "^3.6.0"
cosmiconfig "^7.0.0"
find-versions "^4.0.0"
opencollective-postinstall "^2.0.2"
pkg-dir "^5.0.0"
please-upgrade-node "^3.2.0"
slash "^3.0.0"
which-pm-runs "^1.0.0"
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -7913,6 +7949,13 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@ -8953,6 +8996,11 @@ openapi-sampler@1.0.0-beta.15:
dependencies:
json-pointer "^0.6.0"
opencollective-postinstall@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
opener@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
@ -9070,6 +9118,13 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
p-map@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
@ -9330,6 +9385,13 @@ pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
pkg-dir@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760"
integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==
dependencies:
find-up "^5.0.0"
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
@ -10700,6 +10762,11 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
semver-regex@^3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.3.tgz#b2bcc6f97f63269f286994e297e229b6245d0dc3"
integrity sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -12649,6 +12716,11 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-pm-runs@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"