Implement created alarms limiting

This commit is contained in:
Viacheslav Klimov 2021-05-11 16:17:37 +03:00 committed by Andrew Shvayka
parent a7d1b54bb4
commit 9873115d8b
16 changed files with 84 additions and 24 deletions

View File

@ -130,3 +130,7 @@ DO $$
END; END;
$$; $$;
ALTER TABLE api_usage_state
ADD COLUMN IF NOT EXISTS alarm_exec VARCHAR(32);
UPDATE api_usage_state SET alarm_exec = 'ENABLED' WHERE alarm_exec IS NULL;

View File

@ -106,6 +106,8 @@ public abstract class BaseApiUsageState {
return apiUsageState.getEmailExecState(); return apiUsageState.getEmailExecState();
case SMS: case SMS:
return apiUsageState.getSmsExecState(); return apiUsageState.getSmsExecState();
case ALARM:
return apiUsageState.getAlarmExecState();
default: default:
return ApiUsageStateValue.ENABLED; return ApiUsageStateValue.ENABLED;
} }
@ -132,6 +134,9 @@ public abstract class BaseApiUsageState {
case SMS: case SMS:
apiUsageState.setSmsExecState(value); apiUsageState.setSmsExecState(value);
break; break;
case ALARM:
apiUsageState.setAlarmExecState(value);
break;
} }
return !currentValue.equals(value); return !currentValue.equals(value);
} }

View File

@ -309,6 +309,8 @@ public class DefaultMailService implements MailService {
case EMAIL: case EMAIL:
case SMS: case SMS:
return "send"; return "send";
case ALARM:
return "create";
default: default:
throw new RuntimeException("Not implemented!"); throw new RuntimeException("Not implemented!");
} }
@ -327,6 +329,8 @@ public class DefaultMailService implements MailService {
case EMAIL: case EMAIL:
case SMS: case SMS:
return "sent"; return "sent";
case ALARM:
return "created";
default: default:
throw new RuntimeException("Not implemented!"); throw new RuntimeException("Not implemented!");
} }

View File

@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
@ -43,6 +44,8 @@ import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@ -58,12 +61,18 @@ import java.util.Optional;
public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService { public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService {
private final AlarmService alarmService; private final AlarmService alarmService;
private final TbApiUsageClient apiUsageClient;
private final TbApiUsageStateService apiUsageStateService;
public DefaultAlarmSubscriptionService(TbClusterService clusterService, public DefaultAlarmSubscriptionService(TbClusterService clusterService,
PartitionService partitionService, PartitionService partitionService,
AlarmService alarmService) { AlarmService alarmService,
TbApiUsageClient apiUsageClient,
TbApiUsageStateService apiUsageStateService) {
super(clusterService, partitionService); super(clusterService, partitionService);
this.alarmService = alarmService; this.alarmService = alarmService;
this.apiUsageClient = apiUsageClient;
this.apiUsageStateService = apiUsageStateService;
} }
@Autowired(required = false) @Autowired(required = false)
@ -78,10 +87,19 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
@Override @Override
public Alarm createOrUpdateAlarm(Alarm alarm) { public Alarm createOrUpdateAlarm(Alarm alarm) {
AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm,
() -> {
if (!apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()) {
throw new IllegalStateException("Alarms creation is disabled due to API limits");
}
},
() -> {});
if (result.isSuccessful()) { if (result.isSuccessful()) {
onAlarmUpdated(result); onAlarmUpdated(result);
} }
if (result.isCreated()) {
apiUsageClient.report(alarm.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT);
}
return result.getAlarm(); return result.getAlarm();
} }

View File

@ -26,17 +26,21 @@ import java.util.List;
public class AlarmOperationResult { public class AlarmOperationResult {
private final Alarm alarm; private final Alarm alarm;
private final boolean successful; private final boolean successful;
private final boolean created;
private final List<EntityId> propagatedEntitiesList; private final List<EntityId> propagatedEntitiesList;
public AlarmOperationResult(Alarm alarm, boolean successful) { public AlarmOperationResult(Alarm alarm, boolean successful) {
this.alarm = alarm; this(alarm, successful, Collections.emptyList());
this.successful = successful;
this.propagatedEntitiesList = Collections.emptyList();
} }
public AlarmOperationResult(Alarm alarm, boolean successful, List<EntityId> propagatedEntitiesList) { public AlarmOperationResult(Alarm alarm, boolean successful, List<EntityId> propagatedEntitiesList) {
this(alarm, successful, false, propagatedEntitiesList);
}
public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List<EntityId> propagatedEntitiesList) {
this.alarm = alarm; this.alarm = alarm;
this.successful = successful; this.successful = successful;
this.created = created;
this.propagatedEntitiesList = propagatedEntitiesList; this.propagatedEntitiesList = propagatedEntitiesList;
} }
} }

View File

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataPageLink;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.msg.queue.TbCallback;
import java.util.Collection; import java.util.Collection;
@ -41,6 +42,8 @@ public interface AlarmService {
AlarmOperationResult createOrUpdateAlarm(Alarm alarm); AlarmOperationResult createOrUpdateAlarm(Alarm alarm);
AlarmOperationResult createOrUpdateAlarm(Alarm alarm, Runnable onAlarmCreation, Runnable onAlarmUpdate);
AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId);
ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);

View File

@ -23,7 +23,8 @@ public enum ApiFeature {
RE("ruleEngineApiState", "Rule Engine execution"), RE("ruleEngineApiState", "Rule Engine execution"),
JS("jsExecutionApiState", "JavaScript functions execution"), JS("jsExecutionApiState", "JavaScript functions execution"),
EMAIL("emailApiState", "Email messages"), EMAIL("emailApiState", "Email messages"),
SMS("smsApiState", "SMS messages"); SMS("smsApiState", "SMS messages"),
ALARM("alarmApiState", "Created alarms");
@Getter @Getter
private final String apiStateKey; private final String apiStateKey;

View File

@ -25,13 +25,16 @@ public enum ApiUsageRecordKey {
RE_EXEC_COUNT(ApiFeature.RE, "ruleEngineExecutionCount", "ruleEngineExecutionLimit"), RE_EXEC_COUNT(ApiFeature.RE, "ruleEngineExecutionCount", "ruleEngineExecutionLimit"),
JS_EXEC_COUNT(ApiFeature.JS, "jsExecutionCount", "jsExecutionLimit"), JS_EXEC_COUNT(ApiFeature.JS, "jsExecutionCount", "jsExecutionLimit"),
EMAIL_EXEC_COUNT(ApiFeature.EMAIL, "emailCount", "emailLimit"), EMAIL_EXEC_COUNT(ApiFeature.EMAIL, "emailCount", "emailLimit"),
SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit"); SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit"),
CREATED_ALARMS_COUNT(ApiFeature.ALARM, "createdAlarmsCount", "createdAlarmsLimit");
private static final ApiUsageRecordKey[] JS_RECORD_KEYS = {JS_EXEC_COUNT}; private static final ApiUsageRecordKey[] JS_RECORD_KEYS = {JS_EXEC_COUNT};
private static final ApiUsageRecordKey[] RE_RECORD_KEYS = {RE_EXEC_COUNT}; private static final ApiUsageRecordKey[] RE_RECORD_KEYS = {RE_EXEC_COUNT};
private static final ApiUsageRecordKey[] DB_RECORD_KEYS = {STORAGE_DP_COUNT}; private static final ApiUsageRecordKey[] DB_RECORD_KEYS = {STORAGE_DP_COUNT};
private static final ApiUsageRecordKey[] TRANSPORT_RECORD_KEYS = {TRANSPORT_MSG_COUNT, TRANSPORT_DP_COUNT}; private static final ApiUsageRecordKey[] TRANSPORT_RECORD_KEYS = {TRANSPORT_MSG_COUNT, TRANSPORT_DP_COUNT};
private static final ApiUsageRecordKey[] EMAIL_RECORD_KEYS = {EMAIL_EXEC_COUNT}; private static final ApiUsageRecordKey[] EMAIL_RECORD_KEYS = {EMAIL_EXEC_COUNT};
private static final ApiUsageRecordKey[] SMS_RECORD_KEYS = {SMS_EXEC_COUNT}; private static final ApiUsageRecordKey[] SMS_RECORD_KEYS = {SMS_EXEC_COUNT};
private static final ApiUsageRecordKey[] ALARM_RECORD_KEYS = {CREATED_ALARMS_COUNT};
@Getter @Getter
private final ApiFeature apiFeature; private final ApiFeature apiFeature;
@ -60,6 +63,8 @@ public enum ApiUsageRecordKey {
return EMAIL_RECORD_KEYS; return EMAIL_RECORD_KEYS;
case SMS: case SMS:
return SMS_RECORD_KEYS; return SMS_RECORD_KEYS;
case ALARM:
return ALARM_RECORD_KEYS;
default: default:
return new ApiUsageRecordKey[]{}; return new ApiUsageRecordKey[]{};
} }

View File

@ -25,34 +25,21 @@ import org.thingsboard.server.common.data.id.ApiUsageStateId;
@ToString @ToString
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Getter
@Setter
public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenantId { public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenantId {
private static final long serialVersionUID = 8250339805336035966L; private static final long serialVersionUID = 8250339805336035966L;
@Getter
@Setter
private TenantId tenantId; private TenantId tenantId;
@Getter
@Setter
private EntityId entityId; private EntityId entityId;
@Getter
@Setter
private ApiUsageStateValue transportState; private ApiUsageStateValue transportState;
@Getter
@Setter
private ApiUsageStateValue dbStorageState; private ApiUsageStateValue dbStorageState;
@Getter
@Setter
private ApiUsageStateValue reExecState; private ApiUsageStateValue reExecState;
@Getter
@Setter
private ApiUsageStateValue jsExecState; private ApiUsageStateValue jsExecState;
@Getter
@Setter
private ApiUsageStateValue emailExecState; private ApiUsageStateValue emailExecState;
@Getter
@Setter
private ApiUsageStateValue smsExecState; private ApiUsageStateValue smsExecState;
private ApiUsageStateValue alarmExecState;
public ApiUsageState() { public ApiUsageState() {
super(); super();
@ -72,6 +59,7 @@ public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenan
this.jsExecState = ur.getJsExecState(); this.jsExecState = ur.getJsExecState();
this.emailExecState = ur.getEmailExecState(); this.emailExecState = ur.getEmailExecState();
this.smsExecState = ur.getSmsExecState(); this.smsExecState = ur.getSmsExecState();
this.alarmExecState = ur.getAlarmExecState();
} }
public boolean isTransportEnabled() { public boolean isTransportEnabled() {
@ -97,4 +85,8 @@ public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenan
public boolean isSmsSendEnabled(){ public boolean isSmsSendEnabled(){
return !ApiUsageStateValue.DISABLED.equals(smsExecState); return !ApiUsageStateValue.DISABLED.equals(smsExecState);
} }
public boolean isAlarmCreationEnabled() {
return alarmExecState != ApiUsageStateValue.DISABLED;
}
} }

View File

@ -50,6 +50,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private int maxRuleNodeExecutionsPerMessage; private int maxRuleNodeExecutionsPerMessage;
private long maxEmails; private long maxEmails;
private long maxSms; private long maxSms;
private long maxCreatedAlarms;
private int defaultStorageTtlDays; private int defaultStorageTtlDays;
@ -72,6 +73,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
return maxEmails; return maxEmails;
case SMS_EXEC_COUNT: case SMS_EXEC_COUNT:
return maxSms; return maxSms;
case CREATED_ALARMS_COUNT:
return maxCreatedAlarms;
} }
return 0L; return 0L;
} }

View File

@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -65,6 +66,7 @@ import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -102,6 +104,11 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
@Override @Override
public AlarmOperationResult createOrUpdateAlarm(Alarm alarm) { public AlarmOperationResult createOrUpdateAlarm(Alarm alarm) {
return createOrUpdateAlarm(alarm, () -> {}, () -> {});
}
@Override
public AlarmOperationResult createOrUpdateAlarm(Alarm alarm, Runnable onAlarmCreation, Runnable onAlarmUpdate) {
alarmDataValidator.validate(alarm, Alarm::getTenantId); alarmDataValidator.validate(alarm, Alarm::getTenantId);
try { try {
if (alarm.getStartTs() == 0L) { if (alarm.getStartTs() == 0L) {
@ -114,11 +121,14 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
if (alarm.getId() == null) { if (alarm.getId() == null) {
Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get(); Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get();
if (existing == null || existing.getStatus().isCleared()) { if (existing == null || existing.getStatus().isCleared()) {
onAlarmCreation.run();
return createAlarm(alarm); return createAlarm(alarm);
} else { } else {
onAlarmUpdate.run();
return updateAlarm(existing, alarm); return updateAlarm(existing, alarm);
} }
} else { } else {
onAlarmUpdate.run();
return updateAlarm(alarm).get(); return updateAlarm(alarm).get();
} }
} catch (ExecutionException | InterruptedException e) { } catch (ExecutionException | InterruptedException e) {
@ -159,7 +169,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
log.debug("New Alarm : {}", alarm); log.debug("New Alarm : {}", alarm);
Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm);
List<EntityId> propagatedEntitiesList = createAlarmRelations(saved); List<EntityId> propagatedEntitiesList = createAlarmRelations(saved);
return new AlarmOperationResult(saved, true, propagatedEntitiesList); return new AlarmOperationResult(saved, true, true, propagatedEntitiesList);
} }
private List<EntityId> createAlarmRelations(Alarm alarm) throws InterruptedException, ExecutionException { private List<EntityId> createAlarmRelations(Alarm alarm) throws InterruptedException, ExecutionException {

View File

@ -465,6 +465,7 @@ public class ModelConstants {
public static final String API_USAGE_STATE_JS_EXEC_COLUMN = "js_exec"; public static final String API_USAGE_STATE_JS_EXEC_COLUMN = "js_exec";
public static final String API_USAGE_STATE_EMAIL_EXEC_COLUMN = "email_exec"; public static final String API_USAGE_STATE_EMAIL_EXEC_COLUMN = "email_exec";
public static final String API_USAGE_STATE_SMS_EXEC_COLUMN = "sms_exec"; public static final String API_USAGE_STATE_SMS_EXEC_COLUMN = "sms_exec";
public static final String API_USAGE_STATE_ALARM_EXEC_COLUMN = "alarm_exec";
/** /**
* Resource constants. * Resource constants.

View File

@ -69,6 +69,9 @@ public class ApiUsageStateEntity extends BaseSqlEntity<ApiUsageState> implements
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = ModelConstants.API_USAGE_STATE_SMS_EXEC_COLUMN) @Column(name = ModelConstants.API_USAGE_STATE_SMS_EXEC_COLUMN)
private ApiUsageStateValue smsExecState = ApiUsageStateValue.ENABLED; private ApiUsageStateValue smsExecState = ApiUsageStateValue.ENABLED;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.API_USAGE_STATE_ALARM_EXEC_COLUMN)
private ApiUsageStateValue alarmExecState = ApiUsageStateValue.ENABLED;
public ApiUsageStateEntity() { public ApiUsageStateEntity() {
} }
@ -91,6 +94,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity<ApiUsageState> implements
this.jsExecState = ur.getJsExecState(); this.jsExecState = ur.getJsExecState();
this.emailExecState = ur.getEmailExecState(); this.emailExecState = ur.getEmailExecState();
this.smsExecState = ur.getSmsExecState(); this.smsExecState = ur.getSmsExecState();
this.alarmExecState = ur.getAlarmExecState();
} }
@Override @Override
@ -109,6 +113,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity<ApiUsageState> implements
ur.setJsExecState(jsExecState); ur.setJsExecState(jsExecState);
ur.setEmailExecState(emailExecState); ur.setEmailExecState(emailExecState);
ur.setSmsExecState(smsExecState); ur.setSmsExecState(smsExecState);
ur.setAlarmExecState(alarmExecState);
return ur; return ur;
} }

View File

@ -90,6 +90,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED);
apiUsageStateValidator.validate(apiUsageState, ApiUsageState::getTenantId); apiUsageStateValidator.validate(apiUsageState, ApiUsageState::getTenantId);
ApiUsageState saved = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); ApiUsageState saved = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState);
@ -107,6 +108,8 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
new StringDataEntry(ApiFeature.EMAIL.getApiStateKey(), ApiUsageStateValue.ENABLED.name()))); new StringDataEntry(ApiFeature.EMAIL.getApiStateKey(), ApiUsageStateValue.ENABLED.name())));
apiUsageStates.add(new BasicTsKvEntry(saved.getCreatedTime(), apiUsageStates.add(new BasicTsKvEntry(saved.getCreatedTime(),
new StringDataEntry(ApiFeature.SMS.getApiStateKey(), ApiUsageStateValue.ENABLED.name()))); new StringDataEntry(ApiFeature.SMS.getApiStateKey(), ApiUsageStateValue.ENABLED.name())));
apiUsageStates.add(new BasicTsKvEntry(saved.getCreatedTime(),
new StringDataEntry(ApiFeature.ALARM.getApiStateKey(), ApiUsageStateValue.ENABLED.name())));
tsService.save(tenantId, saved.getId(), apiUsageStates, 0L); tsService.save(tenantId, saved.getId(), apiUsageStates, 0L);
if (entityId.getEntityType() == EntityType.TENANT && !entityId.equals(TenantId.SYS_TENANT_ID)) { if (entityId.getEntityType() == EntityType.TENANT && !entityId.equals(TenantId.SYS_TENANT_ID)) {

View File

@ -454,6 +454,7 @@ CREATE TABLE IF NOT EXISTS api_usage_state (
js_exec varchar(32), js_exec varchar(32),
email_exec varchar(32), email_exec varchar(32),
sms_exec varchar(32), sms_exec varchar(32),
alarm_exec varchar(32),
CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)
); );

View File

@ -491,6 +491,7 @@ CREATE TABLE IF NOT EXISTS api_usage_state (
js_exec varchar(32), js_exec varchar(32),
email_exec varchar(32), email_exec varchar(32),
sms_exec varchar(32), sms_exec varchar(32),
alarm_exec varchar(32),
CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)
); );