From f079563daf486e0be4ef25998540dcb02f2be092 Mon Sep 17 00:00:00 2001 From: Philip Magyar Date: Thu, 22 Feb 2024 18:10:06 +0100 Subject: [PATCH 01/80] UI: sync country list & add flags to tb-contact country selector --- .../app/modules/home/models/contact.models.ts | 252 ------------------ .../shared/components/contact.component.html | 7 +- .../shared/components/contact.component.ts | 11 +- .../src/app/shared/models/country.models.ts | 15 ++ 4 files changed, 26 insertions(+), 259 deletions(-) diff --git a/ui-ngx/src/app/modules/home/models/contact.models.ts b/ui-ngx/src/app/modules/home/models/contact.models.ts index 41ebecc865..f1b23a0839 100644 --- a/ui-ngx/src/app/modules/home/models/contact.models.ts +++ b/ui-ngx/src/app/modules/home/models/contact.models.ts @@ -14,258 +14,6 @@ /// limitations under the License. /// -export const COUNTRIES = [ - 'Afghanistan', - 'ร…land Islands', - 'Albania', - 'Algeria', - 'American Samoa', - 'Andorra', - 'Angola', - 'Anguilla', - 'Antarctica', - 'Antigua and Barbuda', - 'Argentina', - 'Armenia', - 'Aruba', - 'Australia', - 'Austria', - 'Azerbaijan', - 'Bahamas', - 'Bahrain', - 'Bangladesh', - 'Barbados', - 'Belarus', - 'Belgium', - 'Belize', - 'Benin', - 'Bermuda', - 'Bhutan', - 'Bolivia', - 'Bonaire, Sint Eustatius and Saba', - 'Bosnia and Herzegovina', - 'Botswana', - 'Bouvet Island', - 'Brazil', - 'British Indian Ocean Territory', - 'Brunei Darussalam', - 'Bulgaria', - 'Burkina Faso', - 'Burundi', - 'Cambodia', - 'Cameroon', - 'Canada', - 'Cape Verde', - 'Cayman Islands', - 'Central African Republic', - 'Chad', - 'Chile', - 'China', - 'Christmas Island', - 'Cocos (Keeling) Islands', - 'Colombia', - 'Comoros', - 'Congo', - 'Congo, The Democratic Republic of the', - 'Cook Islands', - 'Costa Rica', - 'Cรดte d\'Ivoire', - 'Croatia', - 'Cuba', - 'Curaรงao', - 'Cyprus', - 'Czech Republic', - 'Denmark', - 'Djibouti', - 'Dominica', - 'Dominican Republic', - 'Ecuador', - 'Egypt', - 'El Salvador', - 'Equatorial Guinea', - 'Eritrea', - 'Estonia', - 'Ethiopia', - 'Falkland Islands (Malvinas)', - 'Faroe Islands', - 'Fiji', - 'Finland', - 'France', - 'French Guiana', - 'French Polynesia', - 'French Southern Territories', - 'Gabon', - 'Gambia', - 'Georgia', - 'Germany', - 'Ghana', - 'Gibraltar', - 'Greece', - 'Greenland', - 'Grenada', - 'Guadeloupe', - 'Guam', - 'Guatemala', - 'Guernsey', - 'Guinea', - 'Guinea-Bissau', - 'Guyana', - 'Haiti', - 'Heard Island and McDonald Islands', - 'Holy See (Vatican City State)', - 'Honduras', - 'Hong Kong', - 'Hungary', - 'Iceland', - 'India', - 'Indonesia', - 'Iran, Islamic Republic of', - 'Iraq', - 'Ireland', - 'Isle of Man', - 'Israel', - 'Italy', - 'Jamaica', - 'Japan', - 'Jersey', - 'Jordan', - 'Kazakhstan', - 'Kenya', - 'Kiribati', - 'Korea, Democratic People\'s Republic of', - 'Korea, Republic of', - 'Kuwait', - 'Kyrgyzstan', - 'Lao People\'s Democratic Republic', - 'Latvia', - 'Lebanon', - 'Lesotho', - 'Liberia', - 'Libya', - 'Liechtenstein', - 'Lithuania', - 'Luxembourg', - 'Macao', - 'Macedonia, Republic Of', - 'Madagascar', - 'Malawi', - 'Malaysia', - 'Maldives', - 'Mali', - 'Malta', - 'Marshall Islands', - 'Martinique', - 'Mauritania', - 'Mauritius', - 'Mayotte', - 'Mexico', - 'Micronesia, Federated States of', - 'Moldova, Republic of', - 'Monaco', - 'Mongolia', - 'Montenegro', - 'Montserrat', - 'Morocco', - 'Mozambique', - 'Myanmar', - 'Namibia', - 'Nauru', - 'Nepal', - 'Netherlands', - 'New Caledonia', - 'New Zealand', - 'Nicaragua', - 'Niger', - 'Nigeria', - 'Niue', - 'Norfolk Island', - 'Northern Mariana Islands', - 'Norway', - 'Oman', - 'Pakistan', - 'Palau', - 'Palestinian Territory, Occupied', - 'Panama', - 'Papua New Guinea', - 'Paraguay', - 'Peru', - 'Philippines', - 'Pitcairn', - 'Poland', - 'Portugal', - 'Puerto Rico', - 'Qatar', - 'Reunion', - 'Romania', - 'Russian Federation', - 'Rwanda', - 'Saint Barthรฉlemy', - 'Saint Helena, Ascension and Tristan da Cunha', - 'Saint Kitts and Nevis', - 'Saint Lucia', - 'Saint Martin (French Part)', - 'Saint Pierre and Miquelon', - 'Saint Vincent and the Grenadines', - 'Samoa', - 'San Marino', - 'Sao Tome and Principe', - 'Saudi Arabia', - 'Senegal', - 'Serbia', - 'Seychelles', - 'Sierra Leone', - 'Singapore', - 'Sint Maarten (Dutch Part)', - 'Slovakia', - 'Slovenia', - 'Solomon Islands', - 'Somalia', - 'South Africa', - 'South Georgia and the South Sandwich Islands', - 'South Sudan', - 'Spain', - 'Sri Lanka', - 'Sudan', - 'Suriname', - 'Svalbard and Jan Mayen', - 'Swaziland', - 'Sweden', - 'Switzerland', - 'Syrian Arab Republic', - 'Taiwan', - 'Tajikistan', - 'Tanzania, United Republic of', - 'Thailand', - 'Timor-Leste', - 'Togo', - 'Tokelau', - 'Tonga', - 'Trinidad and Tobago', - 'Tunisia', - 'Turkey', - 'Turkmenistan', - 'Turks and Caicos Islands', - 'Tuvalu', - 'Uganda', - 'Ukraine', - 'United Arab Emirates', - 'United Kingdom', - 'United States', - 'United States Minor Outlying Islands', - 'Uruguay', - 'Uzbekistan', - 'Vanuatu', - 'Venezuela', - 'Viet Nam', - 'Virgin Islands, British', - 'Virgin Islands, U.S.', - 'Wallis and Futuna', - 'Western Sahara', - 'Yemen', - 'Zambia', - 'Zimbabwe' -]; - /* eslint-disable */ export const POSTAL_CODE_PATTERNS = { 'United States': '(\\d{5}([\\-]\\d{4})?)', diff --git a/ui-ngx/src/app/shared/components/contact.component.html b/ui-ngx/src/app/shared/components/contact.component.html index 059fd09c80..c0688be71a 100644 --- a/ui-ngx/src/app/shared/components/contact.component.html +++ b/ui-ngx/src/app/shared/components/contact.component.html @@ -18,9 +18,10 @@
contact.country - - - {{ country }} + + + {{country.flag}} + {{country.name }} diff --git a/ui-ngx/src/app/shared/components/contact.component.ts b/ui-ngx/src/app/shared/components/contact.component.ts index c5a3daecf8..dae5f2ac0d 100644 --- a/ui-ngx/src/app/shared/components/contact.component.ts +++ b/ui-ngx/src/app/shared/components/contact.component.ts @@ -16,11 +16,13 @@ import { Component, Input } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { COUNTRIES } from '@home/models/contact.models'; - +import { CountryData } from '../models/country.models'; @Component({ selector: 'tb-contact', - templateUrl: './contact.component.html' + templateUrl: './contact.component.html', + providers: [ + CountryData + ] }) export class ContactComponent { @@ -29,6 +31,7 @@ export class ContactComponent { @Input() isEdit: boolean; - countries = COUNTRIES; + countries = this.countryData.allCountries; + constructor(private countryData: CountryData) {} } diff --git a/ui-ngx/src/app/shared/models/country.models.ts b/ui-ngx/src/app/shared/models/country.models.ts index 814cb81147..919d362067 100644 --- a/ui-ngx/src/app/shared/models/country.models.ts +++ b/ui-ngx/src/app/shared/models/country.models.ts @@ -32,6 +32,7 @@ export enum CountryISO { Andorra = 'AD', Angola = 'AO', Anguilla = 'AI', + Antarctica = 'AQ', AntiguaAndBarbuda = 'AG', Argentina = 'AR', Armenia = 'AM', @@ -50,6 +51,8 @@ export enum CountryISO { Bermuda = 'BM', Bhutan = 'BT', Bolivia = 'BO', + BonaireAndSintEustatiusAndSaba = 'BQ', + BouvetIsland = 'BV', BosniaAndHerzegovina = 'BA', Botswana = 'BW', Brazil = 'BR', @@ -101,6 +104,7 @@ export enum CountryISO { France = 'FR', FrenchGuiana = 'GF', FrenchPolynesia = 'PF', + FrenchSouthernTerritories = 'TF', Gabon = 'GA', Gambia = 'GM', Georgia = 'GE', @@ -118,6 +122,7 @@ export enum CountryISO { GuineaBissau = 'GW', Guyana = 'GY', Haiti = 'HT', + HeardIslandandMcDonaldIslands = 'HM', Honduras = 'HN', HongKong = 'HK', Hungary = 'HU', @@ -195,6 +200,7 @@ export enum CountryISO { Paraguay = 'PY', Peru = 'PE', Philippines = 'PH', + PitcairnIslands = 'PN', Poland = 'PL', Portugal = 'PT', PuertoRico = 'PR', @@ -225,6 +231,7 @@ export enum CountryISO { SolomonIslands = 'SB', Somalia = 'SO', SouthAfrica = 'ZA', + SouthGeorgiaAndTheSouthSandwichIslands = 'GS', SouthKorea = 'KR', SouthSudan = 'SS', Spain = 'ES', @@ -280,6 +287,7 @@ export class CountryData { {name: 'Andorra', iso2: CountryISO.Andorra, dialCode: '376', flag: '๐Ÿ‡ฆ๐Ÿ‡ฉ'}, {name: 'Angola', iso2: CountryISO.Angola, dialCode: '244', flag: '๐Ÿ‡ฆ๐Ÿ‡ด'}, {name: 'Anguilla', iso2: CountryISO.Anguilla, dialCode: '1', flag: '๐Ÿ‡ฆ๐Ÿ‡ฎ'}, + {name: 'Antarctica', iso2: CountryISO.Antarctica, dialCode: '672', flag: '๐Ÿ‡ฆ๐Ÿ‡ถ'}, {name: 'Antigua and Barbuda', iso2: CountryISO.AntiguaAndBarbuda, dialCode: '1', flag: '๐Ÿ‡ฆ๐Ÿ‡ฌ'}, {name: 'Argentina', iso2: CountryISO.Argentina, dialCode: '54', flag: '๐Ÿ‡ฆ๐Ÿ‡ท'}, {name: 'Armenia', iso2: CountryISO.Armenia, dialCode: '374', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ'}, @@ -298,6 +306,8 @@ export class CountryData { {name: 'Bermuda', iso2: CountryISO.Bermuda, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ฒ'}, {name: 'Bhutan', iso2: CountryISO.Bhutan, dialCode: '975', flag: '๐Ÿ‡ง๐Ÿ‡น'}, {name: 'Bolivia', iso2: CountryISO.Bolivia, dialCode: '591', flag: '๐Ÿ‡ง๐Ÿ‡ด'}, + {name: 'Bonaire, Sint Eustatius and Saba', iso2: CountryISO.BonaireAndSintEustatiusAndSaba, dialCode: '599', flag: '๐Ÿ‡ง๐Ÿ‡ถ'}, + {name: 'Bouvet Island', iso2: CountryISO.BouvetIsland, dialCode: '47', flag: '๐Ÿ‡ง๐Ÿ‡ป'}, {name: 'Bosnia and Herzegovina', iso2: CountryISO.BosniaAndHerzegovina, dialCode: '387', flag: '๐Ÿ‡ง๐Ÿ‡ฆ'}, {name: 'Botswana', iso2: CountryISO.Botswana, dialCode: '267', flag: '๐Ÿ‡ง๐Ÿ‡ผ'}, {name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '๐Ÿ‡ง๐Ÿ‡ท'}, @@ -349,6 +359,7 @@ export class CountryData { {name: 'France', iso2: CountryISO.France, dialCode: '33', flag: '๐Ÿ‡ซ๐Ÿ‡ท'}, {name: 'French Guiana', iso2: CountryISO.FrenchGuiana, dialCode: '594', flag: '๐Ÿ‡ฌ๐Ÿ‡ซ'}, {name: 'French Polynesia', iso2: CountryISO.FrenchPolynesia, dialCode: '689', flag: '๐Ÿ‡ต๐Ÿ‡ซ'}, + {name: 'French Southern Territories', iso2: CountryISO.FrenchSouthernTerritories, dialCode: '262', flag: '๐Ÿ‡น๐Ÿ‡ซ'}, {name: 'Gabon', iso2: CountryISO.Gabon, dialCode: '241', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ'}, {name: 'Gambia', iso2: CountryISO.Gambia, dialCode: '220', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ'}, {name: 'Georgia', iso2: CountryISO.Georgia, dialCode: '995', flag: '๐Ÿ‡ฌ๐Ÿ‡ช'}, @@ -366,6 +377,7 @@ export class CountryData { {name: 'Guinea-Bissau', iso2: CountryISO.GuineaBissau, dialCode: '245', flag: '๐Ÿ‡ฌ๐Ÿ‡ผ'}, {name: 'Guyana', iso2: CountryISO.Guyana, dialCode: '592', flag: '๐Ÿ‡ฌ๐Ÿ‡พ'}, {name: 'Haiti', iso2: CountryISO.Haiti, dialCode: '509', flag: '๐Ÿ‡ญ๐Ÿ‡น'}, + {name: 'Heard Island and McDonald Islands', iso2: CountryISO.HeardIslandandMcDonaldIslands, dialCode: '672', flag: '๐Ÿ‡ญ๐Ÿ‡ฒ'}, {name: 'Honduras', iso2: CountryISO.Honduras, dialCode: '504', flag: '๐Ÿ‡ญ๐Ÿ‡ณ'}, {name: 'Hong Kong', iso2: CountryISO.HongKong, dialCode: '852', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ'}, {name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '๐Ÿ‡ญ๐Ÿ‡บ'}, @@ -443,6 +455,7 @@ export class CountryData { {name: 'Paraguay', iso2: CountryISO.Paraguay, dialCode: '595', flag: '๐Ÿ‡ต๐Ÿ‡พ'}, {name: 'Peru', iso2: CountryISO.Peru, dialCode: '51', flag: '๐Ÿ‡ต๐Ÿ‡ช'}, {name: 'Philippines', iso2: CountryISO.Philippines, dialCode: '63', flag: '๐Ÿ‡ต๐Ÿ‡ญ'}, + {name: 'Pitcairn Islands', iso2: CountryISO.PitcairnIslands, dialCode: '649', flag: '๐Ÿ‡ต๐Ÿ‡ณ'}, {name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ'}, {name: 'Portugal', iso2: CountryISO.Portugal, dialCode: '351', flag: '๐Ÿ‡ต๐Ÿ‡น'}, {name: 'Puerto Rico', iso2: CountryISO.PuertoRico, dialCode: '1', flag: '๐Ÿ‡ต๐Ÿ‡ท'}, @@ -473,6 +486,8 @@ export class CountryData { {name: 'Solomon Islands', iso2: CountryISO.SolomonIslands, dialCode: '677', flag: '๐Ÿ‡ธ๐Ÿ‡ง'}, {name: 'Somalia', iso2: CountryISO.Somalia, dialCode: '252', flag: '๐Ÿ‡ธ๐Ÿ‡ด'}, {name: 'South Africa', iso2: CountryISO.SouthAfrica, dialCode: '27', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ'}, + {name: 'South Georgia and the South Sandwich Islands', + iso2: CountryISO.SouthGeorgiaAndTheSouthSandwichIslands, dialCode: '500', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ'}, {name: 'South Korea', iso2: CountryISO.SouthKorea, dialCode: '82', flag: '๐Ÿ‡ฐ๐Ÿ‡ท'}, {name: 'South Sudan', iso2: CountryISO.SouthSudan, dialCode: '211', flag: '๐Ÿ‡ธ๐Ÿ‡ธ'}, {name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '๐Ÿ‡ช๐Ÿ‡ธ'}, From 4096a60612a7c3329830620764230e5fa27fb62a Mon Sep 17 00:00:00 2001 From: Philip Magyar Date: Thu, 29 Feb 2024 16:15:26 +0100 Subject: [PATCH 02/80] searchable countries --- .../shared/components/contact.component.html | 11 ++-- .../shared/components/contact.component.ts | 50 +++++++++++++++++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/components/contact.component.html b/ui-ngx/src/app/shared/components/contact.component.html index c0688be71a..6b997da07c 100644 --- a/ui-ngx/src/app/shared/components/contact.component.html +++ b/ui-ngx/src/app/shared/components/contact.component.html @@ -18,12 +18,13 @@
contact.country - - + + + {{country.flag}} {{country.name }} - +
@@ -56,7 +57,9 @@ contact.address2 - diff --git a/ui-ngx/src/app/shared/components/contact.component.ts b/ui-ngx/src/app/shared/components/contact.component.ts index dae5f2ac0d..410944f36a 100644 --- a/ui-ngx/src/app/shared/components/contact.component.ts +++ b/ui-ngx/src/app/shared/components/contact.component.ts @@ -14,9 +14,12 @@ /// limitations under the License. /// -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { CountryData } from '../models/country.models'; +import { Country, CountryData } from '../models/country.models'; +import { Observable, map, startWith } from 'rxjs'; +import { PhoneInputComponent } from './phone-input.component'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; @Component({ selector: 'tb-contact', templateUrl: './contact.component.html', @@ -24,14 +27,53 @@ import { CountryData } from '../models/country.models'; CountryData ] }) -export class ContactComponent { +export class ContactComponent implements OnInit { @Input() parentForm: UntypedFormGroup; @Input() isEdit: boolean; + @Input() phoneInputDefaultCountry = 'US'; + + @ViewChild('phoneInput') phoneInput: PhoneInputComponent | undefined; countries = this.countryData.allCountries; + countriesFiltered: Observable; + + constructor(private countryData: CountryData) { + this.displayCountryWithFlag = this.displayCountryWithFlag.bind(this); + } + + ngOnInit() { + this.countriesFiltered = this.parentForm.get('country').valueChanges + .pipe( + startWith(''), + map((countryName: string) => this._filterCountries(countryName)) + ); + } + + private _filterCountries(countryName: string): Country[] { + const filterValue = countryName.toLowerCase(); + return this.countries.filter(country => this.displayCountryWithFlag(country.name).toLowerCase().includes(filterValue)); + } + + displayCountryWithFlag(countryName: string): string { + const country = this.countries.find(c => c.name === countryName); + if (!country) { + return ''; + } + return country ? `${country.flag} ${country.name}` : ''; + } - constructor(private countryData: CountryData) {} + onCountryChange(select: MatAutocompleteSelectedEvent) { + // Get the selected country and check if the phone number is empty + // If it is, set the country code to the selected country + const countryName = select.option.value; + const country = this.countries.find(c => c.name === countryName); + if (country && this.phoneInput) { + if (!this.phoneInput.phoneFormGroup.get('phoneNumber').value) { + this.phoneInput.phoneFormGroup.get('country').setValue(country.iso2); + } + } + } } From 00fe9e294fe1bc9a7943972be300441ebae89bed Mon Sep 17 00:00:00 2001 From: Andrew Walker Date: Fri, 7 Jun 2024 15:36:06 +0100 Subject: [PATCH 03/80] Added after closed return to openDashboardStateInSeparateDialog function --- .../src/app/modules/home/components/widget/widget.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index a79fa89242..c629981bb5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -1411,6 +1411,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } }); this.cd.markForCheck(); + return dashboard.dialogRef.afterClosed(); } private elementClick($event: Event) { From bff2ed2898e62496878f534f80febb5b2925d2fa Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 5 Jul 2024 18:02:30 +0300 Subject: [PATCH 04/80] added tests for generator node and set default value for entity type --- .../rule/engine/debug/TbMsgGeneratorNode.java | 41 +++- .../TbMsgGeneratorNodeConfiguration.java | 1 + .../engine/debug/TbMsgGeneratorNodeTest.java | 214 +++++++++++++++++- 3 files changed, 245 insertions(+), 11 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 196c0fa036..f25f280749 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -29,6 +29,7 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -42,6 +43,8 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import java.util.EnumSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -54,7 +57,7 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; type = ComponentType.ACTION, name = "generator", configClazz = TbMsgGeneratorNodeConfiguration.class, - version = 1, + version = 2, hasQueueName = true, nodeDescription = "Periodically generates messages", nodeDetails = "Generates messages with configurable period. Javascript function used for message generation.", @@ -66,6 +69,9 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; public class TbMsgGeneratorNode implements TbNode { + private static final Set supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET, EntityType.ENTITY_VIEW, + EntityType.TENANT, EntityType.CUSTOMER, EntityType.USER, EntityType.DASHBOARD, EntityType.EDGE, EntityType.RULE_NODE); + private TbMsgGeneratorNodeConfiguration config; private ScriptEngine scriptEngine; private long delay; @@ -83,12 +89,10 @@ public class TbMsgGeneratorNode implements TbNode { this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds()); this.currentMsgCount = 0; this.queueName = ctx.getQueueName(); - if (!StringUtils.isEmpty(config.getOriginatorId())) { - originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId()); - ctx.checkTenantEntity(originatorId); - } else { - originatorId = ctx.getSelfId(); + if (!supportedEntityTypes.contains(config.getOriginatorType())) { + throw new TbNodeException("Originator type '" + config.getOriginatorType() + "' is not supported.", true); } + originatorId = getOriginatorId(ctx); log.debug("[{}] Initializing generator with config {}", originatorId, configuration); updateGeneratorState(ctx); } @@ -173,6 +177,19 @@ public class TbMsgGeneratorNode implements TbNode { return msg != null ? msg.getCustomerId() : null; } + private EntityId getOriginatorId(TbContext ctx) throws TbNodeException { + if (EntityType.RULE_NODE.equals(config.getOriginatorType())) { + return ctx.getSelfId(); + } else if (EntityType.TENANT.equals(config.getOriginatorType())) { + return ctx.getTenantId(); + } else if (!StringUtils.isEmpty(config.getOriginatorId())) { + var entityId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId()); + ctx.checkTenantEntity(entityId); + return entityId; + } + return ctx.getSelfId(); + } + @Override public void destroy() { log.debug("[{}] Stopping generator", originatorId); @@ -195,6 +212,18 @@ public class TbMsgGeneratorNode implements TbNode { hasChanges = true; ((ObjectNode) oldConfiguration).remove(QUEUE_NAME); } + case 1: + String originatorType = "originatorType"; + if (oldConfiguration.has(originatorType)) { + var origType = oldConfiguration.get(originatorType); + if (origType.isNull()) { + ((ObjectNode) oldConfiguration).put(originatorType, EntityType.RULE_NODE.name()); + hasChanges = true; + } else if (EntityType.TENANT.name().equals(origType.asText())) { + ((ObjectNode) oldConfiguration).putNull("originatorId"); + hasChanges = true; + } + } break; default: break; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java index 017b6e4381..2160ec450c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java @@ -42,6 +42,7 @@ public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("Originator type 'NOTIFICATION' is not supported."); + } + + @ParameterizedTest + @MethodSource + public void givenOriginatorEntityType_whenInit_thenVerifyOriginatorId(EntityType entityType, + String originatorId, + EntityId expectedOriginatorId, + Consumer mockCtx) throws TbNodeException { + // GIVEN + config.setOriginatorType(entityType); + config.setOriginatorId(originatorId); + + mockCtx.accept(ctxMock); + + // WHEN + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // THEN + then(ctxMock).should().isLocalEntity(expectedOriginatorId); + } + + private static Stream givenOriginatorEntityType_whenInit_thenVerifyOriginatorId() { + return Stream.of( + Arguments.of(EntityType.RULE_NODE, null, RULE_NODE_ID, + (Consumer) ctxMock -> given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID)), + Arguments.of(EntityType.TENANT, null, TenantId.fromUUID(UUID.fromString("c7f7b865-3e4c-40d3-b333-a7ec2fd871ee")), + (Consumer) ctxMock -> given(ctxMock.getTenantId()).willReturn(TenantId.fromUUID(UUID.fromString("c7f7b865-3e4c-40d3-b333-a7ec2fd871ee")))), + Arguments.of(EntityType.ASSET, "cbb9a3d3-02f1-482b-90ab-2417dcd35f20", new AssetId(UUID.fromString("cbb9a3d3-02f1-482b-90ab-2417dcd35f20")), + (Consumer) ctxMock -> given(ctxMock.getQueueName()).willReturn("Main")) + ); + } + + @Test + public void givenMsgCountAndDelay_whenInit_thenVerifyInvocationOfOnMsgMethod() throws TbNodeException, InterruptedException { + // GIVEN + var awaitTellSelfLatch = new CountDownLatch(5); + config.setMsgCount(5); + + given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID); + given(ctxMock.isLocalEntity(any())).willReturn(true); + given(ctxMock.createScriptEngine(any(), any(), any(), any(), any())).willReturn(scriptEngineMock); + + // creation of tickMsg + TbMsg tickMsg = TbMsg.newMsg(TbMsgType.GENERATOR_NODE_SELF_MSG, RULE_NODE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + given(ctxMock.newMsg(null, TbMsgType.GENERATOR_NODE_SELF_MSG, RULE_NODE_ID, null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING)).willReturn(tickMsg); + + // invocation of tellSelf() method + willAnswer(invocationOnMock -> { + executorService.execute(() -> { + node.onMsg(ctxMock, invocationOnMock.getArgument(0)); + awaitTellSelfLatch.countDown(); + }); + return null; + }).given(ctxMock).tellSelf(any(), any(Long.class)); + + // creation of first message + TbMsg firstMsg = TbMsg.newMsg(TbMsg.EMPTY_STRING, RULE_NODE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + given(ctxMock.newMsg(null, TbMsg.EMPTY_STRING, RULE_NODE_ID, null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT)).willReturn(firstMsg); + + // creation of generated message + TbMsgMetaData metaData = new TbMsgMetaData(Map.of("data", "40")); + TbMsg generatedMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, RULE_NODE_ID, metaData, "{ \"temp\": 42, \"humidity\": 77 }"); + given(scriptEngineMock.executeGenerateAsync(any())).willReturn(Futures.immediateFuture(generatedMsg)); + + // creation of prev message + TbMsg prevMsg = TbMsg.newMsg(generatedMsg.getType(), RULE_NODE_ID, generatedMsg.getMetaData(), generatedMsg.getData()); + given(ctxMock.newMsg(null, generatedMsg.getType(), RULE_NODE_ID, null, generatedMsg.getMetaData(), generatedMsg.getData())).willReturn(prevMsg); + + // WHEN + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + awaitTellSelfLatch.await(); + + // THEN + + // verify invocation of tellSelf() method + ArgumentCaptor actualTickMsg = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should(times(6)).tellSelf(actualTickMsg.capture(), any(Long.class)); + assertThat(actualTickMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(tickMsg); + + // verify invocation of enqueueForTellNext() method + ArgumentCaptor actualGeneratedMsg = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should(times(5)).enqueueForTellNext(actualGeneratedMsg.capture(), eq(TbNodeConnectionType.SUCCESS)); + assertThat(actualGeneratedMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx", "ts", "id").isEqualTo(generatedMsg); + } + + @Test + public void givenOriginatorIsNotLocalEntity_whenInit_thenDestroy() throws TbNodeException { + // GIVEN + config.setOriginatorType(EntityType.DEVICE); + config.setOriginatorId("2e8b77f1-ee33-4207-a3d7-556fb16e0151"); + ReflectionTestUtils.setField(node, "initialized", new AtomicBoolean(true)); + + given(ctxMock.isLocalEntity(any())).willReturn(false); + + // WHEN + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // THEN + then(node).should().destroy(); + } + // Rule nodes upgrade private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { return Stream.of( @@ -32,22 +225,33 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { Arguments.of(0, "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null, \"queueName\":null, \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", true, - "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null, \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"RULE_NODE\", \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), // default config for version 0 with queueName Arguments.of(0, "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null, \"queueName\":\"Main\", \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", true, - "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null, \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"RULE_NODE\", \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), // default config for version 1 with upgrade from version 0 Arguments.of(0, "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null, \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", - false, - "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null, \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}") + true, + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"RULE_NODE\", \"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), + // config for version 2 with upgrade from version 1 (originatorType is not selected) + Arguments.of(1, + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null,\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", + true, + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"RULE_NODE\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), + // config for version 2 with upgrade from version 1 (originatorType is TENANT) + Arguments.of(1, + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":\"ae540d15-7ef6-41d4-9176-bf788324a5c3\",\"originatorType\":\"TENANT\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", + true, + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"TENANT\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}") ); } @Override protected TbNode getTestNode() { - return spy(TbMsgGeneratorNode.class); + return node; } + } From a40ce3305e9afba5b18852ee80ea26cde71c6989 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Jul 2024 11:14:25 +0300 Subject: [PATCH 05/80] added validation for originatorId and changed upgrade script --- .../rule/engine/debug/TbMsgGeneratorNode.java | 20 +-- .../engine/debug/TbMsgGeneratorNodeTest.java | 114 ++++++++++++------ 2 files changed, 89 insertions(+), 45 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index f25f280749..fed7b88bad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -180,14 +180,16 @@ public class TbMsgGeneratorNode implements TbNode { private EntityId getOriginatorId(TbContext ctx) throws TbNodeException { if (EntityType.RULE_NODE.equals(config.getOriginatorType())) { return ctx.getSelfId(); - } else if (EntityType.TENANT.equals(config.getOriginatorType())) { + } + if (EntityType.TENANT.equals(config.getOriginatorType())) { return ctx.getTenantId(); - } else if (!StringUtils.isEmpty(config.getOriginatorId())) { - var entityId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId()); - ctx.checkTenantEntity(entityId); - return entityId; } - return ctx.getSelfId(); + if (StringUtils.isBlank(config.getOriginatorId())) { + throw new TbNodeException("Originator entity must be selected.", true); + } + var entityId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId()); + ctx.checkTenantEntity(entityId); + return entityId; } @Override @@ -216,12 +218,10 @@ public class TbMsgGeneratorNode implements TbNode { String originatorType = "originatorType"; if (oldConfiguration.has(originatorType)) { var origType = oldConfiguration.get(originatorType); - if (origType.isNull()) { + var origId = oldConfiguration.get("originatorId"); + if (origType.isNull() || origType.asText().isEmpty() || origId.isNull() || origId.asText().isEmpty()) { ((ObjectNode) oldConfiguration).put(originatorType, EntityType.RULE_NODE.name()); hasChanges = true; - } else if (EntityType.TENANT.name().equals(origType.asText())) { - ((ObjectNode) oldConfiguration).putNull("originatorId"); - hasChanges = true; } } break; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java index 059f7b2674..f387b5f43f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java @@ -22,7 +22,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -36,7 +37,6 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -45,30 +45,38 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import java.util.EnumSet; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { - private static final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("1c649392-1f53-4377-b12f-1ba172611746")); + private static final Set supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET, EntityType.ENTITY_VIEW, + EntityType.TENANT, EntityType.CUSTOMER, EntityType.USER, EntityType.DASHBOARD, EntityType.EDGE, EntityType.RULE_NODE); + + private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("1c649392-1f53-4377-b12f-1ba172611746")); + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("4470dfc2-f621-42b2-b82c-b5776d424140")); private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("msg-generator-node-test"); @@ -107,45 +115,71 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { assertThat(config.getTbelScript()).isEqualTo(TbMsgGeneratorNodeConfiguration.DEFAULT_SCRIPT); } - @Test - public void givenUnsupportedEntityType_whenInit_thenThrowsException() { + @ParameterizedTest + @EnumSource(EntityType.class) + public void givenEntityType_whenInit_thenVerifyException(EntityType entityType) { // GIVEN - config.setOriginatorType(EntityType.NOTIFICATION); + config.setOriginatorType(entityType); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN-THEN - assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) - .isInstanceOf(TbNodeException.class) - .hasMessage("Originator type 'NOTIFICATION' is not supported."); + if (entityType == EntityType.RULE_NODE || entityType == EntityType.TENANT) { + assertThatNoException().isThrownBy(() -> node.init(ctxMock, configuration)); + } else { + String errorMsg = supportedEntityTypes.contains(entityType) ? "Originator entity must be selected." + : "Originator type '" + entityType + "' is not supported."; + assertThatThrownBy(() -> node.init(ctxMock, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage(errorMsg); + } } - @ParameterizedTest - @MethodSource - public void givenOriginatorEntityType_whenInit_thenVerifyOriginatorId(EntityType entityType, - String originatorId, - EntityId expectedOriginatorId, - Consumer mockCtx) throws TbNodeException { + @Test + public void givenOriginatorEntityTypeIsRuleNode_whenInit_thenVerifyOriginatorId() throws TbNodeException { // GIVEN - config.setOriginatorType(entityType); - config.setOriginatorId(originatorId); + config.setOriginatorType(EntityType.RULE_NODE); - mockCtx.accept(ctxMock); + given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID); // WHEN node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); // THEN - then(ctxMock).should().isLocalEntity(expectedOriginatorId); + then(ctxMock).should().isLocalEntity(RULE_NODE_ID); } - private static Stream givenOriginatorEntityType_whenInit_thenVerifyOriginatorId() { - return Stream.of( - Arguments.of(EntityType.RULE_NODE, null, RULE_NODE_ID, - (Consumer) ctxMock -> given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID)), - Arguments.of(EntityType.TENANT, null, TenantId.fromUUID(UUID.fromString("c7f7b865-3e4c-40d3-b333-a7ec2fd871ee")), - (Consumer) ctxMock -> given(ctxMock.getTenantId()).willReturn(TenantId.fromUUID(UUID.fromString("c7f7b865-3e4c-40d3-b333-a7ec2fd871ee")))), - Arguments.of(EntityType.ASSET, "cbb9a3d3-02f1-482b-90ab-2417dcd35f20", new AssetId(UUID.fromString("cbb9a3d3-02f1-482b-90ab-2417dcd35f20")), - (Consumer) ctxMock -> given(ctxMock.getQueueName()).willReturn("Main")) - ); + @Test + public void givenOriginatorEntityTypeIsTenant_whenInit_thenVerifyOriginatorId() throws TbNodeException { + // GIVEN + config.setOriginatorType(EntityType.TENANT); + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + + // WHEN + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // THEN + then(ctxMock).should().isLocalEntity(TENANT_ID); + } + + @ParameterizedTest + @ValueSource(strings = {"ASSET", "DEVICE", "ENTITY_VIEW", "CUSTOMER", "USER", "DASHBOARD", "EDGE"}) + public void givenOriginatorEntityType_whenInit_thenVerifyOriginatorId(String entityTypeStr) throws TbNodeException { + // GIVEN + EntityType entityType = EntityType.valueOf(entityTypeStr); + config.setOriginatorType(entityType); + UUID entityId = UUID.randomUUID(); + config.setOriginatorId(entityId.toString()); + + // WHEN + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // THEN + ArgumentCaptor actualEntityIdCaptor = ArgumentCaptor.forClass(EntityId.class); + then(ctxMock).should().isLocalEntity(actualEntityIdCaptor.capture()); + EntityId actualEntityId = actualEntityIdCaptor.getValue(); + assertThat(actualEntityId.getEntityType()).isEqualTo(entityType); + assertThat(actualEntityId.getId()).isEqualTo(entityId); } @Test @@ -191,6 +225,9 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { // THEN + //verify creation of prev message + then(ctxMock).should(times(1)).newMsg(null, TbMsg.EMPTY_STRING, RULE_NODE_ID, null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + // verify invocation of tellSelf() method ArgumentCaptor actualTickMsg = ArgumentCaptor.forClass(TbMsg.class); then(ctxMock).should(times(6)).tellSelf(actualTickMsg.capture(), any(Long.class)); @@ -199,11 +236,11 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { // verify invocation of enqueueForTellNext() method ArgumentCaptor actualGeneratedMsg = ArgumentCaptor.forClass(TbMsg.class); then(ctxMock).should(times(5)).enqueueForTellNext(actualGeneratedMsg.capture(), eq(TbNodeConnectionType.SUCCESS)); - assertThat(actualGeneratedMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx", "ts", "id").isEqualTo(generatedMsg); + assertThat(actualGeneratedMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(prevMsg); } @Test - public void givenOriginatorIsNotLocalEntity_whenInit_thenDestroy() throws TbNodeException { + public void givenOriginatorIsNotLocalEntity_whenOnPartitionChangeMsg_thenDestroy() { // GIVEN config.setOriginatorType(EntityType.DEVICE); config.setOriginatorId("2e8b77f1-ee33-4207-a3d7-556fb16e0151"); @@ -212,7 +249,8 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { given(ctxMock.isLocalEntity(any())).willReturn(false); // WHEN - node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + var partitionChangeMsgMock = mock(PartitionChangeMsg.class); + node.onPartitionChangeMsg(ctxMock, partitionChangeMsgMock); // THEN then(node).should().destroy(); @@ -241,11 +279,17 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":null,\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", true, "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"RULE_NODE\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), - // config for version 2 with upgrade from version 1 (originatorType is TENANT) + // config for version 2 with upgrade from version 1 (originatorId is not selected) Arguments.of(1, - "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":\"ae540d15-7ef6-41d4-9176-bf788324a5c3\",\"originatorType\":\"TENANT\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"DEVICE\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", true, - "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"TENANT\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}") + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":null,\"originatorType\":\"RULE_NODE\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}"), + // config for version 2 with upgrade from version 1 (originatorType and originatorId are selected) + Arguments.of(1, + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":\"92b8e1ce-1b58-4f23-b127-7a0a031b0677\",\"originatorType\":\"DEVICE\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}", + false, + "{\"msgCount\":0,\"periodInSeconds\":1,\"originatorId\":\"92b8e1ce-1b58-4f23-b127-7a0a031b0677\",\"originatorType\":\"DEVICE\",\"scriptLang\":\"TBEL\",\"jsScript\":\"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\",\"tbelScript\": \"var msg = { temp: 42, humidity: 77 };\\nvar metadata = { data: 40 };\\nvar msgType = \\\"POST_TELEMETRY_REQUEST\\\";\\n\\nreturn { msg: msg, metadata: metadata, msgType: msgType };\"}") + ); } From c7005971ad614e945887b603e2b2b2381b32d2e3 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Jul 2024 12:41:33 +0300 Subject: [PATCH 06/80] refactored tests --- .../rule/engine/debug/TbMsgGeneratorNode.java | 15 ++++++++------- .../engine/debug/TbMsgGeneratorNodeTest.java | 16 ++++++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index fed7b88bad..c7bf1423f8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -216,13 +216,14 @@ public class TbMsgGeneratorNode implements TbNode { } case 1: String originatorType = "originatorType"; - if (oldConfiguration.has(originatorType)) { - var origType = oldConfiguration.get(originatorType); - var origId = oldConfiguration.get("originatorId"); - if (origType.isNull() || origType.asText().isEmpty() || origId.isNull() || origId.asText().isEmpty()) { - ((ObjectNode) oldConfiguration).put(originatorType, EntityType.RULE_NODE.name()); - hasChanges = true; - } + String originatorId = "originatorId"; + boolean hasType = oldConfiguration.hasNonNull(originatorType); + boolean hasOriginatorId = oldConfiguration.hasNonNull(originatorId) && + StringUtils.isNotBlank(oldConfiguration.get(originatorId).asText()); + boolean hasOriginatorFields = hasType && hasOriginatorId; + if (!hasOriginatorFields) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put(originatorType, EntityType.RULE_NODE.name()); } break; default: diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java index f387b5f43f..c1a2282083 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeTest.java @@ -38,6 +38,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -130,7 +131,9 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { : "Originator type '" + entityType + "' is not supported."; assertThatThrownBy(() -> node.init(ctxMock, configuration)) .isInstanceOf(TbNodeException.class) - .hasMessage(errorMsg); + .hasMessage(errorMsg) + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); } } @@ -168,18 +171,15 @@ public class TbMsgGeneratorNodeTest extends AbstractRuleNodeUpgradeTest { // GIVEN EntityType entityType = EntityType.valueOf(entityTypeStr); config.setOriginatorType(entityType); - UUID entityId = UUID.randomUUID(); - config.setOriginatorId(entityId.toString()); + String entityIdStr = "5751f55e-089e-4be0-b10a-dd942053dcf0"; + config.setOriginatorId(entityIdStr); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); // WHEN node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); // THEN - ArgumentCaptor actualEntityIdCaptor = ArgumentCaptor.forClass(EntityId.class); - then(ctxMock).should().isLocalEntity(actualEntityIdCaptor.capture()); - EntityId actualEntityId = actualEntityIdCaptor.getValue(); - assertThat(actualEntityId.getEntityType()).isEqualTo(entityType); - assertThat(actualEntityId.getId()).isEqualTo(entityId); + then(ctxMock).should().isLocalEntity(entityId); } @Test From f85fada6a190f49b08c87ebb0a659681192d9259 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 17 Jul 2024 11:30:17 +0300 Subject: [PATCH 07/80] UI: Improvment for generator node --- ui-ngx/src/app/core/http/entity.service.ts | 3 +++ .../entity/entity-autocomplete.component.ts | 2 ++ .../components/entity/entity-select.component.html | 5 +++-- .../components/entity/entity-select.component.ts | 5 +++-- .../src/app/shared/components/js-func.component.html | 1 + .../app/shared/components/script-lang.component.html | 7 ++++--- .../app/shared/components/script-lang.component.scss | 11 +++++++++++ .../app/shared/components/script-lang.component.ts | 5 +++++ ui-ngx/src/app/shared/models/entity-type.models.ts | 10 +++++++++- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 ++ 10 files changed, 43 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index b5b2b0e796..35bcc1a614 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -756,6 +756,9 @@ export class EntityService { } } if (allowedEntityTypes && allowedEntityTypes.length) { + if (allowedEntityTypes.includes(AliasEntityType.CURRENT_RULE_NODE)) { + entityTypes.push(AliasEntityType.CURRENT_RULE_NODE); + } for (let index = entityTypes.length - 1; index >= 0; index--) { if (allowedEntityTypes.indexOf(entityTypes[index]) === -1) { entityTypes.splice(index, 1); diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index b55f01a7ae..071559f234 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -414,6 +414,8 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } else { return EntityType.CUSTOMER; } + } else if (entityType === AliasEntityType.CURRENT_RULE_NODE) { + return EntityType.RULE_NODE; } return entityType; } diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index 783febec7e..b3d68a5610 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index 5c63f81cb4..06212236a7 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -15,7 +15,7 @@ /// import { AfterViewInit, Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; @@ -145,7 +145,8 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte if (this.modelValue.entityType === AliasEntityType.CURRENT_TENANT || this.modelValue.entityType === AliasEntityType.CURRENT_USER - || this.modelValue.entityType === AliasEntityType.CURRENT_USER_OWNER) { + || this.modelValue.entityType === AliasEntityType.CURRENT_USER_OWNER + || this.modelValue.entityType === AliasEntityType.CURRENT_RULE_NODE) { this.modelValue.id = NULL_UUID; } else if (this.modelValue.entityType === AliasEntityType.CURRENT_CUSTOMER && !this.modelValue.id) { this.modelValue.id = NULL_UUID; diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index 66bced8adf..5bea273153 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -24,6 +24,7 @@ {{ functionLabel }} + diff --git a/ui-ngx/src/app/shared/components/script-lang.component.html b/ui-ngx/src/app/shared/components/script-lang.component.html index 8887f9d7c3..dfe313bbfc 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.html +++ b/ui-ngx/src/app/shared/components/script-lang.component.html @@ -16,10 +16,11 @@ -->
- {{ 'rulenode.script-lang-tbel' | translate }} - {{ 'rulenode.script-lang-java-script' | translate }} + + {{ (shortLang ? 'rulenode.script-lang-java-script-short' : 'rulenode.script-lang-java-script') | translate }} +
diff --git a/ui-ngx/src/app/shared/components/script-lang.component.scss b/ui-ngx/src/app/shared/components/script-lang.component.scss index b36f22c95e..f92f63a53b 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.scss +++ b/ui-ngx/src/app/shared/components/script-lang.component.scss @@ -14,6 +14,7 @@ * limitations under the License. */ .mat-button-toggle-group.tb-script-lang-toggle-group { + width: 320px; &.mat-button-toggle-group-appearance-standard { border: none; border-radius: 18px; @@ -57,3 +58,13 @@ opacity: .01; } } +.mat-button-toggle-group.tb-script-lang-toggle-group-short { + width: 120px; + .mat-button-toggle { + height: 24px; + } + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 20px; + font-size: 12px; + } +} diff --git a/ui-ngx/src/app/shared/components/script-lang.component.ts b/ui-ngx/src/app/shared/components/script-lang.component.ts index dd9d995bcc..62658a9412 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.ts +++ b/ui-ngx/src/app/shared/components/script-lang.component.ts @@ -20,6 +20,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-script-lang', @@ -43,6 +44,10 @@ export class TbScriptLangComponent extends PageComponent implements ControlValue @Input() disabled: boolean; + @Input() + @coerceBoolean() + shortLang = true; + private propagateChange = null; constructor(protected store: Store, diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 045e95983c..c9740d9ba4 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -51,7 +51,8 @@ export enum AliasEntityType { CURRENT_CUSTOMER = 'CURRENT_CUSTOMER', CURRENT_TENANT = 'CURRENT_TENANT', CURRENT_USER = 'CURRENT_USER', - CURRENT_USER_OWNER = 'CURRENT_USER_OWNER' + CURRENT_USER_OWNER = 'CURRENT_USER_OWNER', + CURRENT_RULE_NODE = 'CURRENT_RULE_NODE' } export interface EntityTypeTranslation { @@ -323,6 +324,13 @@ export const entityTypeTranslations = new Map Date: Thu, 18 Jul 2024 12:08:14 +0300 Subject: [PATCH 08/80] UI: Fixed boolean default value --- ui-ngx/src/app/shared/components/script-lang.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/script-lang.component.ts b/ui-ngx/src/app/shared/components/script-lang.component.ts index 62658a9412..5bc1460cd2 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.ts +++ b/ui-ngx/src/app/shared/components/script-lang.component.ts @@ -46,7 +46,7 @@ export class TbScriptLangComponent extends PageComponent implements ControlValue @Input() @coerceBoolean() - shortLang = true; + shortLang = false; private propagateChange = null; From b62f53415ca78d8ae8ef0b69f82cd88e6f9a5006 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 19 Jul 2024 17:20:58 +0300 Subject: [PATCH 09/80] UI: Refactoring --- .../components/entity/entity-select.component.html | 5 +---- .../components/entity/entity-select.component.ts | 9 +++++---- .../src/app/shared/components/js-func.component.html | 2 +- .../app/shared/components/script-lang.component.html | 7 ++++--- .../app/shared/components/script-lang.component.scss | 10 ---------- .../src/app/shared/components/script-lang.component.ts | 4 ---- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 - 7 files changed, 11 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index b3d68a5610..b4a3a33029 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -25,10 +25,7 @@ formControlName="entityType">
diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index 06212236a7..eff7f5306f 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -63,6 +63,10 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte AliasEntityType = AliasEntityType; + entityTypeNullUid: Set = new Set([ + AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER, AliasEntityType.CURRENT_RULE_NODE + ]); + private readonly defaultEntityType: EntityType | AliasEntityType = null; private propagateChange = (v: any) => { }; @@ -143,10 +147,7 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte id: this.modelValue.entityType !== entityType ? null : entityId }; - if (this.modelValue.entityType === AliasEntityType.CURRENT_TENANT - || this.modelValue.entityType === AliasEntityType.CURRENT_USER - || this.modelValue.entityType === AliasEntityType.CURRENT_USER_OWNER - || this.modelValue.entityType === AliasEntityType.CURRENT_RULE_NODE) { + if (this.entityTypeNullUid.has(this.modelValue.entityType)) { this.modelValue.id = NULL_UUID; } else if (this.modelValue.entityType === AliasEntityType.CURRENT_CUSTOMER && !this.modelValue.id) { this.modelValue.id = NULL_UUID; diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index 5bea273153..082a92b2c1 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -24,7 +24,7 @@ {{ functionLabel }} - + diff --git a/ui-ngx/src/app/shared/components/script-lang.component.html b/ui-ngx/src/app/shared/components/script-lang.component.html index dfe313bbfc..2afa16e743 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.html +++ b/ui-ngx/src/app/shared/components/script-lang.component.html @@ -16,11 +16,12 @@ -->
- + {{ 'rulenode.script-lang-tbel' | translate }} - {{ (shortLang ? 'rulenode.script-lang-java-script-short' : 'rulenode.script-lang-java-script') | translate }} + {{ 'rulenode.script-lang-java-script' | translate }}
diff --git a/ui-ngx/src/app/shared/components/script-lang.component.scss b/ui-ngx/src/app/shared/components/script-lang.component.scss index f92f63a53b..8e47eac4a2 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.scss +++ b/ui-ngx/src/app/shared/components/script-lang.component.scss @@ -58,13 +58,3 @@ opacity: .01; } } -.mat-button-toggle-group.tb-script-lang-toggle-group-short { - width: 120px; - .mat-button-toggle { - height: 24px; - } - .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { - line-height: 20px; - font-size: 12px; - } -} diff --git a/ui-ngx/src/app/shared/components/script-lang.component.ts b/ui-ngx/src/app/shared/components/script-lang.component.ts index 5bc1460cd2..65d9ecc876 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.ts +++ b/ui-ngx/src/app/shared/components/script-lang.component.ts @@ -44,10 +44,6 @@ export class TbScriptLangComponent extends PageComponent implements ControlValue @Input() disabled: boolean; - @Input() - @coerceBoolean() - shortLang = false; - private propagateChange = null; constructor(protected store: Store, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0380d1a6be..f1ded7a441 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4217,7 +4217,6 @@ "invalid-target-rulechain": "Unable to resolve target rule chain!", "test-script-function": "Test script function", "script-lang-java-script": "JavaScript", - "script-lang-java-script-short": "JS", "script-lang-tbel": "TBEL", "message": "Message", "message-type": "Message type", From a19577928f9ac23bc752ddd2613b5e9359d86276 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 19 Jul 2024 17:22:14 +0300 Subject: [PATCH 10/80] UI: optimize import --- ui-ngx/src/app/shared/components/script-lang.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/components/script-lang.component.ts b/ui-ngx/src/app/shared/components/script-lang.component.ts index 65d9ecc876..b3410f1a21 100644 --- a/ui-ngx/src/app/shared/components/script-lang.component.ts +++ b/ui-ngx/src/app/shared/components/script-lang.component.ts @@ -15,12 +15,11 @@ /// import { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ScriptLanguage } from '@shared/models/rule-node.models'; -import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-script-lang', From fc80bda9d9b0e23ff3d0e6f5dde69e4f40da71f5 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 22 Jul 2024 15:04:58 +0300 Subject: [PATCH 11/80] UI:Refactoring --- ui-ngx/src/app/core/http/entity.service.ts | 3 --- .../shared/components/entity/entity-type-select.component.ts | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 35bcc1a614..b5b2b0e796 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -756,9 +756,6 @@ export class EntityService { } } if (allowedEntityTypes && allowedEntityTypes.length) { - if (allowedEntityTypes.includes(AliasEntityType.CURRENT_RULE_NODE)) { - entityTypes.push(AliasEntityType.CURRENT_RULE_NODE); - } for (let index = entityTypes.length - 1; index >= 0; index--) { if (allowedEntityTypes.indexOf(entityTypes[index]) === -1) { entityTypes.splice(index, 1); diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index 04754e4a20..3b0dca6e81 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -93,6 +93,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, this.entityTypes = this.filterAllowedEntityTypes ? this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes; + if (this.allowedEntityTypes?.length && this.allowedEntityTypes.includes(AliasEntityType.CURRENT_RULE_NODE)) { + this.entityTypes.push(AliasEntityType.CURRENT_RULE_NODE); + } this.entityTypeFormGroup.get('entityType').valueChanges.subscribe( (value) => { let modelValue; From 02f7001102ee3d8e4da0005e3e8b80622504dd70 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 31 Jul 2024 16:46:18 +0300 Subject: [PATCH 12/80] added spring security for http transport, added large RequestSizeFilter for http transport --- .../ThingsboardSecurityConfiguration.java | 35 +++++++++-- .../src/main/resources/thingsboard.yml | 6 +- .../server/controller/RpcControllerTest.java | 2 +- .../MaxPayloadSizeExceededException.java | 10 +++- common/transport/http/pom.xml | 4 ++ .../transport/http/DeviceApiController.java | 11 ---- .../http}/config/RequestSizeFilter.java | 37 ++++-------- .../TransportSecurityConfiguration.java | 59 +++++++++++++++++++ .../src/main/resources/tb-http-transport.yml | 2 +- 9 files changed, 120 insertions(+), 46 deletions(-) rename {application/src/main/java/org/thingsboard/server => common/transport/http/src/main/java/org/thingsboard/server/transport/http}/config/RequestSizeFilter.java (61%) create mode 100644 common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 77ad1a1b9e..05693c5e30 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.server.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -56,6 +57,7 @@ import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2Autho import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; +import org.thingsboard.server.transport.http.config.RequestSizeFilter; import java.util.ArrayList; import java.util.Arrays; @@ -82,6 +84,11 @@ public class ThingsboardSecurityConfiguration { public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download"; + @Value("${server.http.max_payload_size:16777216}") + private int maxPayloadSize; + @Value("${transport.http.max_payload_size:65536}") + private int httpTransportMaxPayloadSize; + @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; @@ -124,8 +131,15 @@ public class ThingsboardSecurityConfiguration { @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; - @Autowired - private RequestSizeFilter requestSizeFilter; + @Bean + protected RequestSizeFilter httpTransportRequestSizeFilter() { + return new RequestSizeFilter(httpTransportMaxPayloadSize); + } + + @Bean + protected RequestSizeFilter requestSizeFilter() { + return new RequestSizeFilter(maxPayloadSize); + } @Bean protected FilterRegistrationBean buildEtagFilter() throws Exception { @@ -203,6 +217,20 @@ public class ThingsboardSecurityConfiguration { } @Bean + @Order(1) + SecurityFilterChain httpTransportFilterChain(HttpSecurity http) throws Exception { + http + .securityMatchers(matchers -> matchers.requestMatchers(DEVICE_API_ENTRY_POINT)) + .cors(cors -> {}) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(config -> config + .requestMatchers(DEVICE_API_ENTRY_POINT).permitAll()) + .addFilterBefore(httpTransportRequestSizeFilter(), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + @Order(2) SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.headers(headers -> headers .cacheControl(config -> {}) @@ -214,7 +242,6 @@ public class ThingsboardSecurityConfiguration { .authorizeHttpRequests(config -> config .requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points (webjars included) .requestMatchers( - DEVICE_API_ENTRY_POINT, // Device HTTP Transport API FORM_BASED_LOGIN_ENTRY_POINT, // Login end-point PUBLIC_LOGIN_ENTRY_POINT, // Public login end-point TOKEN_REFRESH_ENTRY_POINT, // Token refresh end-point @@ -228,7 +255,7 @@ public class ThingsboardSecurityConfiguration { .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(requestSizeFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(requestSizeFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); if (oauth2Configuration != null) { http.oauth2Login(login -> login diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 3db77d50c5..496b151ff0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -50,6 +50,10 @@ server: key_alias: "${SSL_KEY_ALIAS:tomcat}" # Password used to access the key key_password: "${SSL_KEY_PASSWORD:thingsboard}" + # HTTP settings + http: + # Maximum request size + max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:16777216}" # max payload size in bytes # HTTP/2 support (takes effect only if server SSL is enabled) http2: # Enable/disable HTTP/2 support @@ -959,7 +963,7 @@ transport: request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" # HTTP maximum request processing timeout in milliseconds max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" - # Maximum request size + # HTTP maximum request size max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes # Local MQTT transport parameters mqtt: diff --git a/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java index 0cf84572c2..7e81cd9c34 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java @@ -40,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest @TestPropertySource(properties = { - "transport.http.max_payload_size=10000" + "server.http.max_payload_size=10000" }) public class RpcControllerTest extends AbstractControllerTest { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java index 8195bf19a7..3c31e7ea81 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java @@ -15,9 +15,15 @@ */ package org.thingsboard.server.common.msg.tools; +import lombok.Getter; + public class MaxPayloadSizeExceededException extends RuntimeException { - public MaxPayloadSizeExceededException() { - super("Payload size exceeds the limit"); + @Getter + private final int limit; + + public MaxPayloadSizeExceededException(int limit) { + super("Payload size exceeds the limit " + limit); + this.limit = limit; } } diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index e27f7cbc1b..8e78d489e2 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -45,6 +45,10 @@ spring-boot-starter-web provided + + org.springframework.boot + spring-boot-starter-security + org.slf4j slf4j-api diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 4825c8922b..73b1e5d09b 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -136,9 +136,6 @@ public class DeviceApiController implements TbTransportService { private static final String ACCESS_TOKEN_PARAM_DESCRIPTION = "Your device access token."; - @Value("${transport.http.max_payload_size:65536}") - private int maxPayloadSize; - @Autowired private HttpTransportContext transportContext; @@ -289,7 +286,6 @@ public class DeviceApiController implements TbTransportService { @PathVariable("requestId") Integer requestId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Reply to the RPC request, JSON. For example: {\"status\":\"success\"}", required = true) @RequestBody String json, HttpServletRequest httpServletRequest) { - checkPayloadSize(httpServletRequest); DeferredResult responseWriter = new DeferredResult(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -320,7 +316,6 @@ public class DeviceApiController implements TbTransportService { @PathVariable("deviceToken") String deviceToken, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The RPC request JSON", required = true) @RequestBody String json, HttpServletRequest httpServletRequest) { - checkPayloadSize(httpServletRequest); DeferredResult responseWriter = new DeferredResult(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -443,12 +438,6 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } - private void checkPayloadSize(HttpServletRequest httpServletRequest) { - if (httpServletRequest.getContentLength() > maxPayloadSize) { - throw new MaxPayloadSizeExceededException(); - } - } - private DeferredResult getOtaPackageCallback(String deviceToken, String title, String version, int size, int chunk, OtaPackageType firmwareType) { DeferredResult responseWriter = new DeferredResult<>(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), diff --git a/application/src/main/java/org/thingsboard/server/config/RequestSizeFilter.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java similarity index 61% rename from application/src/main/java/org/thingsboard/server/config/RequestSizeFilter.java rename to common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java index c2be2ed027..1b813f5d03 100644 --- a/application/src/main/java/org/thingsboard/server/config/RequestSizeFilter.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.config; +package org.thingsboard.server.transport.http.config; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -21,51 +21,31 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.util.AntPathMatcher; +import org.springframework.http.HttpStatus; import org.springframework.web.filter.OncePerRequestFilter; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; -import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import java.io.IOException; -import java.util.List; @Slf4j -@Component @RequiredArgsConstructor public class RequestSizeFilter extends OncePerRequestFilter { - private final List urls = List.of("/api/plugins/rpc/**", "/api/rpc/**"); - private final AntPathMatcher pathMatcher = new AntPathMatcher(); - private final ThingsboardErrorResponseHandler errorResponseHandler; - - @Value("${transport.http.max_payload_size:65536}") - private int maxPayloadSize; + private final int maxPayloadSize; @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (request.getContentLength() > maxPayloadSize) { if (log.isDebugEnabled()) { - log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), - request.getRemoteAddr(), request.getContentLength()); + log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), request.getRemoteAddr(), request.getContentLength()); } - errorResponseHandler.handle(new MaxPayloadSizeExceededException(), response); + handleMaxPayloadSizeExceededException(response, new MaxPayloadSizeExceededException(maxPayloadSize)); return; } chain.doFilter(request, response); } - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - for (String url : urls) { - if (pathMatcher.match(url, request.getRequestURI())) { - return false; - } - } - return true; - } - @Override protected boolean shouldNotFilterAsyncDispatch() { return false; @@ -75,4 +55,9 @@ public class RequestSizeFilter extends OncePerRequestFilter { protected boolean shouldNotFilterErrorDispatch() { return false; } + + private void handleMaxPayloadSizeExceededException(HttpServletResponse response, MaxPayloadSizeExceededException exception) throws IOException { + response.setStatus(HttpStatus.PAYLOAD_TOO_LARGE.value()); + JacksonUtil.writeValue(response.getWriter(), exception.getMessage()); + } } diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java new file mode 100644 index 0000000000..831aede1d1 --- /dev/null +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java @@ -0,0 +1,59 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.http.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@Order(SecurityProperties.BASIC_AUTH_ORDER) +@ConditionalOnExpression("('${service.type:null}'=='tb-transport')") +public class TransportSecurityConfiguration { + public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; + + @Value("${transport.http.max_payload_size:65536}") + private int maxPayloadSize; + + @Bean + protected RequestSizeFilter httpTransportRequestSizeFilter() { + return new RequestSizeFilter(maxPayloadSize); + } + + @Bean + SecurityFilterChain httpTransportFilterChain(HttpSecurity http) throws Exception { + http + .securityMatchers(matchers -> matchers.requestMatchers(DEVICE_API_ENTRY_POINT)) + .cors(cors -> { + }) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(config -> config + .requestMatchers(DEVICE_API_ENTRY_POINT).permitAll()) + .addFilterBefore(httpTransportRequestSizeFilter(), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } +} diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 827ccd1531..91bd875819 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -170,7 +170,7 @@ transport: request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" # HTTP maximum request processing timeout in milliseconds max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" - # Maximum request size + # HTTP maximum request size max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes sessions: # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. From 4169da4c08c51a150319236f868e3cbfadbda72c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 1 Aug 2024 16:10:39 +0300 Subject: [PATCH 13/80] configurable max payload size by url pattern --- .../ThingsboardSecurityConfiguration.java | 27 ++------------- .../src/main/resources/thingsboard.yml | 8 ++--- .../controller/ImageControllerTest.java | 13 +++++++ .../server/controller/RpcControllerTest.java | 2 +- .../server/system/BaseHttpDeviceApiTest.java | 22 +++++++++--- .../MaxPayloadSizeExceededException.java | 4 +-- .../transport/http/DeviceApiController.java | 20 ----------- .../http/config/RequestSizeFilter.java | 34 +++++++++++++++++-- .../TransportSecurityConfiguration.java | 12 ++++--- .../src/main/resources/tb-http-transport.yml | 4 +-- 10 files changed, 81 insertions(+), 65 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 05693c5e30..db497875e4 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -84,10 +84,8 @@ public class ThingsboardSecurityConfiguration { public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download"; - @Value("${server.http.max_payload_size:16777216}") - private int maxPayloadSize; - @Value("${transport.http.max_payload_size:65536}") - private int httpTransportMaxPayloadSize; + @Value("${server.http.max_payload_size:/api/image*/**=52428800;/api/**=16777216}") + private String maxPayloadSizeConfig; @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; @@ -131,14 +129,9 @@ public class ThingsboardSecurityConfiguration { @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; - @Bean - protected RequestSizeFilter httpTransportRequestSizeFilter() { - return new RequestSizeFilter(httpTransportMaxPayloadSize); - } - @Bean protected RequestSizeFilter requestSizeFilter() { - return new RequestSizeFilter(maxPayloadSize); + return new RequestSizeFilter(maxPayloadSizeConfig); } @Bean @@ -217,20 +210,6 @@ public class ThingsboardSecurityConfiguration { } @Bean - @Order(1) - SecurityFilterChain httpTransportFilterChain(HttpSecurity http) throws Exception { - http - .securityMatchers(matchers -> matchers.requestMatchers(DEVICE_API_ENTRY_POINT)) - .cors(cors -> {}) - .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(config -> config - .requestMatchers(DEVICE_API_ENTRY_POINT).permitAll()) - .addFilterBefore(httpTransportRequestSizeFilter(), UsernamePasswordAuthenticationFilter.class); - return http.build(); - } - - @Bean - @Order(2) SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.headers(headers -> headers .cacheControl(config -> {}) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 496b151ff0..27e054175e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -52,8 +52,8 @@ server: key_password: "${SSL_KEY_PASSWORD:thingsboard}" # HTTP settings http: - # Maximum request size - max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:16777216}" # max payload size in bytes + # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. + max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/image*/**=52428800;/api/**=16777216}" # HTTP/2 support (takes effect only if server SSL is enabled) http2: # Enable/disable HTTP/2 support @@ -963,8 +963,8 @@ transport: request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" # HTTP maximum request processing timeout in milliseconds max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" - # HTTP maximum request size - max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes + # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. + max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/v1/*/attributes=52428800;/api/v1/**=65536}" # Local MQTT transport parameters mqtt: # Enable/disable mqtt transport protocol. diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java index e272a40857..74ee06ee4a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ImageDescriptor; @@ -42,9 +43,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest +@TestPropertySource(properties = { + "server.http.max_payload_size=/api/image*/**=3000;/api/**=60000" +}) public class ImageControllerTest extends AbstractControllerTest { private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("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"); + private static final byte[] PNG_IMAGE_5KB = Base64.getDecoder().decode("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"); private static final byte[] JPEG_IMAGE = Base64.getDecoder().decode("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"); private static final byte[] SVG_IMAGE = Base64.getDecoder().decode("PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiMyNzg2MjJ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+Cg=="); @@ -269,6 +274,14 @@ public class ImageControllerTest extends AbstractControllerTest { assertThat(systemParams.getMaxResourceSize()).isEqualTo(0); } + @Test + public void testExportLargeImportImage() throws Exception { + String filename = "my_png_image.png"; + uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE_5KB); + ImageExportData exportData = doGet("/api/images/tenant/" + filename + "/export", ImageExportData.class); + doPut("/api/image/import", exportData).andExpect(status().isPayloadTooLarge()); + } + private TbResourceInfo updateImagePublicStatus(String filename, boolean isPublic) throws Exception { return doPut("/api/images/tenant/" + filename + "/public/" + isPublic, "", TbResourceInfo.class); } diff --git a/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java index 7e81cd9c34..4980ebc159 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RpcControllerTest.java @@ -40,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest @TestPropertySource(properties = { - "server.http.max_payload_size=10000" + "server.http.max_payload_size=/api/image*/**=10;/api/**=10000" }) public class RpcControllerTest extends AbstractControllerTest { diff --git a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java index 78e64b4f0a..e6baa5d8d8 100644 --- a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java +++ b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java @@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @TestPropertySource(properties = { "transport.http.enabled=true", - "transport.http.max_payload_size=10000" + "transport.http.max_payload_size=/api/v1/*/attributes=20000;/api/v1/**=10000" }) public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest { @@ -80,13 +80,13 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest { @Test public void testReplyToCommandWithLargeResponse() throws Exception { String errorResponse = doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/rpc/5", - JacksonUtil.toString(createRpcResponsePayload(10001)), + JacksonUtil.toString(createJsonPayloadOfSize(10001)), String.class, status().isPayloadTooLarge()); assertThat(errorResponse).contains("Payload size exceeds the limit"); doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/rpc/5", - JacksonUtil.toString(createRpcResponsePayload(10000)), + JacksonUtil.toString(createJsonPayloadOfSize(10000)), String.class, status().isOk()); } @@ -105,7 +105,21 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest { status().isOk()); } - private String createRpcResponsePayload(int size) { + @Test + public void testPostLargeAttribute() throws Exception { + String errorResponse = doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", + JacksonUtil.toString(createJsonPayloadOfSize(20001)), + String.class, + status().isPayloadTooLarge()); + assertThat(errorResponse).contains("Payload size exceeds the limit"); + + doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", + JacksonUtil.toString(createJsonPayloadOfSize(20000)), + String.class, + status().isOk()); + } + + private String createJsonPayloadOfSize(int size) { String value = "a".repeat(size - 19); return "{\"result\":\"" + value + "\"}"; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java index 3c31e7ea81..a62d5361ba 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java @@ -20,9 +20,9 @@ import lombok.Getter; public class MaxPayloadSizeExceededException extends RuntimeException { @Getter - private final int limit; + private final long limit; - public MaxPayloadSizeExceededException(int limit) { + public MaxPayloadSizeExceededException(long limit) { super("Payload size exceeds the limit " + limit); this.limit = limit; } diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 73b1e5d09b..d11feb03de 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -24,11 +24,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpHeaders; @@ -36,7 +34,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -44,7 +41,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceTransportType; @@ -53,7 +49,6 @@ import org.thingsboard.server.common.data.TbTransportService; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.rpc.RpcStatus; -import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; @@ -76,7 +71,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMs import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; -import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -626,20 +620,6 @@ public class DeviceApiController implements TbTransportService { } - @ExceptionHandler(MaxPayloadSizeExceededException.class) - public void handle(MaxPayloadSizeExceededException exception, HttpServletRequest request, HttpServletResponse response) { - log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), - request.getRemoteAddr(), request.getContentLength()); - if (!response.isCommitted()) { - try { - response.setStatus(HttpStatus.PAYLOAD_TOO_LARGE.value()); - JacksonUtil.writeValue(response.getWriter(), exception.getMessage()); - } catch (IOException e) { - log.error("Can't handle exception", e); - } - } - } - private static MediaType parseMediaType(String contentType) { try { return MediaType.parseMediaType(contentType); diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java index 1b813f5d03..7808a938be 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java @@ -22,28 +22,56 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; @Slf4j @RequiredArgsConstructor public class RequestSizeFilter extends OncePerRequestFilter { - private final int maxPayloadSize; + private final Map limits = new LinkedHashMap<>(); + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + public RequestSizeFilter(String limitsConfiguration) { + for (String limit : limitsConfiguration.split(";")) { + try { + String urlPathPattern = limit.split("=")[0]; + long maxPayloadSize = Long.parseLong(limit.split("=")[1]); + limits.put(urlPathPattern, maxPayloadSize); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse size limits configuration: " + limitsConfiguration); + } + } + } @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + for (String url : limits.keySet()) { + if (pathMatcher.match(url, request.getRequestURI())) { + if (checkMaxPayloadSizeExceeded(request, response, limits.get(url))) { + return; + } + break; + } + } + chain.doFilter(request, response); + } + + private boolean checkMaxPayloadSizeExceeded(HttpServletRequest request, HttpServletResponse response, long maxPayloadSize) throws IOException { if (request.getContentLength() > maxPayloadSize) { if (log.isDebugEnabled()) { log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), request.getRemoteAddr(), request.getContentLength()); } handleMaxPayloadSizeExceededException(response, new MaxPayloadSizeExceededException(maxPayloadSize)); - return; + return true; } - chain.doFilter(request, response); + return false; } @Override diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java index 831aede1d1..24cca843d8 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java @@ -27,27 +27,29 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @EnableMethodSecurity @Order(SecurityProperties.BASIC_AUTH_ORDER) -@ConditionalOnExpression("('${service.type:null}'=='tb-transport')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.http.enabled}'=='true')") public class TransportSecurityConfiguration { public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; - @Value("${transport.http.max_payload_size:65536}") - private int maxPayloadSize; + @Value("${transport.http.max_payload_size:/api/v1/*/attributes=52428800;/api/v1/**=65536}") + private String maxPayloadSizeConfig; @Bean protected RequestSizeFilter httpTransportRequestSizeFilter() { - return new RequestSizeFilter(maxPayloadSize); + return new RequestSizeFilter(maxPayloadSizeConfig); } @Bean + @Order(1) SecurityFilterChain httpTransportFilterChain(HttpSecurity http) throws Exception { http - .securityMatchers(matchers -> matchers.requestMatchers(DEVICE_API_ENTRY_POINT)) + .securityMatcher(AntPathRequestMatcher.antMatcher(DEVICE_API_ENTRY_POINT)) .cors(cors -> { }) .csrf(AbstractHttpConfigurer::disable) diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 91bd875819..e7a7fcc209 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -170,8 +170,8 @@ transport: request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" # HTTP maximum request processing timeout in milliseconds max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" - # HTTP maximum request size - max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes + # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. + max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/v1/*/attributes=52428800;/api/v1/**=65536}" sessions: # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # The parameter value is in milliseconds. From 936bb82334ee26ce424ac3350ec2682a937ffc08 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 7 Aug 2024 19:03:05 +0300 Subject: [PATCH 14/80] updated transport http limits, added logging for initialized filters --- .../ThingsboardSecurityConfiguration.java | 10 +++++----- application/src/main/resources/thingsboard.yml | 8 ++++---- .../server/controller/ImageControllerTest.java | 18 +++++++++--------- .../server/system/BaseHttpDeviceApiTest.java | 2 +- .../tools/MaxPayloadSizeExceededException.java | 2 +- ...tSizeFilter.java => PayloadSizeFilter.java} | 9 ++++----- .../config/TransportSecurityConfiguration.java | 12 ++++-------- .../src/main/resources/tb-http-transport.yml | 4 ++-- 8 files changed, 30 insertions(+), 35 deletions(-) rename common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/{RequestSizeFilter.java => PayloadSizeFilter.java} (89%) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index db497875e4..55b89b05f4 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -57,7 +57,7 @@ import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2Autho import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; -import org.thingsboard.server.transport.http.config.RequestSizeFilter; +import org.thingsboard.server.transport.http.config.PayloadSizeFilter; import java.util.ArrayList; import java.util.Arrays; @@ -84,7 +84,7 @@ public class ThingsboardSecurityConfiguration { public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download"; - @Value("${server.http.max_payload_size:/api/image*/**=52428800;/api/**=16777216}") + @Value("${server.http.max_payload_size:/api/image*/**=52428800;/api/resource/**=52428800;/api/**=16777216}") private String maxPayloadSizeConfig; @Autowired @@ -130,8 +130,8 @@ public class ThingsboardSecurityConfiguration { private RateLimitProcessingFilter rateLimitProcessingFilter; @Bean - protected RequestSizeFilter requestSizeFilter() { - return new RequestSizeFilter(maxPayloadSizeConfig); + protected PayloadSizeFilter payloadSizeFilter() { + return new PayloadSizeFilter(maxPayloadSizeConfig); } @Bean @@ -234,7 +234,7 @@ public class ThingsboardSecurityConfiguration { .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(requestSizeFilter(), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); if (oauth2Configuration != null) { http.oauth2Login(login -> login diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 27e054175e..f0caadf972 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -52,8 +52,8 @@ server: key_password: "${SSL_KEY_PASSWORD:thingsboard}" # HTTP settings http: - # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. - max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/image*/**=52428800;/api/**=16777216}" + # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. After first match all other will be skipped + max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/image*/**=52428800;/api/resource/**=52428800;/api/**=16777216}" # HTTP/2 support (takes effect only if server SSL is enabled) http2: # Enable/disable HTTP/2 support @@ -963,8 +963,8 @@ transport: request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" # HTTP maximum request processing timeout in milliseconds max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" - # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. - max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/v1/*/attributes=52428800;/api/v1/**=65536}" + # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. After first match all other will be skipped + max_payload_size: "${HTTP_TRANSPORT_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/v1/*/rpc/**=65536;/api/v1/**=52428800}" # Local MQTT transport parameters mqtt: # Enable/disable mqtt transport protocol. diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java index 74ee06ee4a..ed445f2979 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -49,9 +49,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public class ImageControllerTest extends AbstractControllerTest { private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC9FBMVEUAAAABAQEBAgICAgICAwMCBAQDAwMDBQUDBgYEBAQEBwcECAgFCQkFCgoGBgYGCwsGDAwHBwcHDQ0HDg4ICAgIDw8IEBAJCQkJEREKEhIKExMLFBQLFRUMFhYMFxcNDQ0NGBgNGRkODg4OGhoOGxsPDw8PHBwPHR0QEBAQHh4QHx8RERERICARISESEhISIiITExMTIyMTJCQUJSUUJiYVKCgWFhYWKSkXFxcXGhwYGBgYLC0ZGRkaMDEaMTIbGxsbMjMcMzQdNTYfOTogICAgOzwiP0AiQEEjIyMjQkMkQ0QnJycnSEkoS0wpKSkrUFErUVIsLCwvV1gvWFkwWlszMzMzYGE1NTU2NjY3Zmc4aWo5OTk5ams5a2w6Ojo6bG07bm88cXI9cnM9c3Q/dndAQEBAeHlBeXpCQkJCe3xCfH1DQ0NEREREf4FFRUVFgIJGg4VHhYdISEhIhohJh4lLi41LjI5MTExMjpBNj5FNkJJOkpRQUFBQlZdRUVFSUlJTU1NTmpxUVFRUnZ9VVVVVnqBWVlZYpadZWVlZp6laqKpbW1tbqatbqqxcXFxcrK5dra9drrBeXl5er7FfsbNfsrRgs7VhYWFiYmJiuLpjubtku71lvL5lvb9mvsBnwcNowsRpxMZpxcdra2tryctsysxubm5uzc9vb29vz9Fw0dNx0tVy1Ndy1dhz1tlz19p0dHR02Nt02dx12t1229523N93d3d33eB33uF5eXl54eR6enp64+Z65Od75eh75ul8fHx85+p86Ot96ex96u2AgICA7vGA7/KB8fSC8/aD9PeD9fiEhISE9/qF+PuF+fyGhoaG+v2G+/6Hh4eH/P+IiIiMjIyNjY2Ojo6QkJCRkZGSkpKTk5Obm5ucnJyfn5+lpaWnp6eoqKipqamqqqqwsLCzs7O1tbW4uLi5ubm6urq7u7u8vLy/v7/BwcHCwsLFxcXGxsbPz8/Y2Nji4uLj4+Pv7+/4+Pj5+fn+/v7/75T///+GLm1tAAAAAWJLR0T7omo23AAABJtJREFUeNrt3Wd8E3UYB/CH0oqm1dJaS5N0IKu0qQSVinXG4gKlKFi3uMC9FVwoVQnQqCBgBVxFnKCoFFFExFGhliWt/zoYLuIMKEpB7b3xuf9dQu+MvAjXcsTf7/PJk/ul1/S+TS53r3KkNFfk0V6evDHbFGruQ3EQTzNVUFxkHOXFB6QbIQiCIAiC/GeSs/QkR6vkCPeUaNUeSUjkkdR1npCp6a7VV7U6P1dbKfNFrS89rJNas/T6rlZtkUS/i2evhw99Q92y9/r7nVzzw7VfeDX3y2qv893plTVb1uW+uw6xiyNpspAQ8bjLy8l5REiImOlUq3Pniunyxw8Ib+vqF7aB5AgdItLVmit0iOgc9W0owhDt1RSAABL3EGeDDqmXhwRXgw6pj3qESFhtgHC1DYSGrJCQjweFq4SEqzkD67zGah8Inay+p1yl4XqKWt2lF69UDxQrzzevXZprrDn2gfTIUs85Iv/oHpny8HKHdugeVZhpXNudu6u6J1P8lmpIX1ys10X6myVfPeLl919UZFi74JXjWtfCecfa5sj+odx908XSg9Taqdaw+3I1QuYLA6RG2AbiEDpE9JJnvcYP1BRhgiw3QuoAASTuIQnP6JCF8hQlcbYBwrWIKgPDIg9UGSGP2QdCnZ+QkDneKQs4swqe1CDJ09RaXfBUETWKm3a+gFMMEMc0+0AoJVX9nM1+VDsCznLurz64b5VWq7nWLLi81QfygYZfNlU7nAUP0nOwrLnGiiAIgiAIgiAIgiDI/zstLS3tMEtKSiycgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBYAkEQBEEQBEEQBEEQBGmrdLwuyLmhg703km8Z63k7N2Tw0jnqFt/f0bROn69WBYOfbuxiyR+8MXC9vB8QCBTQkEAgMOG2gVyvDmTzdAWuifFp077m8f503vwZr/PSd28Hg+uaTjVDlOFEIxVrINVijfwi4glCHE1XioXPz6kX9xHNFIUkvyM/xqeduIPHup95bGni8edYotOUqJCrrII0iMv4LnNFg4Sczd/9/Zw4abchD0Ygv0pIBVFZG0Nq587lu/PE02EIXSQuaSfI92l88bfNFkHqLxUnEM1+bXQEMloMY8hgn893esyQIzbzWHtveXn51GW89AtfTeyATWZIWm919s6wBtLYdfXdVCyuuEdCHhoxwr/mAzdDtMQKoaP4duQmRVG+kUtyu83X3OuylX09f+9r0c6eOvkjx82fdPdLiHrdjsrD1Z39LP5W06ExQ475g8eqSR6PZ+oXvLSVNWk/nmmGKNcSXaBYBXEPFkMXV1GlhFyYlSof3t19ZOxfPJp+4/HTeh47JhGdqLQxJDtpyRJxBgUi+0g7QkYSlVsHoVtFrcNiyO0SsoXHDxIykej4v/8F+XxDKLRxmXWQfo2jyGJIh894PDs9FArNeIGXvlwbCn37Upl5rXObOMPtf1K4z5u8ne/sx0tl6hbfgtNkBEGQPZs4uUBwTxoTH5DxtM0TD46+20lpHrfXX7e52/jtyj9kFKbIT2L3FQAAAABJRU5ErkJggg=="); - private static final byte[] PNG_IMAGE_5KB = Base64.getDecoder().decode("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"); private static final byte[] JPEG_IMAGE = Base64.getDecoder().decode("/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHCBYVFRUSEhUYGBgaGBocGRwYGhgYHBgSGBwZGRoYGhghIS4lHB4tHxgZJjorKy8xNTU1GiRIQDszPy40NTEBDAwMEA8QHhISHjsrJSxAOjY/PTE/ND86NzY3NDc0NDQ0NjQ0PTQ2Nj00NDQ0NDQ0Nj42MTQ0NDQ0NjQ0NDQ0NP/AABEIAOEA4QMBIgACEQEDEQH/xAAcAAEAAgMBAQEAAAAAAAAAAAAABgcCBAUDCAH/xAA9EAACAQMABwUFBwQBBAMAAAABAgADBBEFBhIhMUFRByJhcYETMlKRoRRCYnKCsdEjksHw8TOi0uFzssL/xAAaAQEAAwEBAQAAAAAAAAAAAAAAAQMEBQIG/8QAKhEBAAICAgAEBQQDAAAAAAAAAAECAxEEIQUSMUETMlFhoSKBsfBCcZH/2gAMAwEAAhEDEQA/ALmiIgIiICIiAiIgIiICIiAkE072h06dRraypNdVlyG2CBTRhuwz8OIImr2i6ysGGj7Zyjsu1Xdfep0jwRTyZt+/kMyM6KCUkFOmoVR05nqTzMDuLpPS1bJNe2thyCUzUYDoSxxnymxQfSyDK3lvXPw1KRTP6kO4+k1Le78Z07e7gbOjdeQtRbfSNE2tRjhHJDUah/DU4A8Nx68pNZBdIW1O5pPQrKGRhz4qeTDoRI92ca1vQuH0NeMW2HK0HY53D3UJ6Fd6+eOkC3IiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAmnpO+WhRq1392mjO35UUsQOp3TckA7Z7409Gug41alNPHZDbZ/wDoB6wKlt9JtVd7iocvVdnb14AeAAA9J1Le78ZC7G4OyB0nVt7uEplb3fjOlb3chtvdzp293AmVvd+MqXXW7zpCtVpkgq1PBG4h0RBkeIZZLNIadWhTLk5bgo6ty9JWVaqWZmY5LEknqScmEPqzVDTP2yzoXO7adBt45VV7rjHIbQOPAidyVf2E3ZayrUjwp1jjydQf8S0ICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAkC7StEi7azoMSED1HcDcWVVVQM8slh6Zk9kN18vfZKzqcOtvUZeeMNTXax0BYH0gVPrlqaLan9ptshFwHUkthScBgTvxnjIYl0Oe4y4dXNIJcmrYu7Vg1JmVn2SxTcjoxUAH31I3cz0lL31sadSpSPFHZfPZJGfpA6ltdbTBVyzHgFBYnyA3zo3r3FBdpreoo+J1IUeP8AziWR2d6t07W2W5qACpUQM7t9ynxCD4d289T5Cd+lpO2rsaIcMWBwrqQHXns7Qw3pBt85XV01Rtpzk/QDoBPCSvtB1dFlc4pjFKou3T/DvwyZ54OD5MJFIF0dgFTu3a+KH6ES5JUPYCn9K7b8aDx90mW9AREQEREBERAREQEREBERAREQEREBERAREQMTK07SbhRd2dFzspXpV6DnGSqVgFD/AKXFNv0yyyZC7ista5BrKCAxUDooJA38eOCZm5HJjBEdbmel2HDOTf27Vn2faOrUdJqKylBSWqKhIIUnZ2RhuDAsVImjrNqddVLm4rUqWabOzL3hkjw85e32GmpyqAHrvP7zzdJiy87LX0iI/KyuKk/VCe0i6alo63NPcGq0lf8AKEdtk9O+iytquslQqvxo6shHEOpHDzGQfAmXPpvRSV6L0KmdhwMgbsEHIYdCDvkL0J2eUaVwtavWZ0RgyLs4yw3jaPQHfu44lmDxPFf9N+p/Dzbj2juvcNbtpxsWZONomofHGE2vTOz9JU8nnapc161zttSdKKDYp5G4jOS5I3AseXQCQMzo1tFo3WdwotWazqYXt2CJi1uG61h9FAlqyp+wOpm3ulzwqqcdAV/9S2JKCIiAiIgIiICIiAiIgIiICIiAiIgIiICIkT1z13t9HriodusRlKSnvHozfCvifSBs66ayJYWz12wXIK0lP36pG4eQ4nwErPUXWBrhdm4bL7eyWPHbbLKx8zkHyleazaxV76sa9w2TwVR7qJ8Kj9zzlj6gaAAsyXXv1O/jnsDco8N2/wA5l5mH4mKdRuY7aONk8l43OonpY1LSP3agII3E9fGe4rKeBkXoXVwgA7tZRwFUYZR0DD/M2l0pU5WgB/8Ak7v8z53Ja0x1aP39Wy2LU9R/x2axB4b5yLyuinDOoPQkZ+XGFoV62522V+GllR+p+J9MTfttAoowNlPyjefM854xcLJl+WJn7+kJi9cfzSj9YK4KkBlIwQQcEHwMpPWSxFG5q0l90Nu8FIDAegOPSW3rbfvbUGrU1DMrAd7OACcZwJS11Xao7O5yzEknqTvnc8P42XDNvP6KeXnx5IiK+qyOw3S607upbOce2TuZ4e0TJwPErn+2X7Pjm2rtTZaiMVZCGVhuKspyCD1zPoXs57QFvlFCvhLlV8hVA4so5HmR/idNgWDERAREQEREBERAREQEREBERAREQERODrZp37JRBQbVao2xRT4qh5n8KjLHwEDk6765LaI9Kky+2CgsdxFFW4Fhzc/dXnx4T5x0hdtWqPVdizOxJLEsxJ6nmZ2NatKF3aktQuAxao541rk++5/COCjgAN0jwGdwgdrVXQrXdwlMZ2QdqoelMHePM8B5+Evq3QJshRgAAADgFG7Hykf1G1d+yW4DjFV8M/geSen75kqSkTwBPlJQzNJH3kDz5zJLRBy+s9Es35K3yxMzTdeKH5GU24+K0+aaxM/6WRlvEaiZeqHG4T9qV9lSx/0zWNbwmtWYtxlsRp4R7WCy9tb105sjfPGRKCIxuM+lWSUBrTY+wu7ilyDkj8j4df8AtYSZHInta3D03WpTYq6EMrDcVYHIIPXM8ZkjEEEcQcjzEgfTXZ3reukbfabAr08Cqo68nA+E4+eZMJ8wau6XeyrUtI0B3NrYroOGGwWXwDDvL0I8MT6VsL1K1NK1IhkdQykc1IzA2oiICIiAiIgIiICIiAiIgIiIGDMACScAcSekofXjWU1Gq3QO5tqhaj4aCnFSrjkXYHf0Cyyu0jSppWwoUzipct7JSOKoRmo48lz6kShtYn9rWdE3U6CbK44AIADgZxktu8gIEdk+7MNXfbVftVRcpSPcBHvVeIPjj98dJBqFJnZUUZZiFAHNicAfMz6X1P0Itrb0qQwSi4JG7ac++3qc+kDq2mjhxqfL+Z00UDcAB5TzUz0WBmDMgZgJlA861ure8o8+BnKvNHld671+o852cxmBFHSVT2taM2Xo3IG5l9m35lyy+e4n+0S6dJWuydteB4+B/iQ/XLRX2i1q0wMsBtJ+Zd4kofP8QRMtg4zjd18ZCXW1dulWp7Or/wBKsPZ1PAE91x0Ktg585b3ZPpd6FSroi5bvIS9An7ycWUeneA/N0lEyxq94xt7LS1L/AKtuyq+PvKDsnPhy/VA+hompoy+WvSp16ZytRFZfJhmbcBERAREQEREBERAREQERPKtVCqzHgoJPkBmBTmvGlfa6RrtnuWlHYXp7ZxtufPgv6BK4akVsmrH3q1xs+aIu23/cy/IzrVLs1KF7cMcmrVqNnqDjH7mY6fttnRWi3HAtc7XHexqHB/tXEDPsv0V7a7FRhlaS7X6z3V/yfQT6EpDAA6Spex+kBQqvjeagGfBQP5lroYGyhnss10M90MD0E/cT8WZmBjMcz9MwJgKqBlKnmJG6y4JHSSMmcK/Hfbz/AHgfO2uWjvs97cUwMLt7S/kfvgDyzj0mvoqh7WncU+a0zVXnvpkbQHmpJ/TJV2uW+LijU+Ons/2Mf/Kcfs6oh7+hTb3XFVGHVWpVARAjEsPs7YVbe6tG3qf2dSu71XMr+qhVmU8QSD5g4MmXZm5FaqORQfMN/wCzAs3sX0qWtqtnU9+2qFev9NicfJlceQEsqUZ2aXnstNXFEHu1VqDHV1xUB88B/mZecBERAREQEREBERAREQE4muN2aVjdVBxWjUI89kgCdksBvO6QztbrFdF3JHP2a+jVFB+hgUno0bVk6DiQ/wA8yTXtmK+rlvUXe1vVfaA6GpUB9Nl1M4epuj3rUmVRu2zx5ggZA/3nJr2fWpprd6Juh3KoZ06EEBHAHxDuH0niMtJt5N9/R7mlvL5tdMOypQLLPWq+fkssig+QDK47PqLW4ubKpuejWO7qrAYYeBAB9ZOrapjyljw66NPZGmmjz2R5A3Fafu1NdXmW3A9S0wLTzLzEvAzLThXj5Zj4zpXVfZXxPCcZ2gVZ2wnv235an7pNXsbsPaaRR+VNHbPiRsAfJj8pj2s3O1c0qfwU8+rsf/ESSdntP7Boq80k256iMKfkoKp83OfSBVGkHDVajKcguxB6gkkSU9nad+s/RVHzJP8AgSGye6sKKFs1Rt2Qzn8oG76CBjqXdE6douOdaovoUdD9J9Iz5i7MVNTS1rniXdj+mm7n9p9OwEREBERAREQERED8n4xwMyN6Q0lUas9KnUWkqYySu0WJ3+gnhcaQuSpp5pnO7bXPA9F6zFk52KkzEz6L64LTqfq8KINy5qVRtLkhF2mVVUHGdx4mautOrjVbd7anUYUn2SysdrZKsHBRjwGRwnZ0Vb7CgDhgAfzOmDOTj5F53bzdz+F99ROojpFdTNWxQpCmd4X5k7ycnxJJ+U4faQGtilzQ9+myup8AcEHqCpYeUsld0jOu+j/a0ifDB8t/8/SescxW9ck9zvuXmLTaZj2mNI9baRp3Hs9J0DjKCncJzVc5BYfgbO/4WJ5SRo0oyy0jWsKzPS3oSQ6H3Tg4Knp4GWjqzrBRuFApNggDuH3kHwkcwOAPhPoY7Y5hLaVbE3Eq5nIR57K8lDqipM9uctax6zL7QYS6W3PGrcgeJmg9YnnPJngZ1qpJyZrO0O84ust+1K3c0wS7DYRRxNR9wx88+kIVnWsm0npSoiE7BchmG8LRp4TaHmF3eLTs9qWsCYp6MtsCnR2dsLwDKMLT/SOPj6zVbSqaMt2t6DBrtx/VdcEU2+EHmR0675BUps7YGSxO/PXmSZCWxoqyNWoE5cWPRf5PCSfWW7CUPZru28KAOSLvP7Aesw0ZbrSXZHHix6n+JHtM3vtahIPdG5fLmYE67C9Hl7565HdpUW39KlQhVHqu38p9AyvuxzQP2exFVhh67bZ6imNyD5ZP6jLBgIiICIiAiIgIiIEZ1g0a4f7TSG1uAdRxIHBl6kTQtqocBlOR/u6TORXTll7BvtCDuMcVAPuseD+XIzi+JcGLROWnr7tvHz/4W/ZsW1TkfSb6tOOjzap1+s4OPNNf0ytvTfcOjtTzrIGUqeBnkKk/dubqZImNSp8swpjX3Q5o1TUA7pOG/wDyflu9JChSKtt0mZGHAqSvyI3iX1rfo5a1FsjkQfLr6f4lC1QVZkPFWKn8ykg/UTucDN56eWfWP4V5661aPf8AlPNW9f8A3aN93W4CqB3W8XA4eY3eUsJKoIBBBBGQRvBB5gz58feMGWJ2Y6TZ6D0WOfZsNnwRuA+YM3s6wxUj2k1Q8bcIbLVMcTMFba93f6jGOpPACatjYtcs3wqcb+A8fE/xIjrRWVbxrIt/QpqjVFGQKlRt+H371A5dZlx8n4mSaxHUe662KK1iZnv6JhTuabsUp16DuPuJWpM2emyGzK67RNYKlOt9mRWQou9iCGDMN+xnh3TjaHUzt0rayddhrenjyxj1mppuzFKkSpNe2Hv0KpLNTU/eo1D3kx04TSq0rGhalt5OB14ztWdNUGF9TzM5ekKaU6h9g5ZCAVJ3MAfuuOG0OB5HGRuMwa+bGBuPM/xA39KaR3Gmp3ncx6DpPXUnQBvrylbgHZztVD8NFcFj67lHiwnB3k9SfrPo3sq1T+xW3tKq4r1gGfPFKf3afhxyfE+ECcUqYUKqjAAAA6KBgCesRAREQEREBERAREQE8q9EOrIwyCCCPAz1iBB0RqLtbt93eh+KmeHqJsh52NO6M9soKbqi70P7qfAyN0q5JKsCrruZTxB/ifLeI8KcV5vWOpdTBkjJX7t0VCOEyF11moXmLPOfWbV9Gj4cS2bmurKV375QWtqFLurjnsn1KjP1BMu6rUwCTylF613Ae6qkHIBA9VAB+uZ3PCZta9pn6MnLrFaRH3ctq5IxLP7NLApbvVPGo278i7h9SZWlvZVH3pTdx+FWb9hN3R2lrm0b+mzJzKsDsnzUzuucvGY1XwpPhK1o9o9UDDUEY9QzLn03yZao3tW+AepTVE2sgAk5UcyT4/tKuRljFSZ/u3vFTzW17J7qzR2aC54sSTKP7S3alpSu2/DBG81Kjf8AMGfQNNQoCjgBiQLtH1SF1isoO0BgsBkqRwJHNf4nO42WMdtWWX7mZVPbaWI3hh85tVdMNVQ0lO4++fw9BPOnqTWL7JqJjPFdpj/bgb/WWVqvqfTt0DVEBPEB8Ek/E3TymzNy8eOu97eaY7WnSs0oIOCj5ZmvpC1DqdkDaG8cB6SV9paCiVemqqS2DgDBGCeHpIlq/ZVr65pWy5w7DaKjctMHvMfADP0luHLGWkWiNbeclPJbSw+y3s7baS+vkwBhqNNhvLcRUYeHED1l0xEteCIiAiIgIiICIiAiIgIiIH5ORpfQq1u8DsVBwYfsw5idcT8ni9K3jy2jcJraazuEFubatS3VKZI+JO8p9OImobteAyT0wc/KWLMPZjoPlOZfwnHM7rMw2051ojuNoZY6Eeuc1AyUvkzeXw+c3rHUDR1I5W1RjnOam1UOeOcuTJTE34MFcFdVZsuW2Sdy8qNBFGEUKPwgD9prX2iaFdSleilRTxDKp/xN+JeqUV2h9mQt1N1ZEmltDbpnLGmCcbatxZd+/O8dSOE91LslpURgAYAUeQH/ABJheU9qm69VI+YIkX1fq/0gOYOD5zm8+Z3X6dtWCN1l3NqfheapqTBqs4+S6yKDogOQqg9cDM1bipP2rcTQq1MzNa8tOPG4Ot+rZvKWVzkHOQMkEdRzGJ1NSrex0dT2E2/aMB7SpUQhmPQBc7K9Bk+JPGZrcshypI/3pMKtyze8R8gJ0OPzrYqRWPy9X4fntuye21ytRQyEMp5ie0jGpiHZqt90sNnoSBvIkmnew3m9ItMa25WWkUvNYn0ZRES1WREQEREBERAREQEREBERAREQEREBERAxMhOkqJt67bO5KhLKeW195fn+8m01L+xSspSoMjl1B5EHkZm5WCM1Ne/stw5Ph23PoigvW8Ji1wTxMwvdE1qJ901E5MoywH4lmityp5/PdPms3Hy0nVol2cc47xust5qk8Hea7XI4ZyeQG8nyE6ej9BVa2C+aaePvMPAcvWTh4t8k6iHq+THjjdpc5dpm2EUu3Rd/z6Ts2WrLvg122V+Fd5Pm3KSSw0fTors01A6nmT1J5zcnbweHUp3buXNzc69+q9Q8regqKERQqgYAHIT1ifs6MRphIiJIREQEREBERAREQEREBERAREQEREBERA/IiIH4ZBdcPfiJj5nyNXF+d6al+83+9JNhERw/kRyvnfsT9ibGYiIgIiICIiAiIgIiIH//2Q=="); private static final byte[] SVG_IMAGE = Base64.getDecoder().decode("PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiMyNzg2MjJ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+Cg=="); + private static final byte[] PNG_IMAGE_5KB = Base64.getDecoder().decode("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"); @Autowired private TbResourceRepository resourceRepository; @@ -197,6 +197,14 @@ public class ImageControllerTest extends AbstractControllerTest { assertThat(downloadImage("tenant", filename)).containsExactly(PNG_IMAGE); } + @Test + public void testExportImportLargeImage() throws Exception { + String filename = "my_png_image.png"; + uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE_5KB); + ImageExportData exportData = doGet("/api/images/tenant/" + filename + "/export", ImageExportData.class); + doPut("/api/image/import", exportData).andExpect(status().isPayloadTooLarge()); + } + @Test public void testGetImages() throws Exception { loginSysAdmin(); @@ -274,14 +282,6 @@ public class ImageControllerTest extends AbstractControllerTest { assertThat(systemParams.getMaxResourceSize()).isEqualTo(0); } - @Test - public void testExportLargeImportImage() throws Exception { - String filename = "my_png_image.png"; - uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE_5KB); - ImageExportData exportData = doGet("/api/images/tenant/" + filename + "/export", ImageExportData.class); - doPut("/api/image/import", exportData).andExpect(status().isPayloadTooLarge()); - } - private TbResourceInfo updateImagePublicStatus(String filename, boolean isPublic) throws Exception { return doPut("/api/images/tenant/" + filename + "/public/" + isPublic, "", TbResourceInfo.class); } diff --git a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java index e6baa5d8d8..4bfc998261 100644 --- a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java +++ b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java @@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @TestPropertySource(properties = { "transport.http.enabled=true", - "transport.http.max_payload_size=/api/v1/*/attributes=20000;/api/v1/**=10000" + "transport.http.max_payload_size=/api/v1/*/rpc/**=10000;/api/v1/**=20000" }) public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java index a62d5361ba..28ff759205 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/MaxPayloadSizeExceededException.java @@ -23,7 +23,7 @@ public class MaxPayloadSizeExceededException extends RuntimeException { private final long limit; public MaxPayloadSizeExceededException(long limit) { - super("Payload size exceeds the limit " + limit); + super("Payload size exceeds the limit of " + limit + " bytes"); this.limit = limit; } } diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/PayloadSizeFilter.java similarity index 89% rename from common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java rename to common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/PayloadSizeFilter.java index 7808a938be..708e9a2264 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/RequestSizeFilter.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/PayloadSizeFilter.java @@ -33,12 +33,12 @@ import java.util.Map; @Slf4j @RequiredArgsConstructor -public class RequestSizeFilter extends OncePerRequestFilter { +public class PayloadSizeFilter extends OncePerRequestFilter { private final Map limits = new LinkedHashMap<>(); private final AntPathMatcher pathMatcher = new AntPathMatcher(); - public RequestSizeFilter(String limitsConfiguration) { + public PayloadSizeFilter(String limitsConfiguration) { for (String limit : limitsConfiguration.split(";")) { try { String urlPathPattern = limit.split("=")[0]; @@ -48,6 +48,7 @@ public class RequestSizeFilter extends OncePerRequestFilter { throw new IllegalArgumentException("Failed to parse size limits configuration: " + limitsConfiguration); } } + log.info("Initialized payload size filter with configuration: {}" , limitsConfiguration); } @Override @@ -65,9 +66,7 @@ public class RequestSizeFilter extends OncePerRequestFilter { private boolean checkMaxPayloadSizeExceeded(HttpServletRequest request, HttpServletResponse response, long maxPayloadSize) throws IOException { if (request.getContentLength() > maxPayloadSize) { - if (log.isDebugEnabled()) { - log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), request.getRemoteAddr(), request.getContentLength()); - } + log.info("[{}] [{}] Payload size {} exceeds the limit of {} bytes", request.getRemoteAddr(), request.getRequestURL(), request.getContentLength(), maxPayloadSize); handleMaxPayloadSizeExceededException(response, new MaxPayloadSizeExceededException(maxPayloadSize)); return true; } diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java index 24cca843d8..8b119270b5 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/config/TransportSecurityConfiguration.java @@ -16,8 +16,6 @@ package org.thingsboard.server.transport.http.config; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -32,17 +30,15 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @EnableMethodSecurity -@Order(SecurityProperties.BASIC_AUTH_ORDER) -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.http.enabled}'=='true')") public class TransportSecurityConfiguration { public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; - @Value("${transport.http.max_payload_size:/api/v1/*/attributes=52428800;/api/v1/**=65536}") + @Value("${transport.http.max_payload_size:/api/v1/*/rpc/**=65536;/api/v1/**=52428800}") private String maxPayloadSizeConfig; @Bean - protected RequestSizeFilter httpTransportRequestSizeFilter() { - return new RequestSizeFilter(maxPayloadSizeConfig); + protected PayloadSizeFilter transportPayloadSizeFilter() { + return new PayloadSizeFilter(maxPayloadSizeConfig); } @Bean @@ -55,7 +51,7 @@ public class TransportSecurityConfiguration { .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(config -> config .requestMatchers(DEVICE_API_ENTRY_POINT).permitAll()) - .addFilterBefore(httpTransportRequestSizeFilter(), UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(transportPayloadSizeFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } } diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index e7a7fcc209..527715b8ca 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -170,8 +170,8 @@ transport: request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" # HTTP maximum request processing timeout in milliseconds max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" - # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. - max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/v1/*/attributes=52428800;/api/v1/**=65536}" + # Semi-colon-separated list of urlPattern=maxPayloadSize pairs that define max http request size for specified url pattern. After first match all other will be skipped + max_payload_size: "${HTTP_TRANSPORT_MAX_PAYLOAD_SIZE_LIMIT_CONFIGURATION:/api/v1/*/rpc/**=65536;/api/v1/**=52428800}" sessions: # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # The parameter value is in milliseconds. From bff310b6913ab750fb506884bb29515098177bc0 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 8 Aug 2024 17:31:05 +0200 Subject: [PATCH 15/80] DefaultTbLocalSubscriptionService WS single lock refactored by tenantId; TbEntityLocalSubsInfo performance optimizations for remove subscription --- .../DefaultTbLocalSubscriptionService.java | 36 ++++++++++++++----- .../subscription/TbAlarmsSubscription.java | 8 ++--- .../subscription/TbAttributeSubscription.java | 8 ++--- .../subscription/TbEntityLocalSubsInfo.java | 32 ++++++++++++++--- .../service/subscription/TbSubscription.java | 24 +++---------- .../subscription/TbSubscriptionUtils.java | 5 +-- .../subscription/TbSubscriptionsInfo.java | 9 +++-- .../TbTimeSeriesSubscription.java | 8 ++--- .../sub/AbstractNotificationSubscription.java | 5 +++ .../sub/NotificationsCountSubscription.java | 6 +++- .../sub/NotificationsSubscription.java | 6 +++- 11 files changed, 87 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 6e99731305..0d29d374a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.DeduplicationUtil; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardExecutors; @@ -120,12 +121,12 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer private String serviceId; private ExecutorService subscriptionUpdateExecutor; - private final Lock subsLock = new ReentrantLock(); + private final ConcurrentReferenceHashMap locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.SOFT); @PostConstruct public void initExecutor() { subscriptionUpdateExecutor = ThingsBoardExecutors.newWorkStealingPool(20, getClass()); - tsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-sub-callback")); + tsCallBackExecutor = Executors.newFixedThreadPool(8, ThingsBoardThreadFactory.forName("ts-sub-callback")); //since we are using locks by TenantId serviceId = serviceInfoProvider.getServiceId(); staleSessionCleanupExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("stale-session-cleanup")); staleSessionCleanupExecutor.scheduleWithFixedDelay(this::cleanupStaleSessions, 60, 60, TimeUnit.SECONDS); @@ -176,6 +177,10 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer }); } + Lock getSubsLock(TenantId tenantId) { + return locks.computeIfAbsent(tenantId, x -> new ReentrantLock()); + } + @Override public void addSubscription(TbSubscription subscription, WebSocketSessionRef sessionRef) { TenantId tenantId = subscription.getTenantId(); @@ -191,6 +196,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription); SubscriptionModificationResult result; + final Lock subsLock = getSubsLock(tenantId); subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); @@ -219,14 +225,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer log.debug("[{}][{}] Processing sub event callback: {}.", entityId, seqNumber, entityUpdatesInfo); entityUpdates.put(entityId, entityUpdatesInfo); Set> pendingSubs = null; - subsLock.lock(); + Lock subsLock = null; try { TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.get(entityId); if (entitySubs != null) { + subsLock = getSubsLock(entitySubs.getTenantId()); + subsLock.lock(); pendingSubs = entitySubs.clearPendingSubscriptions(seqNumber); } } finally { - subsLock.unlock(); + if (subsLock != null) { + subsLock.unlock(); + } } if (pendingSubs != null) { pendingSubs.forEach(this::checkMissedUpdates); @@ -238,12 +248,14 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer public void cancelSubscription(String sessionId, int subscriptionId) { log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); SubscriptionModificationResult result = null; - subsLock.lock(); + Lock subsLock = null; try { Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); if (sessionSubscriptions != null) { TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); if (subscription != null) { + subsLock = getSubsLock(subscription.getTenantId()); + subsLock.lock(); if (sessionSubscriptions.isEmpty()) { subscriptionsBySessionId.remove(sessionId); } @@ -255,7 +267,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer log.debug("[{}] No session subscriptions found!", sessionId); } } finally { - subsLock.unlock(); + if (subsLock != null) { + subsLock.unlock(); + } } if (result != null && result.hasEvent()) { pushSubscriptionEvent(result); @@ -266,18 +280,24 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer public void cancelAllSessionSubscriptions(String sessionId) { log.debug("[{}] Going to remove session subscriptions.", sessionId); List results = new ArrayList<>(); - subsLock.lock(); + Lock subsLock = null; try { Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); if (sessionSubscriptions != null) { for (TbSubscription subscription : sessionSubscriptions.values()) { + if (subsLock == null) { + subsLock = getSubsLock(subscription.getTenantId()); + subsLock.lock(); + } results.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false)); } } else { log.debug("[{}] No session subscriptions found!", sessionId); } } finally { - subsLock.unlock(); + if (subsLock != null) { + subsLock.unlock(); + } } results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java index a1d0aec29f..d59f8d74c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java @@ -36,12 +36,8 @@ public class TbAlarmsSubscription extends TbSubscription> subs = ConcurrentHashMap.newKeySet(); private volatile TbSubscriptionsInfo state = new TbSubscriptionsInfo(); @@ -88,6 +84,7 @@ public class TbEntityLocalSubsInfo { if (!newState.attrAllKeys) { if (attrSub.isAllKeys()) { newState.attrAllKeys = true; + newState.attrKeys = null; stateChanged = true; } else { if (newState.attrKeys == null) { @@ -104,6 +101,7 @@ public class TbEntityLocalSubsInfo { if (!newState.tsAllKeys) { if (tsSub.isAllKeys()) { newState.tsAllKeys = true; + newState.tsKeys = null; stateChanged = true; } else { if (newState.tsKeys == null) { @@ -137,8 +135,30 @@ public class TbEntityLocalSubsInfo { return toEvent(ComponentLifecycleEvent.DELETED); } TbSubscriptionsInfo oldState = state.copy(); - TbSubscriptionsInfo newState = new TbSubscriptionsInfo(); + + //copy unchanged state only + TbSubscriptionsInfo newState = state.copy(); + switch (sub.getType()) { + case NOTIFICATIONS: + case NOTIFICATIONS_COUNT: + newState.notifications = false; + break; + case ALARMS: + newState.alarms = false; + break; + case ATTRIBUTES: + newState.attrAllKeys = false; + newState.attrKeys = null; + break; + case TIMESERIES: + newState.tsAllKeys = false; + newState.tsKeys = null; + } + for (TbSubscription subscription : subs) { + if (subscription.getType() != sub.getType()) { + continue; // skip unchanged types + } switch (subscription.getType()) { case NOTIFICATIONS: case NOTIFICATIONS_COUNT: @@ -155,6 +175,7 @@ public class TbEntityLocalSubsInfo { var attrSub = (TbAttributeSubscription) subscription; if (!newState.attrAllKeys && attrSub.isAllKeys()) { newState.attrAllKeys = true; + newState.attrKeys = null; continue; } if (newState.attrKeys == null) { @@ -167,6 +188,7 @@ public class TbEntityLocalSubsInfo { var tsSub = (TbTimeSeriesSubscription) subscription; if (!newState.tsAllKeys && tsSub.isAllKeys()) { newState.tsAllKeys = true; + newState.tsKeys = null; continue; } if (newState.tsKeys == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java index d1822a6ffe..68c042ea51 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java @@ -15,41 +15,25 @@ */ package org.thingsboard.server.service.subscription; -import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; @Data -@AllArgsConstructor +@EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY, doNotUseGetters = true) public abstract class TbSubscription { + @EqualsAndHashCode.Exclude private final String serviceId; private final String sessionId; private final int subscriptionId; private final TenantId tenantId; private final EntityId entityId; private final TbSubscriptionType type; + @EqualsAndHashCode.Exclude private final BiConsumer, T> updateProcessor; - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TbSubscription that = (TbSubscription) o; - return subscriptionId == that.subscriptionId && - sessionId.equals(that.sessionId) && - tenantId.equals(that.tenantId) && - entityId.equals(that.entityId) && - type == that.type; - } - - @Override - public int hashCode() { - return Objects.hash(sessionId, subscriptionId, tenantId, entityId, type); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 2d8440fa20..d691b70c55 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -107,8 +107,9 @@ public class TbSubscriptionUtils { .type(event); if (!ComponentLifecycleEvent.DELETED.equals(event)) { builder.info(new TbSubscriptionsInfo(proto.getNotifications(), proto.getAlarms(), - proto.getTsAllKeys(), proto.getTsKeysCount() > 0 ? new HashSet<>(proto.getTsKeysList()) : null, - proto.getAttrAllKeys(), proto.getAttrKeysCount() > 0 ? new HashSet<>(proto.getAttrKeysList()) : null, + proto.getTsAllKeys(), proto.getAttrAllKeys(), + proto.getTsKeysCount() > 0 ? new HashSet<>(proto.getTsKeysList()) : null, + proto.getAttrKeysCount() > 0 ? new HashSet<>(proto.getAttrKeysList()) : null, proto.getSeqNumber())); } return builder.build(); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java index aa8ea70eab..7430d2e386 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java @@ -20,25 +20,24 @@ import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import lombok.ToString; -import java.util.HashSet; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; /** * Information about the local websocket subscriptions. */ @RequiredArgsConstructor @AllArgsConstructor -@EqualsAndHashCode(exclude = {"seqNumber"}) +@EqualsAndHashCode @ToString public class TbSubscriptionsInfo { protected boolean notifications; protected boolean alarms; protected boolean tsAllKeys; + protected boolean attrAllKeys; // primitives first for equals performance protected Set tsKeys; - protected boolean attrAllKeys; protected Set attrKeys; + @EqualsAndHashCode.Exclude protected int seqNumber; public boolean isEmpty() { @@ -50,7 +49,7 @@ public class TbSubscriptionsInfo { } protected TbSubscriptionsInfo copy(int seqNumber) { - return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, tsKeys, attrAllKeys, attrKeys, seqNumber); + return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, attrAllKeys, tsKeys, attrKeys, seqNumber); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java index 10a24106ad..0fc9a80122 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java @@ -53,12 +53,8 @@ public class TbTimeSeriesSubscription extends TbSubscription extends TbSubscription super(serviceId, sessionId, subscriptionId, tenantId, entityId, type, updateProcessor); } + @Override + protected boolean canEqual(final Object other) { + return other instanceof AbstractNotificationSubscription; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java index 1e812c3b31..491acedb1e 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java @@ -23,7 +23,6 @@ import org.thingsboard.server.service.subscription.TbSubscription; import org.thingsboard.server.service.subscription.TbSubscriptionType; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; @Getter @@ -35,6 +34,11 @@ public class NotificationsCountSubscription extends AbstractNotificationSubscrip super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS_COUNT, updateProcessor); } + @Override + protected boolean canEqual(final Object other) { + return other instanceof NotificationsCountSubscription; + } + public UnreadNotificationsCountUpdate createUpdate() { return UnreadNotificationsCountUpdate.builder() .cmdId(getSubscriptionId()) diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java index 97dc15f6b0..867f48b72e 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java @@ -30,7 +30,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -48,6 +47,11 @@ public class NotificationsSubscription extends AbstractNotificationSubscription< this.limit = limit; } + @Override + protected boolean canEqual(final Object other) { + return other instanceof NotificationsSubscription; + } + public UnreadNotificationsUpdate createFullUpdate() { return UnreadNotificationsUpdate.builder() .cmdId(getSubscriptionId()) From b5be7af40f23c66ee75cdfd5c4fb970b92fb0843 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 21 Aug 2024 12:07:20 +0300 Subject: [PATCH 16/80] Fixed SYNC issue with redundant name fields in server and broker config --- .../broker-config-control.component.ts | 5 +---- .../opc-server-config/opc-server-config.component.ts | 1 - .../lib/gateway/gateway-connectors.component.ts | 11 ----------- .../metadata/connector-default-configs/mqtt.json | 1 - .../metadata/connector-default-configs/opcua.json | 1 - 5 files changed, 1 insertion(+), 18 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index d4bfdd4694..e44506dccc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -33,7 +33,6 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TranslateService } from '@ngx-translate/core'; import { generateSecret } from '@core/utils'; import { Subject } from 'rxjs'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; @@ -74,10 +73,8 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid private destroy$ = new Subject(); constructor(private fb: FormBuilder, - private cdr: ChangeDetectorRef, - private translate: TranslateService) { + private cdr: ChangeDetectorRef) { this.brokerConfigFormGroup = this.fb.group({ - name: ['', []], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], version: [5, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index 5b14f16db2..5b63689c3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -77,7 +77,6 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator constructor(private fb: FormBuilder) { this.serverConfigFormGroup = this.fb.group({ - name: ['', []], url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]], scanPeriodInMillis: [1000, [Validators.required, Validators.min(1000)]], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index e7e4d05e45..2915cb831e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -147,8 +147,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } ngAfterViewInit(): void { - this.observeName(); - this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = this.getSortingDataAccessor(); @@ -533,15 +531,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.connectorForm.disable(); } - private observeName(): void { - this.connectorForm.get('name').valueChanges - .pipe( - filter(() => this.connectorForm.get('type').value === ConnectorType.MQTT), - takeUntil(this.destroy$) - ) - .subscribe(name => this.connectorForm.get('basicConfig').get('broker.name')?.setValue(name)); - } - private getSortingDataAccessor(): (data: GatewayAttributeData, sortHeaderId: string) => string | number { return (data: GatewayAttributeData, sortHeaderId: string) => { switch (sortHeaderId) { diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json index c60e1dd956..3e0800c5d6 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json @@ -1,6 +1,5 @@ { "broker": { - "name": "Default Local Broker", "host": "127.0.0.1", "port": 1883, "clientId": "ThingsBoard_gateway", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json index 97621deda8..2e93f33942 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json @@ -1,6 +1,5 @@ { "server": { - "name": "OPC-UA Default Server", "url": "localhost:4840/freeopcua/server/", "timeoutInMillis": 5000, "scanPeriodInMillis": 5000, From 0c4507f02a0b83f7b57a492300ad0b7442a37577 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 21 Aug 2024 17:55:34 +0300 Subject: [PATCH 17/80] UI: Add return dialogRef in openDashboardStateInSeparateDialog --- ui-ngx/src/app/core/api/widget-api.models.ts | 3 ++- .../app/modules/home/components/widget/widget.component.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 0300c89d7f..71bfa32ee6 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -63,6 +63,7 @@ import { PopoverPlacement } from '@shared/components/popover.models'; import { PersistentRpc } from '@shared/models/rpc.models'; import { EventEmitter } from '@angular/core'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { MatDialogRef } from '@angular/material/dialog'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -101,7 +102,7 @@ export interface WidgetActionsApi { click: ($event: Event) => void; getActiveEntityInfo: () => SubscriptionEntityInfo; openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, - hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void; + hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => MatDialogRef; openDashboardStateInPopover: ($event: Event, targetDashboardStateId: string, params?: StateParams, hideDashboardToolbar?: boolean, preferredPlacement?: PopoverPlacement, hideOnClickOutside?: boolean, popoverWidth?: string, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 23d5a218e6..76845a11ef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -105,7 +105,7 @@ import { EntityDataService } from '@core/api/entity-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationType } from '@core/notification/notification.models'; import { AlarmDataService } from '@core/api/alarm-data.service'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { ComponentType } from '@angular/cdk/portal'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; import { MobileService } from '@core/services/mobile.service'; @@ -1365,7 +1365,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, - hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) { + hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number): MatDialogRef { const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); const stateObject: StateObject = {}; stateObject.params = params; @@ -1413,7 +1413,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } }); this.cd.markForCheck(); - return dashboard.dialogRef.afterClosed(); + return dashboard.dialogRef; } private elementClick($event: Event) { From 0599ea0647577fe6cf173745ae5c6d0c623be8c3 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 21 Aug 2024 18:37:53 +0300 Subject: [PATCH 18/80] Added Flow to edit Gateway Config as JSON in Advanced config tab --- .../data/json/tenant/dashboards/gateways.json | 2 +- ...eway-advanced-configuration.component.html | 8 + ...eway-advanced-configuration.component.scss | 23 + ...ateway-advanced-configuration.component.ts | 95 +++ ...ateway-basic-configuration.component.html} | 221 +++--- ...ateway-basic-configuration.component.scss} | 16 +- .../gateway-basic-configuration.component.ts} | 676 +++++++----------- .../gateway-configuration.component.html | 64 ++ .../gateway-configuration.component.scss | 64 ++ .../gateway-configuration.component.ts | 377 ++++++++++ .../models/gateway-configuration.models.ts | 148 ++++ .../gateway/gateway-connectors.component.ts | 16 +- .../lib/gateway/gateway-widget.models.ts | 4 +- .../widget/widget-components.module.ts | 10 +- 14 files changed, 1157 insertions(+), 567 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/{gateway-configuration.component.html => configuration/basic/gateway-basic-configuration.component.html} (77%) rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/{gateway-configuration.component.scss => configuration/basic/gateway-basic-configuration.component.scss} (93%) rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/{gateway-configuration.component.ts => configuration/basic/gateway-basic-configuration.component.ts} (52%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index a62550ed95..8030d3e731 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -200,7 +200,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "customPretty", - "customHtml": "\n

gateway.gateway-configuration

\n \n \n
\n\n\n
\n\n", + "customHtml": "\n\n\n", "customCss": ".gateway-config {\n width: 800px !important;\n padding: 0 !important;\n min-height: 75vh;\n max-width: 100%;\n display: grid !important;\n}\n\n@media screen and (max-width: 599px) {\n .mat-mdc-dialog-content {\n max-height: calc(100% - 60px) !important;\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.device = additionalParams.entity.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n", "customResources": [], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html new file mode 100644 index 0000000000..8ec95a2048 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html @@ -0,0 +1,8 @@ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss new file mode 100644 index 0000000000..4825ff0a64 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .config-container { + height: calc(100% - 60px); + padding: 8px; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts new file mode 100644 index 0000000000..c806130975 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, OnDestroy } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validators +} from '@angular/forms'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'tb-gateway-advanced-configuration', + templateUrl: './gateway-advanced-configuration.component.html', + styleUrls: ['./gateway-advanced-configuration.component.scss'], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GatewayAdvancedConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GatewayAdvancedConfigurationComponent), + multi: true + } + ], +}) +export class GatewayAdvancedConfigurationComponent implements OnDestroy, ControlValueAccessor, Validators { + + advancedFormControl: FormControl; + + private onChange: (value: unknown) => void; + private onTouched: () => void; + + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.advancedFormControl = this.fb.control(''); + this.advancedFormControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.onChange(value); + this.onTouched(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: unknown) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + writeValue(basicConfig: unknown): void { + this.advancedFormControl.reset(basicConfig, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.advancedFormControl.valid ? null : { + advancedFormControl: {valid: false} + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html similarity index 77% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html index 36b26cc7a8..b515d1baeb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html @@ -15,10 +15,7 @@ limitations under the License. --> - -

gateway.gateway-configuration

-
- +
@@ -42,23 +39,23 @@ info_outlined - + {{ 'gateway.thingsboard-host-required' | translate }} gateway.thingsboard-port - + {{ 'gateway.thingsboard-port-required' | translate }} - + {{ 'gateway.thingsboard-port-min' | translate }} - + {{ 'gateway.thingsboard-port-max' | translate }} - + {{ 'gateway.thingsboard-port-pattern' | translate }} + *ngIf="basicFormGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')"> security.access-token - + {{ 'security.access-token-required' | translate }} @@ -97,18 +94,18 @@
+ *ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"> security.clientId - + {{ 'security.clientId-required' | translate }} @@ -120,14 +117,14 @@ security.username - + {{ 'security.username-required' | translate }} @@ -138,14 +135,14 @@
+ *ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"> gateway.password @@ -156,13 +153,13 @@
gateway.logs.date-format - + {{ 'gateway.logs.date-format-required' | translate }} gateway.logs.log-format - + {{ 'gateway.logs.log-format-required' | translate }} gateway.logs.file-path - + {{ 'gateway.logs.file-path-required' | translate }}
@@ -244,11 +241,11 @@ gateway.logs.saving-period + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')"> {{ 'gateway.logs.saving-period-required' | translate }} + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')"> {{ 'gateway.logs.saving-period-min' | translate }} @@ -264,11 +261,11 @@ gateway.logs.backup-count + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')"> {{ 'gateway.logs.backup-count-required' | translate }} + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')"> {{ 'gateway.logs.backup-count-min' | translate }} -
{{ 'gateway.hints.' + gatewayConfigGroup.get('storage.type').value | translate }}
- +
{{ 'gateway.hints.' + basicFormGroup.get('storage.type').value | translate }}
+
gateway.storage-read-record-count - + {{ 'gateway.storage-read-record-count-required' | translate }} - + {{ 'gateway.storage-read-record-count-min' | translate }} - + {{ 'gateway.storage-read-record-count-pattern' | translate }} gateway.storage-max-records - + {{ 'gateway.storage-max-records-required' | translate }} - + {{ 'gateway.storage-max-records-min' | translate }} - + {{ 'gateway.storage-max-records-pattern' | translate }} gateway.storage-data-folder-path - + {{ 'gateway.storage-data-folder-path-required' | translate }} gateway.storage-max-files - + {{ 'gateway.storage-max-files-required' | translate }} - + {{ 'gateway.storage-max-files-min' | translate }} - + {{ 'gateway.storage-max-files-pattern' | translate }} gateway.storage-max-read-record-count - + {{ 'gateway.storage-max-read-record-count-required' | translate }} - + {{ 'gateway.storage-max-read-record-count-min' | translate }} - + {{ 'gateway.storage-max-read-record-count-pattern' | translate }} gateway.storage-max-file-records - + {{ 'gateway.storage-max-records-required' | translate }} - + {{ 'gateway.storage-max-records-min' | translate }} - + {{ 'gateway.storage-max-records-pattern' | translate }} gateway.storage-path - + {{ 'gateway.storage-path-required' | translate }} gateway.messages-ttl-check-in-hours - + {{ 'gateway.messages-ttl-check-in-hours-required' | translate }} - + {{ 'gateway.messages-ttl-check-in-hours-min' | translate }} - + {{ 'gateway.messages-ttl-check-in-hours-pattern' | translate }} gateway.messages-ttl-in-days - + {{ 'gateway.messages-ttl-in-days-required' | translate }} - + {{ 'gateway.messages-ttl-in-days-min' | translate }} - + {{ 'gateway.messages-ttl-in-days-pattern' | translate }} info_outlined - + {{ 'gateway.thingsboard-port-required' | translate }} - + {{ 'gateway.thingsboard-port-min' | translate }} - + {{ 'gateway.thingsboard-port-max' | translate }} - + {{ 'gateway.thingsboard-port-pattern' | translate }} @@ -485,13 +482,13 @@ info_outlined - + {{ 'gateway.grpc-keep-alive-timeout-required' | translate }} - + {{ 'gateway.grpc-keep-alive-timeout-min' | translate }} - + {{ 'gateway.grpc-keep-alive-timeout-pattern' | translate }} @@ -503,13 +500,13 @@ info_outlined - + {{ 'gateway.grpc-keep-alive-required' | translate }} - + {{ 'gateway.grpc-keep-alive-min' | translate }} - + {{ 'gateway.grpc-keep-alive-pattern' | translate }} @@ -519,13 +516,13 @@ info_outlined - + {{ 'gateway.grpc-min-time-between-pings-required' | translate }} - + {{ 'gateway.grpc-min-time-between-pings-min' | translate }} - + {{ 'gateway.grpc-min-time-between-pings-pattern' | translate }} @@ -537,13 +534,13 @@ info_outlined - + {{ 'gateway.grpc-max-pings-without-data-required' | translate }} - + {{ 'gateway.grpc-max-pings-without-data-min' | translate }} - + {{ 'gateway.grpc-max-pings-without-data-pattern' | translate }} @@ -553,13 +550,13 @@ info_outlined - + {{ 'gateway.grpc-min-ping-interval-without-data-required' | translate }} - + {{ 'gateway.grpc-min-ping-interval-without-data-min' | translate }} - + {{ 'gateway.grpc-min-ping-interval-without-data-pattern' | translate }} @@ -580,15 +577,15 @@ gateway.statistics.send-period + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')"> {{ 'gateway.statistics.send-period-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')"> {{ 'gateway.statistics.send-period-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')"> {{ 'gateway.statistics.send-period-pattern' | translate }} @@ -643,7 +640,7 @@
@@ -665,7 +662,7 @@
+ [class.no-padding-bottom]="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
@@ -673,20 +670,20 @@
+ *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"> gateway.inactivity-timeout-seconds + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')"> {{ 'gateway.inactivity-timeout-seconds-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')"> {{ 'gateway.inactivity-timeout-seconds-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('pattern')"> {{ 'gateway.inactivity-timeout-seconds-pattern' | translate }} gateway.inactivity-check-period-seconds + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')"> {{ 'gateway.inactivity-check-period-seconds-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')"> {{ 'gateway.inactivity-check-period-seconds-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('pattern')"> {{ 'gateway.inactivity-check-period-seconds-pattern' | translate }} gateway.min-pack-send-delay - + {{ 'gateway.min-pack-send-delay-required' | translate }} - + {{ 'gateway.min-pack-send-delay-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('pattern')"> {{ 'gateway.min-pack-send-delay-min-pattern' | translate }} gateway.mqtt-qos - + {{ 'gateway.mqtt-qos-required' | translate }} - + {{ 'gateway.mqtt-qos-range' | translate }} - + {{ 'gateway.mqtt-qos-range' | translate }} gateway.statistics.check-connectors-configuration + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')"> {{ 'gateway.statistics.check-connectors-configuration-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')"> {{ 'gateway.statistics.check-connectors-configuration-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')"> {{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }} @@ -772,15 +769,15 @@ gateway.statistics.max-payload-size-bytes + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('required')"> {{ 'gateway.statistics.max-payload-size-bytes-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('min')"> {{ 'gateway.statistics.max-payload-size-bytes-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')"> {{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }} gateway.statistics.min-pack-size-to-send + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('required')"> {{ 'gateway.statistics.min-pack-size-to-send-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('min')"> {{ 'gateway.statistics.min-pack-size-to-send-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')"> {{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }} -
- - -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss similarity index 93% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss index 9d72d1d759..9807fb71c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss @@ -23,6 +23,13 @@ display: flex; flex-direction: column; gap: 16px; + max-height: 70vh; + } + + .dialog-mode { + .configuration-block { + max-height: 60vh; + } } .mat-toolbar { @@ -62,15 +69,6 @@ } } - .actions { - grid-row: 3; - padding: 8px; - display: flex; - gap: 8px; - justify-content: flex-end; - flex: 1; - } - mat-form-field { mat-error { display: none !important; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts similarity index 52% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts index 55121da8c5..4b47dd15c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts @@ -14,29 +14,37 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { + ChangeDetectorRef, + Component, + EventEmitter, + forwardRef, + Input, + OnDestroy, + Output +} from '@angular/core'; +import { + ControlValueAccessor, FormArray, FormBuilder, FormControl, FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { EntityId } from '@shared/models/id/entity-id'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { AttributeService } from '@core/http/attribute.service'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { MatDialog } from '@angular/material/dialog'; import { GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogData } from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog'; import { DeviceService } from '@core/http/device.service'; -import { Observable, of } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; -import { NULL_UUID } from '@shared/models/id/has-uuid'; import { GatewayLogLevel, GecurityTypesTranslationsMap, @@ -46,51 +54,228 @@ import { LogSavingPeriodTranslations, SecurityTypes, StorageTypes, - StorageTypesTranslationMap -} from './gateway-widget.models'; -import { deepTrim } from '@core/utils'; + StorageTypesTranslationMap, +} from '../../gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + GatewayConfigCommand, + GatewayConfigValue, LogConfig +} from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models'; @Component({ - selector: 'tb-gateway-configuration', - templateUrl: './gateway-configuration.component.html', - styleUrls: ['./gateway-configuration.component.scss'] + selector: 'tb-gateway-basic-configuration', + templateUrl: './gateway-basic-configuration.component.html', + styleUrls: ['./gateway-basic-configuration.component.scss'], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GatewayBasicConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GatewayBasicConfigurationComponent), + multi: true + } + ], }) -export class GatewayConfigurationComponent implements OnInit { +export class GatewayBasicConfigurationComponent implements OnDestroy, ControlValueAccessor, Validators { + + @Input() + device: EntityId; + + @coerceBoolean() + @Input() + dialogMode = false; - gatewayConfigGroup: FormGroup; + @Output() + initialCredentialsUpdated = new EventEmitter(); StorageTypes = StorageTypes; storageTypes = Object.values(StorageTypes) as StorageTypes[]; storageTypesTranslationMap = StorageTypesTranslationMap; - logSavingPeriods = LogSavingPeriodTranslations; - localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[]; localLogsConfigTranslateMap = LocalLogsConfigTranslateMap; - securityTypes = GecurityTypesTranslationsMap; - gatewayLogLevel = Object.values(GatewayLogLevel); - - @Input() - device: EntityId; - - @Input() - dialogRef: MatDialogRef; - logSelector: FormControl; + basicFormGroup: FormGroup; + + private onChange: (value: GatewayConfigValue) => void; + private onTouched: () => void; - private initialCredentials: DeviceCredentials; + private destroy$ = new Subject(); constructor(private fb: FormBuilder, - private attributeService: AttributeService, private deviceService: DeviceService, private cd: ChangeDetectorRef, private dialog: MatDialog) { + this.initBasicFormGroup(); + this.basicFormGroup.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.onChange(value); + this.onTouched(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: GatewayConfigValue) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + writeValue(basicConfig: GatewayConfigValue): void { + this.basicFormGroup.patchValue(basicConfig, {emitEvent: false}); + if (basicConfig?.thingsboard?.security) { + this.checkAndFetchCredentials(basicConfig.thingsboard.security); + } + if (basicConfig?.grpc) { + this.toggleRpcFields(basicConfig.grpc.enabled); + } + const commands = basicConfig?.thingsboard?.statistics?.commands || []; + commands.forEach(command => this.addCommand(command, false)); + } + + validate(): ValidationErrors | null { + return this.basicFormGroup.valid ? null : { + basicFormGroup: {valid: false} + }; + } + + private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { + return (group: FormGroup): ValidationErrors | null => { + if (!controls) { + controls = Object.keys(group.controls); + } + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); + + return hasAtLeastOne ? null : {atLeastOne: true}; + }; } - ngOnInit() { - this.gatewayConfigGroup = this.fb.group({ + private toggleRpcFields(enable: boolean): void { + const grpcGroup = this.basicFormGroup.get('grpc') as FormGroup; + if (enable) { + grpcGroup.get('serverPort').enable({emitEvent: false}); + grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false}); + grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false}); + grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false}); + grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false}); + grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false}); + grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false}); + } else { + grpcGroup.get('serverPort').disable({emitEvent: false}); + grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false}); + grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false}); + grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false}); + grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false}); + grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false}); + grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false}); + } + } + + private addLocalLogConfig(name: string, config: LogConfig): void { + const localLogsFormGroup = this.basicFormGroup.get('logs.local') as FormGroup; + const configGroup = this.fb.group({ + logLevel: [config.logLevel || GatewayLogLevel.INFO, [Validators.required]], + filePath: [config.filePath || './logs', [Validators.required]], + backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], + savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], + savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]] + }); + localLogsFormGroup.addControl(name, configGroup); + } + + getLogFormGroup(value: string): FormGroup { + return this.basicFormGroup.get(`logs.local.${value}`) as FormGroup; + } + + commandFormArray(): FormArray { + return this.basicFormGroup.get('thingsboard.statistics.commands') as FormArray; + } + + removeCommandControl(index: number, event: PointerEvent): void { + if (event.pointerType === '') { + return; + } + this.commandFormArray().removeAt(index); + this.basicFormGroup.markAsDirty(); + } + + private removeAllSecurityValidators(): void { + const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup; + securityGroup.clearValidators(); + for (const controlsKey in securityGroup.controls) { + if (controlsKey !== 'type') { + securityGroup.controls[controlsKey].clearValidators(); + securityGroup.controls[controlsKey].setErrors(null); + securityGroup.controls[controlsKey].updateValueAndValidity(); + } + } + } + + private removeAllStorageValidators(): void { + const storageGroup = this.basicFormGroup.get('storage') as FormGroup; + for (const storageKey in storageGroup.controls) { + if (storageKey !== 'type') { + storageGroup.controls[storageKey].clearValidators(); + storageGroup.controls[storageKey].setErrors(null); + storageGroup.controls[storageKey].updateValueAndValidity(); + } + } + } + + + private openConfigurationConfirmDialog(): void { + this.deviceService.getDevice(this.device.id).pipe(takeUntil(this.destroy$)).subscribe(gateway => { + this.dialog.open + (GatewayRemoteConfigurationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + gatewayName: gateway.name + } + }).afterClosed().subscribe( + (res) => { + if (!res) { + this.basicFormGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); + } + } + ); + }); + } + + addCommand(command?: GatewayConfigCommand, emitEvent: boolean = true): void { + const { attributeOnGateway = null, command: cmd = null, timeout = null } = command || {}; + + const commandFormGroup = this.fb.group({ + attributeOnGateway: [attributeOnGateway, [Validators.required, Validators.pattern(/^[^.\s]+$/)]], + command: [cmd, [Validators.required, Validators.pattern(/^(?=\S).*\S$/)]], + timeout: [timeout, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]] + }); + + this.commandFormArray().push(commandFormGroup, { emitEvent }); + } + + + private initBasicFormGroup(): void { + this.basicFormGroup = this.fb.group({ thingsboard: this.fb.group({ host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]], port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], @@ -125,8 +310,14 @@ export class GatewayConfigurationComponent implements OnInit { }), storage: this.fb.group({ type: [StorageTypes.MEMORY, [Validators.required]], - read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], - max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], + read_records_count: [ + 100, [Validators.min(1), + Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)] + ], + max_records_count: [ + 100000, + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)] + ], data_folder_path: ['./data/', [Validators.required]], max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], @@ -160,18 +351,18 @@ export class GatewayConfigurationComponent implements OnInit { }) }); - this.gatewayConfigGroup.get('thingsboard.security.password').valueChanges.subscribe(password => { + this.basicFormGroup.get('thingsboard.security.password').valueChanges.subscribe(password => { if (password && password !== '') { - this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]); + this.basicFormGroup.get('thingsboard.security.username').setValidators([Validators.required]); } else { - this.gatewayConfigGroup.get('thingsboard.security.username').clearValidators(); + this.basicFormGroup.get('thingsboard.security.username').clearValidators(); } - this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); }); this.toggleRpcFields(false); - this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { + this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { if (!enabled) { this.openConfigurationConfirmDialog(); } @@ -180,15 +371,17 @@ export class GatewayConfigurationComponent implements OnInit { this.logSelector = this.fb.control(LocalLogsConfigs.service); for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { - this.addLocalLogConfig(localLogsConfigsKey, {}); + this.addLocalLogConfig(localLogsConfigsKey, {} as LogConfig); } - const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; + const checkingDeviceActivityGroup = this.basicFormGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => { checkingDeviceActivityGroup.updateValueAndValidity(); if (enabled) { - checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); - checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); + checkingDeviceActivityGroup.get('inactivityTimeoutSeconds') + .setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); + checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds') + .setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); } else { checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); @@ -197,11 +390,11 @@ export class GatewayConfigurationComponent implements OnInit { checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false}); }); - this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => { + this.basicFormGroup.get('grpc.enabled').valueChanges.subscribe(value => { this.toggleRpcFields(value); }); - const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; + const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup; securityGroup.get('type').valueChanges.subscribe(type => { this.removeAllSecurityValidators(); if (type === SecurityTypes.ACCESS_TOKEN) { @@ -229,7 +422,7 @@ export class GatewayConfigurationComponent implements OnInit { securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges()); securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges()); - const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; + const storageGroup = this.basicFormGroup.get('storage') as FormGroup; storageGroup.get('type').valueChanges.subscribe(type => { this.removeAllStorageValidators(); if (type === StorageTypes.MEMORY) { @@ -262,403 +455,32 @@ export class GatewayConfigurationComponent implements OnInit { storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false}); } }); - - this.fetchConfigAttribute(this.device); - } - - private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { - return (group: FormGroup): ValidationErrors | null => { - if (!controls) { - controls = Object.keys(group.controls); - } - const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); - - return hasAtLeastOne ? null : {atLeastOne: true}; - }; - } - - private fetchConfigAttribute(entityId: EntityId) { - if (entityId.id === NULL_UUID) { - return; - } - this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, - ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( - mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( - entityId, AttributeScope.SHARED_SCOPE, ['general_configuration', 'grpc_configuration', - 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel'])) - ).subscribe(attributes => { - if (attributes.length) { - const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value; - const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value; - const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value; - const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value; - const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel')?.value; - if (general_configuration) { - const configObj = {thingsboard: general_configuration}; - if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) { - for (const command of configObj.thingsboard.statistics.commands) { - this.addCommand(command); - } - delete configObj.thingsboard.statistics.commands; - } - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.gatewayConfigGroup.markAsPristine(); - if (!configObj.thingsboard.remoteConfiguration) { - this.gatewayConfigGroup.disable({emitEvent: false}); - } - this.checkAndFetchCredentials(configObj.thingsboard.security); - } - if (grpc_configuration) { - const configObj = {grpc: grpc_configuration}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.toggleRpcFields(grpc_configuration.enabled); - } - if (logs_configuration) { - const configObj = {logs: this.logsToObj(logs_configuration)}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.cd.detectChanges(); - } - if (storage_configuration) { - const configObj = {storage: storage_configuration}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - } - if (remoteLoggingLevel) { - const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); - remoteLogsFormGroup.patchValue({ - enabled: remoteLoggingLevel !== GatewayLogLevel.NONE, - logLevel: remoteLoggingLevel - }, {emitEvent: false}); - remoteLogsFormGroup.markAsPristine(); - } - this.cd.detectChanges(); - } else { - this.checkAndFetchCredentials(); - } - }); } private checkAndFetchCredentials(security: any = {}): void { if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { - this.initialCredentials = credentials; + this.initialCredentialsUpdated.emit(credentials); if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) { - this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN - ? SecurityTypes.TLS_ACCESS_TOKEN - : SecurityTypes.ACCESS_TOKEN); - this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId); + this.basicFormGroup.get('thingsboard.security.type') + .setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN + ? SecurityTypes.TLS_ACCESS_TOKEN + : SecurityTypes.ACCESS_TOKEN, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, {emitEvent: false}); if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) { - this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert); + this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, {emitEvent: false}); } } else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { const parsedValue = JSON.parse(credentials.credentialsValue); - this.gatewayConfigGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD); - this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId); - this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName); - this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.username').setValue(parsedValue.userName, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.password') + .setValue(parsedValue.password, {emitEvent: false}); } else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE) { //if sertificate is present set sertificate as present } }); } } - - private logsToObj(logsConfig: any) { - const logsObject = { - local: {} - }; - const logFormat = logsConfig.formatters.LogFormatter.format; - const dateFormat = logsConfig.formatters.LogFormatter.datefmt; - for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { - const handlerKey = localLogsConfigsKey + 'Handler'; - logsObject[localLogsConfigsKey] = { - logLevel: logsConfig.loggers[localLogsConfigsKey].level, - filePath: logsConfig.handlers[handlerKey].filename.split('/' + localLogsConfigsKey)[0], - backupCount: logsConfig.handlers[handlerKey].backupCount, - savingTime: logsConfig.handlers[handlerKey].interval, - savingPeriod: logsConfig.handlers[handlerKey].when, - }; - } - - - return {local: logsObject, logFormat, dateFormat}; - } - - private toggleRpcFields(enable: boolean) { - const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup; - if (enable) { - grpcGroup.get('serverPort').enable({emitEvent: false}); - grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false}); - grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false}); - grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false}); - grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false}); - grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false}); - grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false}); - } else { - grpcGroup.get('serverPort').disable({emitEvent: false}); - grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false}); - grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false}); - grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false}); - grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false}); - grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false}); - grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false}); - } - } - - - addCommand(command: any = {}): void { - const commandsFormArray = this.commandFormArray(); - const commandFormGroup = this.fb.group({ - attributeOnGateway: [command.attributeOnGateway || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]], - command: [command.command || null, [Validators.required, Validators.pattern(/^(?=\S).*\S$/)]], - timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]], - }); - commandsFormArray.push(commandFormGroup); - } - - private addLocalLogConfig(name: string, config: any): void { - const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; - const configGroup = this.fb.group({ - logLevel: [config.logLevel || GatewayLogLevel.INFO, [Validators.required]], - filePath: [config.filePath || './logs', [Validators.required]], - backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], - savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], - savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]] - }); - localLogsFormGroup.addControl(name, configGroup); - } - - getLogFormGroup(value: string): FormGroup { - return this.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup; - } - - commandFormArray(): FormArray { - return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray; - } - - removeCommandControl(index: number, event: any): void { - if (event.pointerType === '') { - return; - } - this.commandFormArray().removeAt(index); - this.gatewayConfigGroup.markAsDirty(); - } - - private removeAllSecurityValidators(): void { - const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; - securityGroup.clearValidators(); - for (const controlsKey in securityGroup.controls) { - if (controlsKey !== 'type') { - securityGroup.controls[controlsKey].clearValidators(); - securityGroup.controls[controlsKey].setErrors(null); - securityGroup.controls[controlsKey].updateValueAndValidity(); - } - } - } - - private removeAllStorageValidators(): void { - const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; - for (const storageKey in storageGroup.controls) { - if (storageKey !== 'type') { - storageGroup.controls[storageKey].clearValidators(); - storageGroup.controls[storageKey].setErrors(null); - storageGroup.controls[storageKey].updateValueAndValidity(); - } - } - } - - private removeEmpty(obj: any) { - return Object.fromEntries( - Object.entries(obj) - .filter(([_, v]) => v != null) - .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) - ); - } - - private generateLogsFile(logsObj: any) { - const logAttrObj = { - version: 1, - disable_existing_loggers: false, - formatters: { - LogFormatter: { - class: 'logging.Formatter', - format: logsObj.logFormat, - datefmt: logsObj.dateFormat, - } - }, - handlers: { - consoleHandler: { - class: 'logging.StreamHandler', - formatter: 'LogFormatter', - level: 'DEBUG', - stream: 'ext://sys.stdout' - }, - databaseHandler: { - class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', - formatter: 'LogFormatter', - filename: './logs/database.log', - backupCount: 1, - encoding: 'utf-8' - } - }, - loggers: { - database: { - handlers: ['databaseHandler', 'consoleHandler'], - level: 'DEBUG', - propagate: false - } - }, - root: { - level: 'ERROR', - handlers: [ - 'consoleHandler' - ] - }, - ts: new Date().getTime() - }; - for (const key of Object.keys(logsObj.local)) { - logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(logsObj.local[key], key); - logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key); - } - return logAttrObj; - } - - private createHandlerObj(logObj: any, key: string) { - return { - class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', - formatter: 'LogFormatter', - filename: `${logObj.filePath}/${key}.log`, - backupCount: logObj.backupCount, - interval: logObj.savingTime, - when: logObj.savingPeriod, - encoding: 'utf-8' - }; - } - - private createLoggerObj(logObj: any, key: string) { - return { - handlers: [`${key}Handler`, 'consoleHandler'], - level: logObj.logLevel, - propagate: false - }; - } - - saveConfig(): void { - const value = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value)); - value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands); - const attributes = []; - attributes.push({ - key: 'RemoteLoggingLevel', - value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE - }); - delete value.connectors; - attributes.push({ - key: 'logs_configuration', - value: this.generateLogsFile(value.logs) - }); - value.grpc.ts = new Date().getTime(); - attributes.push({ - key: 'grpc_configuration', - value: value.grpc - }); - value.storage.ts = new Date().getTime(); - attributes.push({ - key: 'storage_configuration', - value: value.storage - }); - value.thingsboard.ts = new Date().getTime(); - attributes.push({ - key: 'general_configuration', - value: value.thingsboard - }); - - - this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { - this.updateCredentials(value.thingsboard.security).subscribe(() => { - if (this.dialogRef) { - this.dialogRef.close(); - } else { - this.gatewayConfigGroup.markAsPristine(); - this.cd.detectChanges(); - } - }); - }); - } - - private updateCredentials(securityConfig: any): Observable { - let updateCredentials = false; - let newCredentials = {}; - if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) { - if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) { - updateCredentials = true; - } else { - const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); - updateCredentials = !( - parsedCredentials.clientId === securityConfig.clientId && - parsedCredentials.userName === securityConfig.username && - parsedCredentials.password === securityConfig.password); - } - if (updateCredentials) { - const credentialsValue: { clientId?: string; userName?: string; password?: string } = {}; - const credentialsType = DeviceCredentialsType.MQTT_BASIC; - if (securityConfig.clientId) { - credentialsValue.clientId = securityConfig.clientId; - } - if (securityConfig.username) { - credentialsValue.userName = securityConfig.username; - } - if (securityConfig.password) { - credentialsValue.password = securityConfig.password; - } - newCredentials = { - credentialsType, - credentialsValue: JSON.stringify(credentialsValue) - }; - } - } else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) { - if (this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN) { - updateCredentials = true; - } else { - updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken; - if (updateCredentials) { - this.initialCredentials.credentialsId = securityConfig.accessToken; - } - } - if (updateCredentials) { - newCredentials = { - credentialsType: DeviceCredentialsType.ACCESS_TOKEN, - credentialsId: securityConfig.accessToken - }; - } - } - - if (updateCredentials) { - return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials}); - } - return of(null); - } - - cancel(): void { - if (this.dialogRef) { - this.dialogRef.close(); - } - } - - private openConfigurationConfirmDialog(): void { - this.deviceService.getDevice(this.device.id).subscribe(gateway => { - this.dialog.open - (GatewayRemoteConfigurationDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - gatewayName: gateway.name - } - }).afterClosed().subscribe( - (res) => { - if (!res) { - this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); - } - } - ); - }); - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html new file mode 100644 index 0000000000..01fc12f167 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html @@ -0,0 +1,64 @@ + +
+
+ +
+

gateway.gateway-configuration

+
+ + + {{ 'gateway.basic' | translate }} + + + {{ 'gateway.advanced' | translate }} + + + +
+
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss new file mode 100644 index 0000000000..4d1d62ddad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss @@ -0,0 +1,64 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + + .page-header.mat-toolbar { + background: transparent; + color: rgba(0, 0, 0, .87) !important; + } + + .actions { + grid-row: 3; + padding: 8px 16px 8px 8px; + display: flex; + gap: 8px; + justify-content: flex-end; + position: absolute; + bottom: 0; + right: 0; + z-index: 1; + background: white; + width: 100%; + } + + .gateway-config-container { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + + .content-wrapper { + flex: 1; + } + + .toolbar-actions { + display: flex; + align-items: center; + } +} + +.dialog-toggle { + ::ng-deep.mat-button-toggle-button { + color: rgba(255, 255, 255, .75); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts new file mode 100644 index 0000000000..3016d65c72 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts @@ -0,0 +1,377 @@ +import { ChangeDetectorRef, Component, Input, AfterViewInit, OnDestroy } from '@angular/core'; +import { + FormBuilder, + FormGroup, +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { DeviceService } from '@core/http/device.service'; +import { Observable, of, Subject } from 'rxjs'; +import { mergeMap, takeUntil } from 'rxjs/operators'; +import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { + GatewayLogLevel, + SecurityTypes, + ConfigurationModes, + LocalLogsConfigs, + LogSavingPeriod, Attribute +} from '../gateway-widget.models'; +import { deepTrim, isEqual } from '@core/utils'; +import { + GatewayConfigSecurity, + GatewayConfigValue, + GatewayGeneralConfig, + GatewayLogsConfig, + LocalLogs, + LogAttribute, + LogConfig, +} from './models/gateway-configuration.models'; +import { DeviceId } from '@shared/models/id/device-id'; + +@Component({ + selector: 'tb-gateway-configuration', + templateUrl: './gateway-configuration.component.html', + styleUrls: ['./gateway-configuration.component.scss'] +}) +export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy { + + @Input() device: EntityId; + + @Input() dialogRef: MatDialogRef; + + initialCredentials: DeviceCredentials; + gatewayConfigGroup: FormGroup; + ConfigurationModes = ConfigurationModes; + + private destroy$ = new Subject(); + private readonly gatewayConfigAttributeKeys = + ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel', 'mode']; + + constructor(private fb: FormBuilder, + private attributeService: AttributeService, + private deviceService: DeviceService, + private cd: ChangeDetectorRef, + private dialog: MatDialog) { + + this.gatewayConfigGroup = this.fb.group({ + basicConfig: [], + advancedConfig: [], + mode: [ConfigurationModes.BASIC], + }); + + this.observeAlignConfigs(); + } + + ngAfterViewInit(): void { + this.fetchConfigAttribute(this.device); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + saveConfig(): void { + const { mode, advancedConfig } = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value)); + const value = { mode, ...advancedConfig }; + value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands ?? []); + const attributes = this.generateAttributes(value); + + this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { + this.updateCredentials(value.thingsboard.security).subscribe(() => { + if (this.dialogRef) { + this.dialogRef.close(); + } else { + this.gatewayConfigGroup.markAsPristine(); + this.cd.detectChanges(); + } + }); + }); + } + + private observeAlignConfigs(): void { + this.gatewayConfigGroup.get('basicConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { + const advancedControl = this.gatewayConfigGroup.get('advancedConfig'); + + if (!isEqual(advancedControl.value, value)) { + advancedControl.patchValue(value, {emitEvent: false}); + } + }); + + this.gatewayConfigGroup.get('advancedConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { + const basicControl = this.gatewayConfigGroup.get('basicConfig'); + + if (!isEqual(basicControl.value, value)) { + basicControl.patchValue(value, {emitEvent: false}); + } + }); + } + + private generateAttributes(value: GatewayConfigValue): Attribute[] { + const attributes = []; + + const addAttribute = (key: string, val: unknown) => { + attributes.push({ key, value: val }); + }; + + const addTimestampedAttribute = (key: string, val: unknown) => { + val = {...val as Record, ts: new Date().getTime()}; + addAttribute(key, val); + }; + + addAttribute('RemoteLoggingLevel', value.logs?.remote?.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE); + + delete value.connectors; + addAttribute('logs_configuration', this.generateLogsFile(value.logs)); + + addTimestampedAttribute('grpc_configuration', value.grpc); + addTimestampedAttribute('storage_configuration', value.storage); + addTimestampedAttribute('general_configuration', value.thingsboard); + + addAttribute('mode', value.mode); + + return attributes; + } + + private updateCredentials(securityConfig: GatewayConfigSecurity): Observable { + let newCredentials: Partial = {}; + + switch (securityConfig.type) { + case SecurityTypes.USERNAME_PASSWORD: + if (this.shouldUpdateCredentials(securityConfig)) { + newCredentials = this.generateMqttCredentials(securityConfig); + } + break; + + case SecurityTypes.ACCESS_TOKEN: + case SecurityTypes.TLS_ACCESS_TOKEN: + if (this.shouldUpdateAccessToken(securityConfig)) { + newCredentials = { + credentialsType: DeviceCredentialsType.ACCESS_TOKEN, + credentialsId: securityConfig.accessToken + }; + } + break; + } + + return Object.keys(newCredentials).length + ? this.deviceService.saveDeviceCredentials({ ...this.initialCredentials, ...newCredentials }) + : of(null); + } + + private shouldUpdateCredentials(securityConfig: GatewayConfigSecurity): boolean { + if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) { + return true; + } + const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); + return !( + parsedCredentials.clientId === securityConfig.clientId && + parsedCredentials.userName === securityConfig.username && + parsedCredentials.password === securityConfig.password + ); + } + + private generateMqttCredentials(securityConfig: GatewayConfigSecurity): Partial { + const { clientId, username, password } = securityConfig; + + const credentialsValue = { + ...(clientId && { clientId }), + ...(username && { userName: username }), + ...(password && { password }), + }; + + return { + credentialsType: DeviceCredentialsType.MQTT_BASIC, + credentialsValue: JSON.stringify(credentialsValue) + }; + } + + private shouldUpdateAccessToken(securityConfig: GatewayConfigSecurity): boolean { + return this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN || + this.initialCredentials.credentialsId !== securityConfig.accessToken; + } + + cancel(): void { + if (this.dialogRef) { + this.dialogRef.close(); + } + } + + private removeEmpty(obj: unknown) { + return Object.fromEntries( + Object.entries(obj) + .filter(([_, v]) => v != null) + .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) + ); + } + + private generateLogsFile(logsObj: GatewayLogsConfig): LogAttribute { + const logAttrObj = { + version: 1, + disable_existing_loggers: false, + formatters: { + LogFormatter: { + class: 'logging.Formatter', + format: logsObj.logFormat, + datefmt: logsObj.dateFormat, + } + }, + handlers: { + consoleHandler: { + class: 'logging.StreamHandler', + formatter: 'LogFormatter', + level: 'DEBUG', + stream: 'ext://sys.stdout' + }, + databaseHandler: { + class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', + formatter: 'LogFormatter', + filename: './logs/database.log', + backupCount: 1, + encoding: 'utf-8' + } + }, + loggers: { + database: { + handlers: ['databaseHandler', 'consoleHandler'], + level: 'DEBUG', + propagate: false + } + }, + root: { + level: 'ERROR', + handlers: [ + 'consoleHandler' + ] + }, + ts: new Date().getTime() + }; + + this.addLocalLoggers(logAttrObj, logsObj.local); + + return logAttrObj; + } + + private addLocalLoggers(logAttrObj: LogAttribute, localLogs: LocalLogs): void { + for (const key of Object.keys(localLogs)) { + logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(localLogs[key], key); + logAttrObj.loggers[key] = this.createLoggerObj(localLogs[key], key); + } + } + + private createHandlerObj(logObj: LogConfig, key: string) { + return { + class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', + formatter: 'LogFormatter', + filename: `${logObj.filePath}/${key}.log`, + backupCount: logObj.backupCount, + interval: logObj.savingTime, + when: logObj.savingPeriod, + encoding: 'utf-8' + }; + } + + private createLoggerObj(logObj: LogConfig, key: string) { + return { + handlers: [`${key}Handler`, 'consoleHandler'], + level: logObj.logLevel, + propagate: false + }; + } + + private fetchConfigAttribute(entityId: EntityId): void { + if (entityId.id === NULL_UUID) { + return; + } + + this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, + ) + .pipe( + mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( + entityId, AttributeScope.SHARED_SCOPE, this.gatewayConfigAttributeKeys) + ), + takeUntil(this.destroy$) + ) + .subscribe(attributes => { + this.updateConfigs(attributes); + this.cd.detectChanges(); + }); + } + + private updateConfigs(attributes: AttributeData[]): void { + const formValue: GatewayConfigValue = { + thingsboard: null, + grpc: null, + logs: null, + storage: null, + mode: ConfigurationModes.BASIC + }; + + attributes.forEach(attr => { + switch (attr.key) { + case 'general_configuration': + formValue.thingsboard = attr.value; + this.updateFormControls(attr.value); + break; + case 'grpc_configuration': + formValue.grpc = attr.value; + break; + case 'logs_configuration': + formValue.logs = this.logsToObj(attr.value); + break; + case 'storage_configuration': + formValue.storage = attr.value; + break; + case 'mode': + formValue.mode = attr.value; + break; + case 'RemoteLoggingLevel': + formValue.logs = { + ...formValue.logs, + remote: { + enabled: attr.value !== GatewayLogLevel.NONE, + logLevel: attr.value + } + }; + } + }); + + this.gatewayConfigGroup.get('basicConfig').setValue(formValue, { emitEvent: false }); + this.gatewayConfigGroup.get('advancedConfig').setValue(formValue, { emitEvent: false }); + } + + private updateFormControls(thingsboard: GatewayGeneralConfig): void { + const { type, accessToken, ...securityConfig } = thingsboard.security || {}; + + this.initialCredentials = { + deviceId: this.device as DeviceId, + credentialsType: type as unknown as DeviceCredentialsType, + credentialsId: accessToken, + credentialsValue: JSON.stringify(securityConfig) + }; + } + + private logsToObj(logsConfig: LogAttribute): GatewayLogsConfig { + const { format: logFormat, datefmt: dateFormat } = logsConfig.formatters.LogFormatter; + + const localLogs = Object.keys(LocalLogsConfigs).reduce((acc, key) => { + const handler = logsConfig.handlers[`${key}Handler`] || {}; + const logger = logsConfig.loggers[key] || {}; + + acc[key] = { + logLevel: logger.level || GatewayLogLevel.INFO, + filePath: handler.filename?.split(`/${key}`)[0] || './logs', + backupCount: handler.backupCount || 7, + savingTime: handler.interval || 3, + savingPeriod: handler.when || LogSavingPeriod.days + }; + + return acc; + }, {}) as LocalLogs; + + return { local: localLogs, logFormat, dateFormat }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts new file mode 100644 index 0000000000..ba2b7998a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts @@ -0,0 +1,148 @@ +import { + ConfigurationModes, + GatewayConnector, LocalLogsConfigs, LogSavingPeriod, + SecurityTypes, + StorageTypes +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-form.models'; + +export interface GatewayConfigValue { + mode: ConfigurationModes; + thingsboard: GatewayGeneralConfig; + storage: { + type: StorageTypes; + read_records_count?: number; + max_records_count?: number; + data_folder_path?: string; + max_file_count?: number; + max_read_records_count?: number; + max_records_per_file?: number; + data_file_path?: string; + messages_ttl_check_in_hours?: number; + messages_ttl_in_days?: number; + }; + grpc: { + enabled: boolean; + serverPort: number; + keepAliveTimeMs: number; + keepAliveTimeoutMs: number; + keepalivePermitWithoutCalls: boolean; + maxPingsWithoutData: number; + minTimeBetweenPingsMs: number; + minPingIntervalWithoutDataMs: number; + }; + connectors?: GatewayConnector[]; + logs: GatewayLogsConfig; +} + +export interface GatewayGeneralConfig { + host: string; + port: number; + remoteShell: boolean; + remoteConfiguration: boolean; + checkConnectorsConfigurationInSeconds: number; + statistics: { + enable: boolean; + statsSendPeriodInSeconds: number; + commands: GatewayConfigCommand[]; + }; + maxPayloadSizeBytes: number; + minPackSendDelayMS: number; + minPackSizeToSend: number; + handleDeviceRenaming: boolean; + checkingDeviceActivity: { + checkDeviceInactivity: boolean; + inactivityTimeoutSeconds?: number; + inactivityCheckPeriodSeconds?: number; + }; + security: GatewayConfigSecurity; + qos: number; +} + +export interface GatewayLogsConfig { + dateFormat: string; + logFormat: string; + type?: string; + remote?: { + enabled: boolean; + logLevel: GatewayLogLevel; + }; + local: LocalLogs; +} + +export interface GatewayConfigSecurity { + type: SecurityTypes; + accessToken?: string; + clientId?: string; + username?: string; + password?: string; + caCert?: string; + cert?: string; + privateKey?: string; +} + +export interface GatewayConfigCommand { + attributeOnGateway: string; + command: string; + timeout: number; +} + +export interface LogConfig { + logLevel: GatewayLogLevel; + filePath: string; + backupCount: number; + savingTime: number; + savingPeriod: LogSavingPeriod; +} + +export type LocalLogs = Record; + +interface LogFormatterConfig { + class: string; + format: string; + datefmt: string; +} + +interface StreamHandlerConfig { + class: string; + formatter: string; + level: string; + stream: string; +} + +interface FileHandlerConfig { + class: string; + formatter: string; + filename: string; + backupCount: number; + encoding: string; +} + +interface LoggerConfig { + handlers: string[]; + level: string; + propagate: boolean; +} + +interface RootConfig { + level: string; + handlers: string[]; +} + +export interface LogAttribute { + version: number; + disable_existing_loggers: boolean; + formatters: { + LogFormatter: LogFormatterConfig; + }; + handlers: { + consoleHandler: StreamHandlerConfig; + databaseHandler: FileHandlerConfig; + }; + loggers: { + database: LoggerConfig; + }; + root: RootConfig; + ts: number; +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index e7e4d05e45..962d380788 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -52,7 +52,7 @@ import { AddConnectorConfigData, ConnectorBaseConfig, ConnectorBaseInfo, - ConnectorConfigurationModes, + ConfigurationModes, ConnectorType, GatewayAttributeData, GatewayConnector, @@ -97,13 +97,13 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie readonly gatewayLogLevel = Object.values(GatewayLogLevel); readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; - readonly ConnectorConfigurationModes = ConnectorConfigurationModes; + readonly ConnectorConfigurationModes = ConfigurationModes; pageLink: PageLink; dataSource: MatTableDataSource; connectorForm: FormGroup; activeConnectors: Array; - mode: ConnectorConfigurationModes = this.ConnectorConfigurationModes.BASIC; + mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC; initialConnector: GatewayConnector; private inactiveConnectors: Array; @@ -332,7 +332,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private clearOutConnectorForm(): void { this.initialConnector = null; this.connectorForm.setValue({ - mode: ConnectorConfigurationModes.BASIC, + mode: ConfigurationModes.BASIC, name: '', type: ConnectorType.MQTT, sendDataOnlyOnChange: false, @@ -518,7 +518,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private initConnectorForm(): void { this.connectorForm = this.fb.group({ - mode: [ConnectorConfigurationModes.BASIC], + mode: [ConfigurationModes.BASIC], name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], type: ['', [Validators.required]], enableRemoteLogging: [false], @@ -691,7 +691,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const configJson = this.connectorForm.get('configurationJson'); const type = this.connectorForm.get('type').value; const mode = this.connectorForm.get('mode').value; - if (!isEqual(config, configJson?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.BASIC) { + if (!isEqual(config, configJson?.value) && this.allowBasicConfig.has(type) && mode === ConfigurationModes.BASIC) { const newConfig = {...configJson.value, ...config}; this.connectorForm.get('configurationJson').patchValue(newConfig, {emitEvent: false}); } @@ -708,7 +708,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const basicConfig = this.connectorForm.get('basicConfig'); const type = this.connectorForm.get('type').value; const mode = this.connectorForm.get('mode').value; - if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) { + if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConfigurationModes.ADVANCED) { this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false}); } }); @@ -749,7 +749,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie case ConnectorType.MQTT: case ConnectorType.OPCUA: case ConnectorType.MODBUS: - this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false}); + this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false}); setTimeout(() => { this.connectorForm.patchValue(connector, {emitEvent: false}); this.connectorForm.markAsPristine(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 2dde0b8f54..4b0f684bc7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -123,7 +123,7 @@ export interface GatewayConnector { logLevel: string; key?: string; class?: string; - mode?: ConnectorConfigurationModes; + mode?: ConfigurationModes; } export interface DataMapping { @@ -493,7 +493,7 @@ export interface ModbusSlaveInfo { buttonTitle: string; } -export enum ConnectorConfigurationModes { +export enum ConfigurationModes { BASIC = 'basic', ADVANCED = 'advanced' } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 955fd1ecf6..2acc299586 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -56,7 +56,7 @@ import { GatewayServiceRPCConnectorTemplatesComponent } from '@home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component'; import { DeviceGatewayCommandComponent } from '@home/components/widget/lib/gateway/device-gateway-command.component'; -import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component'; +import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/configuration/gateway-configuration.component'; import { GatewayRemoteConfigurationDialogComponent } from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog'; @@ -145,6 +145,12 @@ import { ModbusRpcParametersComponent } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; +import { + GatewayBasicConfigurationComponent +} from '@home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component'; +import { + GatewayAdvancedConfigurationComponent +} from '@home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component'; @NgModule({ declarations: [ @@ -233,6 +239,8 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc ModbusBasicConfigComponent, EllipsisChipListDirective, ModbusRpcParametersComponent, + GatewayBasicConfigurationComponent, + GatewayAdvancedConfigurationComponent, ], exports: [ EntitiesTableWidgetComponent, From f0123d7c7b70c7c6808acc886d3dc25f41f7b937 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 21 Aug 2024 18:46:49 +0300 Subject: [PATCH 19/80] license headers --- ...ateway-advanced-configuration.component.html | 17 +++++++++++++++++ .../gateway-configuration.component.ts | 16 ++++++++++++++++ .../models/gateway-configuration.models.ts | 16 ++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html index 8ec95a2048..843e67b0fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html @@ -1,3 +1,20 @@ + Date: Thu, 22 Aug 2024 17:28:45 +0300 Subject: [PATCH 20/80] refactoring --- .../gateway-configuration.component.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts index 386180f1eb..317b616110 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts @@ -25,7 +25,7 @@ import { AttributeService } from '@core/http/attribute.service'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { DeviceService } from '@core/http/device.service'; import { Observable, of, Subject } from 'rxjs'; -import { mergeMap, takeUntil } from 'rxjs/operators'; +import { mergeMap, switchMap, takeUntil } from 'rxjs/operators'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { @@ -92,19 +92,20 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy { saveConfig(): void { const { mode, advancedConfig } = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value)); - const value = { mode, ...advancedConfig }; + const value = { mode, ...advancedConfig as GatewayConfigValue }; value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands ?? []); const attributes = this.generateAttributes(value); - this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { - this.updateCredentials(value.thingsboard.security).subscribe(() => { - if (this.dialogRef) { - this.dialogRef.close(); - } else { - this.gatewayConfigGroup.markAsPristine(); - this.cd.detectChanges(); - } - }); + this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).pipe( + switchMap(_ => this.updateCredentials(value.thingsboard.security)), + takeUntil(this.destroy$), + ).subscribe(() => { + if (this.dialogRef) { + this.dialogRef.close(); + } else { + this.gatewayConfigGroup.markAsPristine(); + this.cd.detectChanges(); + } }); } @@ -216,11 +217,11 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy { } } - private removeEmpty(obj: unknown) { + private removeEmpty(obj: Record): Record { return Object.fromEntries( Object.entries(obj) .filter(([_, v]) => v != null) - .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) + .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v as Record) : v]) ); } From 6df8a97a018208d428ff93518f858c872feebdc8 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 22 Aug 2024 17:52:01 +0300 Subject: [PATCH 21/80] Added trace log level --- .../components/widget/lib/gateway/gateway-widget.models.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 2dde0b8f54..c7f2a35047 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -38,7 +38,8 @@ export enum GatewayLogLevel { ERROR = 'ERROR', WARNING = 'WARNING', INFO = 'INFO', - DEBUG = 'DEBUG' + DEBUG = 'DEBUG', + TRACE = 'TRACE' } export enum PortLimits { From a63ed4fa6cc2e38b855d0f1ed631addd238f359d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 27 Aug 2024 13:57:32 +0300 Subject: [PATCH 22/80] Versioning for rule chain metadata --- .../controller/RuleChainControllerTest.java | 43 +++++++++++++++++++ .../EntityVersionMismatchException.java | 6 +++ .../common/data/rule/RuleChainMetaData.java | 6 ++- .../data/sync/ie/RuleChainExportData.java | 2 +- .../server/dao/rule/BaseRuleChainService.java | 4 ++ .../server/dao/sql/JpaAbstractDao.java | 2 +- 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 949dd4a781..0aca783e67 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -30,6 +30,8 @@ import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbCreateAlarmNode; import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode; import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration; import org.thingsboard.server.common.data.StringUtils; @@ -356,6 +358,47 @@ public class RuleChainControllerTest extends AbstractControllerTest { assertThat(error).contains("alarmType is malformed"); } + @Test + public void testSaveRuleChainWithOutdatedVersion() throws Exception { + RuleChain ruleChain = createRuleChain("Rule chain with invalid nodes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("Test"); + ruleNode.setType(TbMsgGeneratorNode.class.getName()); + TbMsgGeneratorNodeConfiguration config = new TbMsgGeneratorNodeConfiguration(); + ruleNode.setConfiguration(JacksonUtil.valueToTree(config)); + List ruleNodes = new ArrayList<>(); + ruleNodes.add(ruleNode); + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(ruleNodes); + + ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); // in this case when saving metadata, rule chain will also be updated + + ruleChain = doGet("/api/ruleChain/" + ruleChain.getId(), RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(2); + + ruleChain.setName("Updated"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(3); + + ruleChain.setVersion(1L); + doPost("/api/ruleChain", ruleChain) + .andExpect(status().isConflict()); + ruleChainMetaData.setVersion(1L); + doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isConflict()); + + ruleChainMetaData.setVersion(3L); + doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isOk()); + ruleChain.setVersion(3L); + doPost("/api/ruleChain", ruleChain) + .andExpect(status().isOk()); + } + private RuleChain createRuleChain(String name) { RuleChain ruleChain = new RuleChain(); ruleChain.setName(name); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java index 310c6bccf7..e6ed0b824a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java @@ -15,10 +15,16 @@ */ package org.thingsboard.server.common.data.exception; +import org.thingsboard.server.common.data.EntityType; + public class EntityVersionMismatchException extends RuntimeException { public EntityVersionMismatchException(String message, Throwable cause) { super(message, cause); } + public EntityVersionMismatchException(EntityType entityType, Throwable cause) { + this((entityType != null ? entityType.getNormalName() : "Entity") + " was already changed by someone else", cause); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index eb18e28c8c..0ccbd1d34a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.rule; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.RuleChainId; import java.util.ArrayList; @@ -27,11 +28,14 @@ import java.util.List; */ @Schema @Data -public class RuleChainMetaData { +public class RuleChainMetaData implements HasVersion { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Version of the Rule Chain") + private Long version; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of the first rule node in the 'nodes' list") private Integer firstNodeIndex; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java index 98eaff96c9..cccffd6fed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; public class RuleChainExportData extends EntityExportData { @JsonProperty(index = 3) - @JsonIgnoreProperties("ruleChainId") + @JsonIgnoreProperties({"ruleChainId", "version"}) private RuleChainMetaData metaData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index fea3980446..68bb185e5f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -174,6 +175,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { return RuleChainUpdateResult.failed(); + } else if (ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { + throw new EntityVersionMismatchException(EntityType.RULE_CHAIN, null); } RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); @@ -298,6 +301,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChainId); + ruleChainMetaData.setVersion(ruleChain.getVersion()); List ruleNodes = getRuleChainNodes(tenantId, ruleChainId); Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId))); Map ruleNodeIndexMap = new HashMap<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 801898386e..a7d49e244b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -79,7 +79,7 @@ public abstract class JpaAbstractDao, D> try { entity = doSave(entity, isNew, flush); } catch (OptimisticLockException e) { - throw new EntityVersionMismatchException((getEntityType() != null ? getEntityType().getNormalName() : "Entity") + " was already changed by someone else", e); + throw new EntityVersionMismatchException(getEntityType(), e); } return DaoUtil.getData(entity); } From bc0022aafda9c8d3ffc7a7efad14f27680a4aeb3 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 27 Aug 2024 15:05:39 +0200 Subject: [PATCH 23/80] Added tenant id to the methods with locks and minor refactoring --- .../DefaultSubscriptionManagerService.java | 8 +-- .../DefaultTbLocalSubscriptionService.java | 64 +++++++++---------- .../subscription/TbAbstractDataSubCtx.java | 2 +- .../subscription/TbAbstractSubCtx.java | 2 +- .../subscription/TbAlarmDataSubCtx.java | 2 +- .../subscription/TbAlarmsSubscription.java | 2 +- .../subscription/TbEntityDataSubCtx.java | 2 +- .../TbLocalSubscriptionService.java | 6 +- .../subscription/TbSubscriptionUtils.java | 6 +- .../service/ws/DefaultWebSocketService.java | 22 ++++--- .../server/service/ws/WebSocketService.java | 3 +- .../DefaultNotificationCommandsHandler.java | 2 +- common/proto/src/main/proto/queue.proto | 2 + 13 files changed, 63 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 66b73781a0..5e1cd06b62 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -118,7 +118,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } callback.onSuccess(); if (event.hasTsOrAttrSub()) { - sendSubEventCallback(serviceId, entityId, event.getSeqNumber()); + sendSubEventCallback(tenantId, serviceId, entityId, event.getSeqNumber()); } } else { log.warn("[{}][{}][{}] Event belongs to external partition. Probably re-balancing is in progress. Topic: {}" @@ -142,12 +142,12 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } - private void sendSubEventCallback(String targetId, EntityId entityId, int seqNumber) { + private void sendSubEventCallback(TenantId tenantId, String targetId, EntityId entityId, int seqNumber) { var update = getEntityUpdatesInfo(entityId); if (serviceId.equals(targetId)) { - localSubscriptionService.onSubEventCallback(entityId, seqNumber, update, TbCallback.EMPTY); + localSubscriptionService.onSubEventCallback(tenantId, entityId, seqNumber, update, TbCallback.EMPTY); } else { - sendCoreNotification(targetId, entityId, TbSubscriptionUtils.toProto(entityId.getId(), seqNumber, update)); + sendCoreNotification(targetId, entityId, TbSubscriptionUtils.toProto(tenantId, entityId.getId(), seqNumber, update)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 0d29d374a3..ceafea6553 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -213,30 +213,28 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @Override public void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback) { UUID entityId = new UUID(subEventCallback.getEntityIdMSB(), subEventCallback.getEntityIdLSB()); - onSubEventCallback(entityId, subEventCallback.getSeqNumber(), new TbEntityUpdatesInfo(subEventCallback.getAttributesUpdateTs(), subEventCallback.getTimeSeriesUpdateTs()), callback); + TenantId tenantId = TenantId.fromUUID(new UUID(subEventCallback.getTenantIdMSB(), subEventCallback.getTenantIdLSB())); + onSubEventCallback(tenantId, entityId, subEventCallback.getSeqNumber(), new TbEntityUpdatesInfo(subEventCallback.getAttributesUpdateTs(), subEventCallback.getTimeSeriesUpdateTs()), callback); } @Override - public void onSubEventCallback(EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { - onSubEventCallback(entityId.getId(), seqNumber, entityUpdatesInfo, callback); + public void onSubEventCallback(TenantId tenantId, EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { + onSubEventCallback(tenantId, entityId.getId(), seqNumber, entityUpdatesInfo, callback); } - public void onSubEventCallback(UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { - log.debug("[{}][{}] Processing sub event callback: {}.", entityId, seqNumber, entityUpdatesInfo); + public void onSubEventCallback(TenantId tenantId, UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { + log.debug("[{}][{}][{}] Processing sub event callback: {}.", tenantId, entityId, seqNumber, entityUpdatesInfo); entityUpdates.put(entityId, entityUpdatesInfo); Set> pendingSubs = null; - Lock subsLock = null; + Lock subsLock = getSubsLock(tenantId); + subsLock.lock(); try { TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.get(entityId); if (entitySubs != null) { - subsLock = getSubsLock(entitySubs.getTenantId()); - subsLock.lock(); pendingSubs = entitySubs.clearPendingSubscriptions(seqNumber); } } finally { - if (subsLock != null) { - subsLock.unlock(); - } + subsLock.unlock(); } if (pendingSubs != null) { pendingSubs.forEach(this::checkMissedUpdates); @@ -245,31 +243,29 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override - public void cancelSubscription(String sessionId, int subscriptionId) { - log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); + public void cancelSubscription(TenantId tenantId, String sessionId, int subscriptionId) { + log.debug("[{}][{}][{}] Going to remove subscription.", tenantId, sessionId, subscriptionId); SubscriptionModificationResult result = null; - Lock subsLock = null; + Lock subsLock = getSubsLock(tenantId); + subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); if (sessionSubscriptions != null) { TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); if (subscription != null) { - subsLock = getSubsLock(subscription.getTenantId()); - subsLock.lock(); + if (sessionSubscriptions.isEmpty()) { subscriptionsBySessionId.remove(sessionId); } result = modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); } else { - log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); + log.debug("[{}][{}][{}] Subscription not found!", tenantId, sessionId, subscriptionId); } } else { - log.debug("[{}] No session subscriptions found!", sessionId); + log.debug("[{}][{}] No session subscriptions found!", tenantId, sessionId); } } finally { - if (subsLock != null) { - subsLock.unlock(); - } + subsLock.unlock(); } if (result != null && result.hasEvent()) { pushSubscriptionEvent(result); @@ -277,27 +273,22 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override - public void cancelAllSessionSubscriptions(String sessionId) { - log.debug("[{}] Going to remove session subscriptions.", sessionId); + public void cancelAllSessionSubscriptions(TenantId tenantId, String sessionId) { + log.debug("[{}][{}] Going to remove session subscriptions.", tenantId, sessionId); List results = new ArrayList<>(); - Lock subsLock = null; + Lock subsLock = getSubsLock(tenantId); + subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); if (sessionSubscriptions != null) { for (TbSubscription subscription : sessionSubscriptions.values()) { - if (subsLock == null) { - subsLock = getSubsLock(subscription.getTenantId()); - subsLock.lock(); - } - results.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false)); + results.add(modifySubscription(tenantId, subscription.getEntityId(), subscription, false)); } } else { - log.debug("[{}] No session subscriptions found!", sessionId); + log.debug("[{}][{}] No session subscriptions found!", tenantId, sessionId); } } finally { - if (subsLock != null) { - subsLock.unlock(); - } + subsLock.unlock(); } results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent); } @@ -602,7 +593,12 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } private void cleanupStaleSessions() { - subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale); + subscriptionsBySessionId.forEach((sessionId, subscriptions) -> + subscriptions.values() + .stream() + .findAny() + .ifPresent(subscription -> webSocketService.cleanupIfStale(subscription.getTenantId(), sessionId)) + ); } private void handleRateLimitError(TbSubscription subscription, WebSocketSessionRef sessionRef, String message) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index 77ef960d72..1ec47beeab 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -113,7 +113,7 @@ public abstract class TbAbstractDataSubCtx { protected void clearDynamicValueSubscriptions() { if (subToDynamicValueKeySet != null) { for (Integer subId : subToDynamicValueKeySet) { - localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId); + localSubscriptionService.cancelSubscription(getTenantId(), sessionRef.getSessionId(), subId); } subToDynamicValueKeySet.clear(); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 828da388fb..93c5f4323d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -341,7 +341,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { long startTs = System.currentTimeMillis() - query.getPageLink().getTimeWindow(); newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity)); } - subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); + subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId)); subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java index d59f8d74c3..a9ab7797b7 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java @@ -36,7 +36,7 @@ public class TbAlarmsSubscription extends TbSubscription { } } } - subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); + subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId)); subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); sendWsMsg(new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription)); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java index 59e7ad532d..6d742e88a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java @@ -34,11 +34,11 @@ public interface TbLocalSubscriptionService { void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback); - void onSubEventCallback(EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback empty); + void onSubEventCallback(TenantId tenantId, EntityId entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback empty); - void cancelSubscription(String sessionId, int subscriptionId); + void cancelSubscription(TenantId tenantId, String sessionId, int subscriptionId); - void cancelAllSessionSubscriptions(String sessionId); + void cancelAllSessionSubscriptions(TenantId tenantId, String sessionId); void onTimeSeriesUpdate(TransportProtos.TbSubUpdateProto tsUpdate, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index d691b70c55..79e41ecbfe 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -82,10 +82,12 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder).build(); } - public static ToCoreNotificationMsg toProto(UUID id, int seqNumber, TbEntityUpdatesInfo update) { + public static ToCoreNotificationMsg toProto(TenantId tenantId, UUID id, int seqNumber, TbEntityUpdatesInfo update) { TransportProtos.TbEntitySubEventCallbackProto.Builder updateProto = TransportProtos.TbEntitySubEventCallbackProto.newBuilder() .setEntityIdMSB(id.getMostSignificantBits()) - .setEntityIdLSB(id.getLeastSignificantBits()) + .setEntityIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(id.getLeastSignificantBits()) .setSeqNumber(seqNumber) .setAttributesUpdateTs(update.attributesUpdateTs) .setTimeSeriesUpdateTs(update.timeSeriesUpdateTs); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 33b011cc0a..8d96d14b49 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -193,17 +193,18 @@ public class DefaultWebSocketService implements WebSocketService { @Override public void handleSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) { String sessionId = sessionRef.getSessionId(); + TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); log.debug(PROCESSING_MSG, sessionId, event); switch (event.getEventType()) { case ESTABLISHED: wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); break; case ERROR: - log.debug("[{}] Unknown websocket session error: ", sessionId, + log.debug("[{}][{}] Unknown websocket session error: ", tenantId, sessionId, event.getError().orElse(new RuntimeException("No error specified"))); break; case CLOSED: - cleanupSessionById(sessionId); + cleanupSessionById(tenantId, sessionId); processSessionClose(sessionRef); break; } @@ -297,10 +298,10 @@ public class DefaultWebSocketService implements WebSocketService { } @Override - public void cleanupIfStale(String sessionId) { + public void cleanupIfStale(TenantId tenantId, String sessionId) { if (!msgEndpoint.isOpen(sessionId)) { log.info("[{}] Cleaning up stale session ", sessionId); - cleanupSessionById(sessionId); + cleanupSessionById(tenantId, sessionId); } } @@ -754,22 +755,23 @@ public class DefaultWebSocketService implements WebSocketService { } private void unsubscribe(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { + TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { - log.warn("[{}][{}] Cleanup session due to empty entity id.", sessionId, cmd.getCmdId()); - cleanupSessionById(sessionId); + log.warn("[{}][{}][{}] Cleanup session due to empty entity id.", tenantId, sessionId, cmd.getCmdId()); + cleanupSessionById(tenantId, sessionId); } else { Integer subId = sessionCmdMap.getOrDefault(sessionId, Collections.emptyMap()).remove(cmd.getCmdId()); if (subId == null) { - log.trace("[{}][{}] Failed to lookup subscription id mapping", sessionId, cmd.getCmdId()); + log.trace("[{}][{}][{}] Failed to lookup subscription id mapping", tenantId, sessionId, cmd.getCmdId()); subId = cmd.getCmdId(); } - oldSubService.cancelSubscription(sessionId, subId); + oldSubService.cancelSubscription(tenantId, sessionId, subId); } } - private void cleanupSessionById(String sessionId) { + private void cleanupSessionById(TenantId tenantId, String sessionId) { wsSessionsMap.remove(sessionId); - oldSubService.cancelAllSessionSubscriptions(sessionId); + oldSubService.cancelAllSessionSubscriptions(tenantId, sessionId); sessionCmdMap.remove(sessionId); entityDataSubService.cancelAllSessionSubscriptions(sessionId); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java index 284431ff84..00fd19780d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.ws; import org.springframework.web.socket.CloseStatus; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; @@ -37,6 +38,6 @@ public interface WebSocketService { void close(String sessionId, CloseStatus status); - void cleanupIfStale(String sessionId); + void cleanupIfStale(TenantId tenantId, String sessionId); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index 5bfe976194..84f3698845 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -242,7 +242,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH @Override public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { - localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), cmd.getCmdId()); + localSubscriptionService.cancelSubscription(sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSessionId(), cmd.getCmdId()); } private void sendUpdate(String sessionId, CmdUpdate update) { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 581b993120..2be1254a1a 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -817,6 +817,8 @@ message TbEntitySubEventCallbackProto { int32 seqNumber = 3; int64 attributesUpdateTs = 4; int64 timeSeriesUpdateTs = 5; + int64 tenantIdMSB = 6; + int64 tenantIdLSB = 7; } message TsValueProto { From 4bf34281f0810ecf5974b0b8691f3543ec6e638c Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 27 Aug 2024 17:55:41 +0300 Subject: [PATCH 24/80] HttpClient - fixed validation of hostname and port in case proxy usage --- .../org/thingsboard/rule/engine/rest/TbHttpClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 96ec648941..8c7dac0b50 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -336,8 +336,8 @@ public class TbHttpClient { String hostname = properties.getProperty(hostProperty); int port = Integer.parseInt(properties.getProperty(portProperty)); - checkProxyHost(config.getProxyHost()); - checkProxyPort(config.getProxyPort()); + checkProxyHost(hostname); + checkProxyPort(port); var proxy = option .type(ProxyProvider.Proxy.HTTP) @@ -362,8 +362,8 @@ public class TbHttpClient { ProxyProvider.Proxy type = SOCKS_VERSION_5.equals(version) ? ProxyProvider.Proxy.SOCKS5 : ProxyProvider.Proxy.SOCKS4; int port = Integer.parseInt(properties.getProperty(SOCKS_PROXY_PORT)); - checkProxyHost(config.getProxyHost()); - checkProxyPort(config.getProxyPort()); + checkProxyHost(hostname); + checkProxyPort(port); ProxyProvider.Builder proxy = option .type(type) From c38584e79f55ba355f0743adf197067a36ab7857 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 27 Aug 2024 18:04:20 +0300 Subject: [PATCH 25/80] Added new config for TbRestApiCallNodeConfiguration - maxInMemoryBufferSizeInKb --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 1 + .../rule/engine/rest/TbRestApiCallNodeConfiguration.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 8c7dac0b50..d71cc7f021 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -132,6 +132,7 @@ public class TbHttpClient { this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") + .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) .build(); } catch (SSLException e) { throw new TbNodeException(e); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 7d2ff7167d..1825bd1ae7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -46,6 +46,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration Date: Tue, 27 Aug 2024 19:22:49 +0300 Subject: [PATCH 26/80] TbHttpClient: Add cause to metadata in cause of buffer overloaded exception --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index d71cc7f021..4830761110 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -132,7 +132,7 @@ public class TbHttpClient { this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") - .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) .build(); } catch (SSLException e) { throw new TbNodeException(e); @@ -286,6 +286,9 @@ public class TbHttpClient { metaData.putValue(STATUS, restClientResponseException.getStatusText()); metaData.putValue(STATUS_CODE, restClientResponseException.getStatusCode().value() + ""); metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString()); + if (restClientResponseException.getStatusCode().is2xxSuccessful()) { + metaData.putValue(ERROR, metaData.getValue(ERROR) + ". Cause: " +restClientResponseException.getCause().getMessage()); + } } return TbMsg.transformMsgMetadata(origMsg, metaData); } From 4753455941e838dadfebef2391688fd096da2cb9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 11:17:10 +0300 Subject: [PATCH 27/80] UI: Improved move all widget action - fixed overlap and add support dashboard layout --- .../core/services/dashboard-utils.service.ts | 19 +++++++++++++++++-- .../dashboard-page.component.ts | 7 ++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 8d599ad925..5b98c7cb7b 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -752,10 +752,25 @@ export class DashboardUtilsService { public moveWidgets(layout: DashboardLayout, cols: number, rows: number) { cols = isDefinedAndNotNull(cols) ? Math.round(cols) : 0; rows = isDefinedAndNotNull(rows) ? Math.round(rows) : 0; + if (cols < 0 || rows < 0) { + let widgetMinCol = Infinity; + let widgetMinRow = Infinity; + for (const w of Object.keys(layout.widgets)) { + const widget = layout.widgets[w]; + widgetMinCol = Math.min(widgetMinCol, widget.col); + widgetMinRow = Math.min(widgetMinRow, widget.row); + } + if ((cols + widgetMinCol) < 0 ){ + cols = -widgetMinCol; + } + if ((rows + widgetMinRow) < 0 ){ + rows = -widgetMinRow; + } + } for (const w of Object.keys(layout.widgets)) { const widget = layout.widgets[w]; - widget.col = Math.max(0, widget.col + cols); - widget.row = Math.max(0, widget.row + rows); + widget.col += cols; + widget.row += rows; } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 0d379b1623..ee29dffb59 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -988,7 +988,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }); } - private moveWidgets($event: Event, layoutId: DashboardLayoutId) { + private moveWidgets($event: Event, layoutId: DashboardLayoutId, breakpointId: BreakpointId) { if ($event) { $event.stopPropagation(); } @@ -1001,7 +1001,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }).afterClosed().subscribe((result) => { this.layouts[layoutId].layoutCtx.displayGrid = 'onDrag&Resize'; if (result) { - const targetLayout = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts[layoutId]; + const dashboardLayout = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts[layoutId]; + const targetLayout = this.dashboardUtils.getDashboardLayoutConfig(dashboardLayout, breakpointId); this.dashboardUtils.moveWidgets(targetLayout, result.cols, result.rows); this.updateLayouts(); } else { @@ -1582,7 +1583,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC dashboardContextActions.push( { action: ($event) => { - this.moveWidgets($event, layoutCtx.id); + this.moveWidgets($event, layoutCtx.id, layoutCtx.breakpoint); }, enabled: true, value: 'dashboard.move-all-widgets', From b26bf1410ce9e863d91c3a6509231d2024c2eaa8 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 28 Aug 2024 10:24:22 +0200 Subject: [PATCH 28/80] fixed TbEntitySubEventCallbackProto build --- .../server/service/subscription/TbSubscriptionUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 79e41ecbfe..3d0037caba 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -85,9 +85,9 @@ public class TbSubscriptionUtils { public static ToCoreNotificationMsg toProto(TenantId tenantId, UUID id, int seqNumber, TbEntityUpdatesInfo update) { TransportProtos.TbEntitySubEventCallbackProto.Builder updateProto = TransportProtos.TbEntitySubEventCallbackProto.newBuilder() .setEntityIdMSB(id.getMostSignificantBits()) - .setEntityIdLSB(tenantId.getId().getLeastSignificantBits()) + .setEntityIdLSB(id.getLeastSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(id.getLeastSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setSeqNumber(seqNumber) .setAttributesUpdateTs(update.attributesUpdateTs) .setTimeSeriesUpdateTs(update.timeSeriesUpdateTs); From 6b00fb49002e0556408660f49349b57678389c92 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 Aug 2024 12:32:11 +0300 Subject: [PATCH 29/80] Increment entity's version regardless of the changes --- .../controller/DeviceControllerTest.java | 3 +- .../controller/RuleChainControllerTest.java | 12 ++--- .../server/dao/rule/BaseRuleChainService.java | 4 +- .../server/dao/sql/JpaAbstractDao.java | 19 +++++--- .../dao/sql/device/JpaDeviceDaoTest.java | 44 +++++++++++++++++-- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index cf46d0f7fd..c036f70e4a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -1503,11 +1503,12 @@ public class DeviceControllerTest extends AbstractControllerTest { Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); Device updatedDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class); + savedDevice.setVersion(updatedDevice.getVersion()); Assert.assertEquals(savedDevice, updatedDevice); DeviceCredentials updatedCredentials = doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); - + savedCredentials.setVersion(updatedCredentials.getVersion()); Assert.assertEquals(savedCredentials, updatedCredentials); } diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 0aca783e67..3d85c4cc12 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -360,7 +360,7 @@ public class RuleChainControllerTest extends AbstractControllerTest { @Test public void testSaveRuleChainWithOutdatedVersion() throws Exception { - RuleChain ruleChain = createRuleChain("Rule chain with invalid nodes"); + RuleChain ruleChain = createRuleChain("My rule chain"); RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChain.getId()); @@ -392,11 +392,11 @@ public class RuleChainControllerTest extends AbstractControllerTest { .andExpect(status().isConflict()); ruleChainMetaData.setVersion(3L); - doPost("/api/ruleChain/metadata", ruleChainMetaData) - .andExpect(status().isOk()); - ruleChain.setVersion(3L); - doPost("/api/ruleChain", ruleChain) - .andExpect(status().isOk()); + ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + assertThat(ruleChainMetaData.getVersion()).isEqualTo(4); + ruleChain.setVersion(4L); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(5); } private RuleChain createRuleChain(String name) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 68bb185e5f..902bf0f6a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -168,7 +168,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true); } - + @Transactional @Override public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); @@ -237,7 +237,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { ruleChain.setFirstRuleNodeId(firstRuleNodeId); - ruleChainDao.save(tenantId, ruleChain); } if (ruleChainMetaData.getConnections() != null) { for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) { @@ -286,6 +285,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } + ruleChain = ruleChainDao.save(tenantId, ruleChain); if (publishSaveEvent) { eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain).entityId(ruleChain.getId()).build()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index a7d49e244b..f044629364 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -97,20 +97,28 @@ public abstract class JpaAbstractDao, D> if (versionedEntity.getVersion() == null) { HasVersion existingEntity = entityManager.find(versionedEntity.getClass(), entity.getUuid()); if (existingEntity != null) { - versionedEntity.setVersion(existingEntity.getVersion()); // manually resetting the version to latest to allow force overwrite of the entity + /* + * manually resetting the version to latest to allow force overwrite of the entity + * */ + versionedEntity.setVersion(existingEntity.getVersion()); } else { return doSave(entity, true, flush); } } - entity = entityManager.merge(entity); + versionedEntity = entityManager.merge(versionedEntity); + /* + * by default, Hibernate doesn't issue an update query and thus version increment + * if the entity was not modified. to bypass this and always increment the version, we do it manually + * */ + versionedEntity.setVersion(versionedEntity.getVersion() + 1); /* - * flushing so that the query is executed right away and the version is incremented, - * then removing the entity from the persistence context so that it is not affected + * flushing and then removing the entity from the persistence context so that it is not affected * by next flushes (e.g. when a transaction is committed) to avoid double version increment * */ entityManager.flush(); - entityManager.detach(entity); + entityManager.detach(versionedEntity); flushed = true; + entity = (E) versionedEntity; } else { entity = entityManager.merge(entity); } @@ -161,6 +169,7 @@ public abstract class JpaAbstractDao, D> log.debug("Remove request: {}", id); } + @Override @Transactional public void removeAllByIds(Collection ids) { JpaRepository repository = getRepository(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java index 644ce40bb8..90510cf2b7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -105,7 +106,37 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { @Test public void testSaveDeviceName0x00_thenSomeDatabaseException() { Device device = getDevice(tenantId1, customerId1, "\u0000"); - assertThatThrownBy(() -> deviceIds.add(deviceDao.save(TenantId.fromUUID(tenantId1), device).getUuidId())); + assertThatThrownBy(() -> deviceIds.add(saveDevice(tenantId1, device).getUuidId())); + } + + @Test + public void testSaveDevice_versionIncrement() { + Device device = getDevice(tenantId1, customerId1, "1ewfewf2"); + device = saveDevice(tenantId1, device); + deviceIds.add(device.getUuidId()); + assertThat(device.getVersion()).isEqualTo(1); + + device.setName(device.getName() + "x"); + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(2); + + device.setName(device.getName() + "x"); + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(3); + } + + @Test + public void testSaveDevice_versionIncrement_noChanges() { + Device device = getDevice(tenantId1, customerId1, "1ewfewf2"); + device = saveDevice(tenantId1, device); + deviceIds.add(device.getUuidId()); + assertThat(device.getVersion()).isEqualTo(1); + + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(2); + + device = saveDevice(tenantId1, device); + assertThat(device.getVersion()).isEqualTo(3); } @Test @@ -126,7 +157,7 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { UUID customerId = Uuids.timeBased(); // send to method getDevice() number = 40, because make random name is bad and name "SEARCH_TEXT_40" don't used Device device = getDevice(tenantId, customerId, 40); - deviceIds.add(deviceDao.save(TenantId.fromUUID(tenantId), device).getUuidId()); + deviceIds.add(saveDevice(tenantId, device).getUuidId()); UUID uuid = device.getId().getId(); Device entity = deviceDao.findById(TenantId.fromUUID(tenantId), uuid); @@ -156,8 +187,8 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { private List createDevices(UUID tenantId1, UUID tenantId2, UUID customerId1, UUID customerId2, int count) { List savedDevicesUUID = new ArrayList<>(); for (int i = 0; i < count / 2; i++) { - savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId1), getDevice(tenantId1, customerId1, i)).getUuidId()); - savedDevicesUUID.add(deviceDao.save(TenantId.fromUUID(tenantId2), getDevice(tenantId2, customerId2, i + count / 2)).getUuidId()); + savedDevicesUUID.add(saveDevice(tenantId1, getDevice(tenantId1, customerId1, i)).getUuidId()); + savedDevicesUUID.add(saveDevice(tenantId2, getDevice(tenantId2, customerId2, i + count / 2)).getUuidId()); } return savedDevicesUUID; } @@ -175,4 +206,9 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { device.setDeviceProfileId(savedDeviceProfile.getId()); return device; } + + private Device saveDevice(UUID tenantId, Device device) { + return deviceDao.save(TenantId.fromUUID(tenantId), device); + } + } From d656ab5540826570a506f5c5cb3753e21a9c4f9c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 Aug 2024 12:33:17 +0300 Subject: [PATCH 30/80] Remove unneeded comment --- .../thingsboard/server/controller/RuleChainControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 3d85c4cc12..4edc3de01e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -375,7 +375,7 @@ public class RuleChainControllerTest extends AbstractControllerTest { ruleChainMetaData.setNodes(ruleNodes); ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); - assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); // in this case when saving metadata, rule chain will also be updated + assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); ruleChain = doGet("/api/ruleChain/" + ruleChain.getId(), RuleChain.class); assertThat(ruleChain.getVersion()).isEqualTo(2); From cbe5150eda97834cf2587b2d271793509c25e403 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 14:44:20 +0300 Subject: [PATCH 31/80] UI: Fixed error tooltip for 'Minimum layout width' in layout configuration --- .../dashboard-settings-dialog.component.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index f6b427cd3e..1c3ffbc922 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -145,15 +145,16 @@
dashboard.min-layout-width
- + warning @@ -190,7 +191,7 @@
dashboard.row-height
- + @@ -245,7 +246,7 @@
dashboard.mobile-row-height
- + From 7e7656f7b439b6b648640a2ec4523849b40b8b39 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 16:32:09 +0300 Subject: [PATCH 32/80] UI: Fixed infinite load when select divided layout --- .../core/services/dashboard-utils.service.ts | 19 +++++++++++++++++ .../dashboard-page.component.ts | 4 ++-- .../layout/dashboard-layout.component.ts | 21 ------------------- .../dashboard-page/layout/layout.models.ts | 1 - .../select-dashboard-breakpoint.component.ts | 2 +- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 8d599ad925..5fdf6f8d62 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -59,6 +59,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models'; import { MediaBreakpoints } from '@shared/models/constants'; import { TranslateService } from '@ngx-translate/core'; +import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models'; @Injectable({ providedIn: 'root' @@ -1066,4 +1067,22 @@ export class DashboardUtilsService { const maxStr = isDefined(currentData?.maxWidth) ? `max ${currentData.maxWidth}px` : ''; return minStr && maxStr ? `${minStr} - ${maxStr}` : `${minStr}${maxStr}`; } + + updatedLayoutForBreakpoint(layout: DashboardPageLayout, breakpointId: BreakpointId) { + let selectBreakpointId: BreakpointId = 'default'; + if (layout.layoutCtx.layoutData[breakpointId]) { + selectBreakpointId = breakpointId; + } + layout.layoutCtx.breakpoint = selectBreakpointId; + const layoutInfo = layout.layoutCtx.layoutData[selectBreakpointId]; + if (layoutInfo.gridSettings) { + layout.layoutCtx.gridSettings = layoutInfo.gridSettings; + } + layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds); + layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; + if (layout.show && layout.layoutCtx.ctrl) { + layout.layoutCtx.ctrl.reload(); + } + layout.layoutCtx.ignoreLoading = true; + } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 0d379b1623..bb5c2cb796 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -450,7 +450,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }), skip(1) ).subscribe(() => { - this.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(); + this.dashboardUtils.updatedLayoutForBreakpoint(this.layouts.main, this.dashboardCtx.breakpoint); this.updateLayoutSizes(); } ) @@ -1129,7 +1129,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) { layout.layoutCtx.layoutData = layoutInfo; layout.layoutCtx.layoutDataChanged.next(); - layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(this.isEdit ? layout.layoutCtx.breakpoint : null, layout.show); + this.dashboardUtils.updatedLayoutForBreakpoint(layout, this.isEdit ? layout.layoutCtx.breakpoint : this.dashboardCtx.breakpoint); this.updateLayoutSizes(); } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index 79eef476b3..9646152eba 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -332,25 +332,4 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo const pos = this.dashboard.getEventGridPosition($event); this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos); } - - updatedCurrentBreakpoint(breakpointId?: BreakpointId, showLayout = true) { - if (!isNotEmptyStr(breakpointId)) { - breakpointId = this.dashboardCtx.breakpoint; - } - if (this.layoutCtx.layoutData[breakpointId]) { - this.layoutCtx.breakpoint = breakpointId; - } else { - this.layoutCtx.breakpoint = 'default'; - } - const layoutInfo = this.layoutCtx.layoutData[this.layoutCtx.breakpoint]; - if (layoutInfo.gridSettings) { - this.layoutCtx.gridSettings = layoutInfo.gridSettings; - } - this.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds); - this.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; - if (showLayout && this.layoutCtx.ctrl) { - this.layoutCtx.ctrl.reload(); - } - this.layoutCtx.ignoreLoading = true; - } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts index 6aa69c51af..3ea9f553a8 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts @@ -21,7 +21,6 @@ export interface ILayoutController { selectWidget(widgetId: string, delay?: number); pasteWidget($event: MouseEvent); pasteWidgetReference($event: MouseEvent); - updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean); } export enum LayoutWidthType { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts index 130d0e02b2..6366154d80 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts @@ -63,7 +63,7 @@ export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy { } selectLayoutChanged() { - this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectedBreakpoint); + this.dashboardUtils.updatedLayoutForBreakpoint(this.dashboardCtrl.layouts.main, this.selectedBreakpoint); this.dashboardCtrl.updateLayoutSizes(); } From 897fe09c5ddf2d6ebe72af4d87df3e48147b86bc Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 16:50:58 +0300 Subject: [PATCH 33/80] UI: Improved style for select dashboard breakpoint --- .../layout/add-new-breakpoint-dialog.component.html | 4 ++-- .../layout/select-dashboard-breakpoint.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html index 8baab0f295..15b7ee173a 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html @@ -30,7 +30,7 @@ layout.breakpoint {{ getName(this.addBreakpointFormGroup.get('newBreakpointId').value) }} - + {{ getIcon(breakpoint) }}
{{ getName(breakpoint) }}
{{ getSizeDescription(breakpoint) }}
@@ -41,7 +41,7 @@ layout.copy-from {{ getName(this.addBreakpointFormGroup.get('copyFrom').value) }} - + {{ getIcon(breakpoint) }}
{{ getName(breakpoint) }}
{{ getSizeDescription(breakpoint) }}
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html index 417c8d9e26..790835352d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html @@ -21,7 +21,7 @@ [(ngModel)]="selectedBreakpoint" (ngModelChange)="selectLayoutChanged()"> {{ getName(selectedBreakpoint) }} - + {{ getIcon(breakpointId) }}
{{ getName(breakpointId) }}
{{ getSizeDescription(breakpointId) }}
From 6c1b03dc47203c9918e7ca03e0a5ec57b1d22279 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 28 Aug 2024 17:57:47 +0300 Subject: [PATCH 34/80] UI: Fixed incorrect min-width calculation for dashboard layout in edit preview mode at specific breakpoints --- .../dashboard-page.component.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 0d379b1623..5edd69bb7f 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -759,6 +759,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC const prevMainLayoutWidth = this.mainLayoutSize.width; const prevMainLayoutHeight = this.mainLayoutSize.height; const prevMainLayoutMaxWidth = this.mainLayoutSize.maxWidth; + const prevMainLayoutMinWidth = this.mainLayoutSize.minWidth; if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') { this.mainLayoutSize.width = '100%'; } else { @@ -773,19 +774,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC const xOffset = this.dashboardContainer.nativeElement.getBoundingClientRect().x; const breakpoint = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint); - let maxWidth: string; - if (breakpoint?.maxWidth) { - if (this.isMobileSize(breakpoint)) { - maxWidth = `${breakpoint.maxWidth}px`; - } else { - maxWidth = `${breakpoint.maxWidth - xOffset}px`; + let maxWidth = '100%'; + let minWidth: string; + + if (breakpoint) { + const isMobile = this.isMobileSize(breakpoint); + if (breakpoint.maxWidth) { + maxWidth = isMobile ? `${breakpoint.maxWidth}px` : `${breakpoint.maxWidth - xOffset}px`; + } + if (breakpoint.minWidth) { + minWidth = isMobile ? `${breakpoint.minWidth}px` : `${breakpoint.minWidth - xOffset}px`; } - } else { - maxWidth = '100%'; } - const minWidth = breakpoint?.minWidth ? `${breakpoint.minWidth}px` : undefined; - this.mainLayoutSize.maxWidth = maxWidth; this.mainLayoutSize.minWidth = minWidth; } else { @@ -793,7 +794,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.mainLayoutSize.minWidth = undefined; } return prevMainLayoutWidth !== this.mainLayoutSize.width || prevMainLayoutHeight !== this.mainLayoutSize.height || - prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth; + prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth || prevMainLayoutMinWidth !== this.mainLayoutSize.minWidth; } private updateRightLayoutSize(): boolean { From 779acf1891788d4a4bcd5c0e30118c1262ab6c03 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 28 Aug 2024 23:00:08 +0200 Subject: [PATCH 35/80] used sys tenant id for old protos --- .../subscription/DefaultTbLocalSubscriptionService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index ceafea6553..bd2ae08474 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -212,8 +212,13 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @Override public void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback) { + TenantId tenantId; + if (subEventCallback.getTenantIdMSB() == 0 && subEventCallback.getTenantIdLSB() == 0) { + tenantId = TenantId.SYS_TENANT_ID; //TODO: remove after release + } else { + tenantId = TenantId.fromUUID(new UUID(subEventCallback.getTenantIdMSB(), subEventCallback.getTenantIdLSB())); + } UUID entityId = new UUID(subEventCallback.getEntityIdMSB(), subEventCallback.getEntityIdLSB()); - TenantId tenantId = TenantId.fromUUID(new UUID(subEventCallback.getTenantIdMSB(), subEventCallback.getTenantIdLSB())); onSubEventCallback(tenantId, entityId, subEventCallback.getSeqNumber(), new TbEntityUpdatesInfo(subEventCallback.getAttributesUpdateTs(), subEventCallback.getTimeSeriesUpdateTs()), callback); } From 399c07259503c2fe5616b0c4d9a75e0cbe639941 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 28 Aug 2024 23:26:00 +0200 Subject: [PATCH 36/80] revert changes that are not related to this PR --- .../DefaultTbLocalSubscriptionService.java | 2 +- .../subscription/TbAlarmsSubscription.java | 8 ++++-- .../subscription/TbAttributeSubscription.java | 8 ++++-- .../subscription/TbEntityLocalSubsInfo.java | 28 +------------------ .../service/subscription/TbSubscription.java | 23 ++++++++++++--- .../subscription/TbSubscriptionUtils.java | 5 ++-- .../subscription/TbSubscriptionsInfo.java | 7 ++--- .../TbTimeSeriesSubscription.java | 8 ++++-- .../sub/AbstractNotificationSubscription.java | 5 ---- .../sub/NotificationsCountSubscription.java | 5 ---- .../sub/NotificationsSubscription.java | 5 ---- 11 files changed, 44 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index bd2ae08474..9e08e0359b 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -227,7 +227,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer onSubEventCallback(tenantId, entityId.getId(), seqNumber, entityUpdatesInfo, callback); } - public void onSubEventCallback(TenantId tenantId, UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { + private void onSubEventCallback(TenantId tenantId, UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { log.debug("[{}][{}][{}] Processing sub event callback: {}.", tenantId, entityId, seqNumber, entityUpdatesInfo); entityUpdates.put(entityId, entityUpdatesInfo); Set> pendingSubs = null; diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java index a9ab7797b7..a1d0aec29f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java @@ -36,8 +36,12 @@ public class TbAlarmsSubscription extends TbSubscription subscription : subs) { - if (subscription.getType() != sub.getType()) { - continue; // skip unchanged types - } switch (subscription.getType()) { case NOTIFICATIONS: case NOTIFICATIONS_COUNT: @@ -175,7 +151,6 @@ public class TbEntityLocalSubsInfo { var attrSub = (TbAttributeSubscription) subscription; if (!newState.attrAllKeys && attrSub.isAllKeys()) { newState.attrAllKeys = true; - newState.attrKeys = null; continue; } if (newState.attrKeys == null) { @@ -188,7 +163,6 @@ public class TbEntityLocalSubsInfo { var tsSub = (TbTimeSeriesSubscription) subscription; if (!newState.tsAllKeys && tsSub.isAllKeys()) { newState.tsAllKeys = true; - newState.tsKeys = null; continue; } if (newState.tsKeys == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java index 68c042ea51..6d210befc0 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java @@ -15,25 +15,40 @@ */ package org.thingsboard.server.service.subscription; +import lombok.AllArgsConstructor; import lombok.Data; -import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Objects; import java.util.function.BiConsumer; @Data -@EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY, doNotUseGetters = true) +@AllArgsConstructor public abstract class TbSubscription { - @EqualsAndHashCode.Exclude private final String serviceId; private final String sessionId; private final int subscriptionId; private final TenantId tenantId; private final EntityId entityId; private final TbSubscriptionType type; - @EqualsAndHashCode.Exclude private final BiConsumer, T> updateProcessor; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TbSubscription that = (TbSubscription) o; + return subscriptionId == that.subscriptionId && + sessionId.equals(that.sessionId) && + tenantId.equals(that.tenantId) && + entityId.equals(that.entityId) && + type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(sessionId, subscriptionId, tenantId, entityId, type); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 3d0037caba..f5572a9a5c 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -109,9 +109,8 @@ public class TbSubscriptionUtils { .type(event); if (!ComponentLifecycleEvent.DELETED.equals(event)) { builder.info(new TbSubscriptionsInfo(proto.getNotifications(), proto.getAlarms(), - proto.getTsAllKeys(), proto.getAttrAllKeys(), - proto.getTsKeysCount() > 0 ? new HashSet<>(proto.getTsKeysList()) : null, - proto.getAttrKeysCount() > 0 ? new HashSet<>(proto.getAttrKeysList()) : null, + proto.getTsAllKeys(), proto.getTsKeysCount() > 0 ? new HashSet<>(proto.getTsKeysList()) : null, + proto.getAttrAllKeys(), proto.getAttrKeysCount() > 0 ? new HashSet<>(proto.getAttrKeysList()) : null, proto.getSeqNumber())); } return builder.build(); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java index 7430d2e386..48464d5297 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionsInfo.java @@ -27,17 +27,16 @@ import java.util.Set; */ @RequiredArgsConstructor @AllArgsConstructor -@EqualsAndHashCode +@EqualsAndHashCode(exclude = {"seqNumber"}) @ToString public class TbSubscriptionsInfo { protected boolean notifications; protected boolean alarms; protected boolean tsAllKeys; - protected boolean attrAllKeys; // primitives first for equals performance protected Set tsKeys; + protected boolean attrAllKeys; protected Set attrKeys; - @EqualsAndHashCode.Exclude protected int seqNumber; public boolean isEmpty() { @@ -49,7 +48,7 @@ public class TbSubscriptionsInfo { } protected TbSubscriptionsInfo copy(int seqNumber) { - return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, attrAllKeys, tsKeys, attrKeys, seqNumber); + return new TbSubscriptionsInfo(notifications, alarms, tsAllKeys, tsKeys, attrAllKeys, attrKeys, seqNumber); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java index 0fc9a80122..10a24106ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeSeriesSubscription.java @@ -53,8 +53,12 @@ public class TbTimeSeriesSubscription extends TbSubscription extends TbSubscription super(serviceId, sessionId, subscriptionId, tenantId, entityId, type, updateProcessor); } - @Override - protected boolean canEqual(final Object other) { - return other instanceof AbstractNotificationSubscription; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java index 491acedb1e..171416a63d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java @@ -34,11 +34,6 @@ public class NotificationsCountSubscription extends AbstractNotificationSubscrip super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS_COUNT, updateProcessor); } - @Override - protected boolean canEqual(final Object other) { - return other instanceof NotificationsCountSubscription; - } - public UnreadNotificationsCountUpdate createUpdate() { return UnreadNotificationsCountUpdate.builder() .cmdId(getSubscriptionId()) diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java index 867f48b72e..8306e7f95c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java @@ -47,11 +47,6 @@ public class NotificationsSubscription extends AbstractNotificationSubscription< this.limit = limit; } - @Override - protected boolean canEqual(final Object other) { - return other instanceof NotificationsSubscription; - } - public UnreadNotificationsUpdate createFullUpdate() { return UnreadNotificationsUpdate.builder() .cmdId(getSubscriptionId()) From b56f75a99675a110b6efe23fd17162c02e108b98 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 29 Aug 2024 10:45:34 +0300 Subject: [PATCH 37/80] added validation on init --- .../engine/aws/lambda/TbAwsLambdaNode.java | 25 ++++++- .../aws/lambda/TbAwsLambdaNodeTest.java | 72 +++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 2512125ff1..d20ff2f75c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -62,9 +62,7 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); - if (StringUtils.isBlank(config.getFunctionName())) { - throw new TbNodeException("Function name must be set!", true); - } + validateConfig(); try { AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() @@ -138,6 +136,27 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { return TbMsg.transformMsgMetadata(origMsg, metaData); } + private void validateConfig() throws TbNodeException { + if (StringUtils.isBlank(config.getFunctionName())) { + throw new TbNodeException("Function name must be set!", true); + } + if (StringUtils.isBlank(config.getAccessKey())) { + throw new TbNodeException("Access Key must be set!", true); + } + if (StringUtils.isBlank(config.getSecretKey())) { + throw new TbNodeException("Secret Access Key must be set!", true); + } + if (StringUtils.isBlank(config.getRegion())) { + throw new TbNodeException("Region must be set!", true); + } + if (config.getConnectionTimeout() < 0) { + throw new TbNodeException("Min connection timeout is 0!", true); + } + if (config.getRequestTimeout() < 0) { + throw new TbNodeException("Min request timeout is 0!", true); + } + } + @Override public void destroy() { if (client != null) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java index 4ee954991c..eed3613a48 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java @@ -75,10 +75,14 @@ public class TbAwsLambdaNodeTest { void setUp() { node = new TbAwsLambdaNode(); config = new TbAwsLambdaNodeConfiguration().defaultConfiguration(); + config.setAccessKey("accessKey"); + config.setSecretKey("secretKey"); + config.setFunctionName("new-function"); } @Test public void verifyDefaultConfig() { + config = new TbAwsLambdaNodeConfiguration().defaultConfiguration(); assertThat(config.getAccessKey()).isNull(); assertThat(config.getSecretKey()).isNull(); assertThat(config.getRegion()).isEqualTo(("us-east-1")); @@ -97,7 +101,70 @@ public class TbAwsLambdaNodeTest { var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); assertThatThrownBy(() -> node.init(ctx, configuration)) .isInstanceOf(TbNodeException.class) - .hasMessage("Function name must be set!"); + .hasMessage("Function name must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { + config.setAccessKey(accessKey); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Access Key must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { + config.setSecretKey(secretAccessKey); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Secret Access Key must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + public void givenInvalidRegion_whenInit_thenThrowsException(String region) { + config.setRegion(region); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Region must be set!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { + config.setConnectionTimeout(-100); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Min connection timeout is 0!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { + config.setRequestTimeout(-100); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatThrownBy(() -> node.init(ctx, configuration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Min request timeout is 0!") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); } @ParameterizedTest @@ -281,9 +348,6 @@ public class TbAwsLambdaNodeTest { } private void init() { - config.setAccessKey("accessKey"); - config.setSecretKey("secretKey"); - config.setFunctionName("new-function"); ReflectionTestUtils.setField(node, "client", clientMock); ReflectionTestUtils.setField(node, "config", config); } From 5256cae80406a06842cff423a2ecdfe2361fe2b0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 29 Aug 2024 12:35:30 +0300 Subject: [PATCH 38/80] used jakarta validation constraints --- .../engine/aws/lambda/TbAwsLambdaNode.java | 26 ++------ .../lambda/TbAwsLambdaNodeConfiguration.java | 8 +++ .../aws/lambda/TbAwsLambdaNodeTest.java | 59 +++++++------------ 3 files changed, 32 insertions(+), 61 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index d20ff2f75c..06dc0310ae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -39,6 +39,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; + @Slf4j @RuleNode( type = ComponentType.EXTERNAL, @@ -62,7 +64,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); - validateConfig(); + String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; + validateFields(config, errorPrefix); try { AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() @@ -136,27 +139,6 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { return TbMsg.transformMsgMetadata(origMsg, metaData); } - private void validateConfig() throws TbNodeException { - if (StringUtils.isBlank(config.getFunctionName())) { - throw new TbNodeException("Function name must be set!", true); - } - if (StringUtils.isBlank(config.getAccessKey())) { - throw new TbNodeException("Access Key must be set!", true); - } - if (StringUtils.isBlank(config.getSecretKey())) { - throw new TbNodeException("Secret Access Key must be set!", true); - } - if (StringUtils.isBlank(config.getRegion())) { - throw new TbNodeException("Region must be set!", true); - } - if (config.getConnectionTimeout() < 0) { - throw new TbNodeException("Min connection timeout is 0!", true); - } - if (config.getRequestTimeout() < 0) { - throw new TbNodeException("Min request timeout is 0!", true); - } - } - @Override public void destroy() { if (client != null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java index c7c66599d5..b395f6b15e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.aws.lambda; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; @@ -23,12 +25,18 @@ public class TbAwsLambdaNodeConfiguration implements NodeConfiguration node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Function name must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -111,12 +107,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { config.setAccessKey(accessKey); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Access Key must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -124,12 +115,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { config.setSecretKey(secretAccessKey); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Secret Access Key must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -137,34 +123,19 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidRegion_whenInit_thenThrowsException(String region) { config.setRegion(region); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Region must be set!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @Test public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { config.setConnectionTimeout(-100); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Min connection timeout is 0!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @Test public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { config.setRequestTimeout(-100); - var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); - assertThatThrownBy(() -> node.init(ctx, configuration)) - .isInstanceOf(TbNodeException.class) - .hasMessage("Min request timeout is 0!") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + verifyDataValidationExceptionOnInit(); } @ParameterizedTest @@ -347,6 +318,16 @@ public class TbAwsLambdaNodeTest { assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg); } + private void verifyDataValidationExceptionOnInit() { + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("test"); + when(ctx.getSelf()).thenReturn(ruleNode); + String errorPrefix = "'test' node configuration is invalid: "; + assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining(errorPrefix); + } + private void init() { ReflectionTestUtils.setField(node, "client", clientMock); ReflectionTestUtils.setField(node, "config", config); From 5602807dbe87978f68366f290ed8484599b8d5b8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak <50847617+AndriiLandiak@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:51:20 +0300 Subject: [PATCH 39/80] Introduce Edge queue to handle edge notification messages --- .../server/actors/app/AppActor.java | 21 -- .../device/DeviceActorMessageProcessor.java | 7 +- .../server/actors/tenant/TenantActor.java | 25 -- .../server/controller/EdgeController.java | 3 +- .../edge/DefaultEdgeNotificationService.java | 222 ------------ .../service/edge/EdgeContextComponent.java | 14 +- .../edge/EdgeEventSourcingListener.java | 3 +- ...DefaultEdgeInstallInstructionsService.java | 1 + ...DefaultEdgeUpgradeInstructionsService.java | 3 +- .../EdgeUpgradeInstructionsService.java | 1 + .../service/edge/rpc/EdgeGrpcService.java | 157 ++++---- .../service/edge/rpc/EdgeGrpcSession.java | 62 +++- .../service/edge/rpc/EdgeRpcService.java | 3 +- .../service/edge/rpc/EdgeSessionState.java | 1 + .../fetch/BasePageableEdgeEventFetcher.java | 2 +- .../rpc/fetch/RuleChainsEdgeEventFetcher.java | 2 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 9 +- .../rule/RuleChainEdgeProcessor.java | 2 +- .../rpc/sync/DefaultEdgeRequestsService.java | 6 +- .../entitiy/EntityStateSourcingListener.java | 18 +- .../entitiy/edge/DefaultTbEdgeService.java | 7 +- .../service/entitiy/edge/TbEdgeService.java | 2 + .../AbstractPartitionBasedService.java | 4 +- .../queue/DefaultTbClusterService.java | 123 +++++-- .../queue/DefaultTbCoreConsumerService.java | 37 +- .../queue/DefaultTbEdgeConsumerService.java | 334 ++++++++++++++++++ .../DefaultTbRuleEngineConsumerService.java | 2 +- .../service/queue/EdgeConsumerStats.java | 99 ++++++ .../service/queue/TbCoreConsumerStats.java | 31 +- .../service/queue/TbEdgeConsumerService.java | 23 ++ .../processing/AbstractConsumerService.java | 3 +- .../AbstractSubscriptionService.java | 2 +- .../src/main/resources/thingsboard.yml | 37 +- ...CaffeineCacheDefaultConfigurationTest.java | 6 +- .../controller/AbstractNotifyEntityTest.java | 26 +- .../server/controller/EdgeControllerTest.java | 57 ++- .../controller/EntityQueryControllerTest.java | 2 +- .../controller/TenantControllerTest.java | 14 +- .../server/edge/AbstractEdgeTest.java | 28 +- .../server/edge/DeviceEdgeTest.java | 67 +--- .../server/edge/TelemetryEdgeTest.java | 33 +- .../thingsboard/server/edge/UserEdgeTest.java | 15 +- .../discovery/HashPartitionServiceTest.java | 2 + .../notification/NotificationApiTest.java | 4 +- .../notification/NotificationRuleApiTest.java | 4 +- .../queue/DefaultTbClusterServiceTest.java | 7 +- .../service/script/TbelInvokeServiceTest.java | 4 +- .../ota/sql/OtaLwM2MIntegrationTest.java | 4 +- .../cache/RedisTbTransactionalCache.java | 5 +- .../cache/TbCaffeineCacheConfiguration.java | 4 +- .../server/cluster/TbClusterService.java | 20 +- .../server/dao/edge/EdgeService.java | 3 + .../server/common/data/CacheConstants.java | 1 + .../server/common/data/DataConstants.java | 2 + .../server/common/msg/MsgType.java | 5 +- .../common/msg/edge/EdgeEventUpdateMsg.java | 1 + .../common/msg/edge/EdgeHighPriorityMsg.java | 21 +- .../common/msg/edge/EdgeSessionMsg.java | 11 +- .../common/msg/edge/FromEdgeSyncResponse.java | 1 + .../common/msg/edge/ToEdgeSyncRequest.java | 2 + .../server/common/util/ProtoUtils.java | 59 +++- common/proto/src/main/proto/queue.proto | 43 ++- .../server/common/util/ProtoUtilsTest.java | 13 +- .../servicebus/TbServiceBusQueueConfigs.java | 7 + .../DefaultTbServiceInfoProvider.java | 2 +- .../queue/discovery/HashPartitionService.java | 46 +-- .../server/queue/discovery/QueueKey.java | 2 + .../server/queue/discovery/TopicService.java | 30 +- .../discovery/event/PartitionChangeEvent.java | 21 +- .../queue/kafka/TbKafkaTopicConfigs.java | 6 + .../provider/AwsSqsMonolithQueueFactory.java | 47 ++- .../provider/AwsSqsTbCoreQueueFactory.java | 42 ++- .../AwsSqsTbRuleEngineQueueFactory.java | 38 +- .../AwsSqsTbVersionControlQueueFactory.java | 1 - .../InMemoryMonolithQueueFactory.java | 27 +- .../provider/KafkaMonolithQueueFactory.java | 71 +++- .../provider/KafkaTbCoreQueueFactory.java | 55 ++- .../KafkaTbRuleEngineQueueFactory.java | 32 +- .../provider/PubSubMonolithQueueFactory.java | 45 ++- .../provider/PubSubTbCoreQueueFactory.java | 42 ++- .../PubSubTbRuleEngineQueueFactory.java | 20 +- .../RabbitMqMonolithQueueFactory.java | 48 ++- .../provider/RabbitMqTbCoreQueueFactory.java | 55 ++- .../RabbitMqTbRuleEngineQueueFactory.java | 23 +- .../RabbitMqTransportQueueFactory.java | 4 +- .../ServiceBusMonolithQueueFactory.java | 46 ++- .../ServiceBusTbCoreQueueFactory.java | 42 ++- .../ServiceBusTbRuleEngineQueueFactory.java | 23 +- .../ServiceBusTransportQueueFactory.java | 4 +- .../queue/provider/TbCoreQueueFactory.java | 10 + .../provider/TbCoreQueueProducerProvider.java | 17 + .../provider/TbQueueProducerProvider.java | 8 +- .../TbRuleEngineProducerProvider.java | 20 +- .../provider/TbRuleEngineQueueFactory.java | 11 +- .../TbTransportQueueProducerProvider.java | 16 +- .../TbVersionControlProducerProvider.java | 16 +- .../pubsub/TbPubSubSubscriptionSettings.java | 6 + .../rabbitmq/TbRabbitMqQueueArguments.java | 5 + .../queue/settings/TbQueueEdgeSettings.java | 31 ++ .../queue/sqs/TbAwsSqsQueueAttributes.java | 5 + .../server/common/stats/StatsType.java | 5 +- .../DefaultClusterVersionControlService.java | 3 +- .../server/dao/edge/EdgeServiceImpl.java | 64 ++-- .../dao/edge/EdgeSessionCaffeineCache.java | 33 ++ .../dao/edge/EdgeSessionRedisCache.java | 36 ++ .../resources/application-test.properties | 3 + 106 files changed, 2025 insertions(+), 773 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java rename application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java => common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java (55%) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 93cf5bd8c7..622812b327 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -37,12 +37,10 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -110,11 +108,6 @@ public class AppActor extends ContextAwareActor { case REMOVE_RPC_TO_DEVICE_ACTOR_MSG: onToDeviceActorMsg((TenantAwareMsg) msg, true); break; - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG: - onToEdgeSessionMsg((EdgeSessionMsg) msg); - break; case SESSION_TIMEOUT_MSG: ctx.broadcastToChildrenByType(msg, EntityType.TENANT); break; @@ -206,20 +199,6 @@ public class AppActor extends ContextAwareActor { () -> true)); } - private void onToEdgeSessionMsg(EdgeSessionMsg msg) { - TbActorRef target = null; - if (ModelConstants.SYSTEM_TENANT.equals(msg.getTenantId())) { - log.warn("Message has system tenant id: {}", msg); - } else { - target = getOrCreateTenantActor(msg.getTenantId()).orElse(null); - } - if (target != null) { - target.tellWithHighPriority(msg); - } else { - log.debug("[{}] Invalid edge session msg: {}", msg.getTenantId(), msg); - } - } - @Override public ProcessFailureStrategy onProcessFailure(TbActorMsg msg, Throwable t) { log.error("Failed to process msg: {}", msg, t); diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index b34ddca1af..3e8cd9592f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; @@ -214,7 +215,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId); try { if (systemContext.getEdgeService().isEdgeActiveAsync(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE).get()) { - saveRpcRequestToEdgeQueue(request, requestId).get(); + saveRpcRequestToEdgeQueue(request, requestId); } else { log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}. The Edge is currently offline or unreachable", tenantId, deviceId, edgeId.getId(), request); } @@ -921,7 +922,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso systemContext.getTbCoreToTransportService().process(nodeId, msg); } - private ListenableFuture saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { + private void saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { ObjectNode body = JacksonUtil.newObjectNode(); body.put("requestId", requestId); body.put("requestUUID", msg.getId().toString()); @@ -935,7 +936,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, body); - return systemContext.getEdgeEventService().saveAsync(edgeEvent); + systemContext.getClusterService().onEdgeHighPriorityMsg(new EdgeHighPriorityMsg(tenantId, edgeEvent)); } void restoreSessions() { diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index d55e08143b..5edecba31a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -33,9 +33,7 @@ import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -48,14 +46,12 @@ import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; -import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg; -import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; @@ -163,11 +159,6 @@ public class TenantActor extends RuleChainManagerActor { case RULE_CHAIN_TO_RULE_CHAIN_MSG: onRuleChainMsg((RuleChainAwareMsg) msg); break; - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG: - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG: - onToEdgeSessionMsg((EdgeSessionMsg) msg); - break; default: return false; } @@ -270,18 +261,6 @@ public class TenantActor extends RuleChainManagerActor { initRuleChains(); } } - if (msg.getEntityId().getEntityType() == EntityType.EDGE && isCore) { - EdgeId edgeId = new EdgeId(msg.getEntityId().getId()); - EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService(); - if (edgeRpcService != null) { - if (msg.getEvent() == ComponentLifecycleEvent.DELETED) { - edgeRpcService.deleteEdge(tenantId, edgeId); - } else if (msg.getEvent() == ComponentLifecycleEvent.UPDATED) { - Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId); - edgeRpcService.updateEdge(tenantId, edge); - } - } - } if (msg.getEntityId().getEntityType() == EntityType.DEVICE && ComponentLifecycleEvent.DELETED == msg.getEvent() && isMyPartition(msg.getEntityId())) { DeviceId deviceId = (DeviceId) msg.getEntityId(); onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true); @@ -311,10 +290,6 @@ public class TenantActor extends RuleChainManagerActor { () -> true); } - private void onToEdgeSessionMsg(EdgeSessionMsg msg) { - systemContext.getEdgeRpcService().onToEdgeSessionMsg(tenantId, msg); - } - private ApiUsageState getApiUsageState() { if (apiUsageState == null) { apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId)); diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 26c30f85d1..2cb8eb7378 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -478,8 +478,7 @@ public class EdgeController extends BaseController { edgeId = checkNotNull(edgeId); SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); - ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId); - edgeRpcServiceOpt.get().processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); + edgeRpcServiceOpt.get().processSyncRequest(tenantId, edgeId, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse)); } else { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java deleted file mode 100644 index 740b19e5d8..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright ยฉ 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.edge; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.customer.CustomerEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantProfileEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; -import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; - -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; - -@Service -@TbCoreComponent -@Slf4j -public class DefaultEdgeNotificationService implements EdgeNotificationService { - - public static final String EDGE_IS_ROOT_BODY_KEY = "isRoot"; - - @Autowired - private EdgeService edgeService; - - @Autowired - private EdgeProcessor edgeProcessor; - - @Autowired - private AssetEdgeProcessor assetProcessor; - - @Autowired - private AssetProfileEdgeProcessor assetProfileEdgeProcessor; - - @Autowired - private DeviceEdgeProcessor deviceProcessor; - - @Autowired - private DeviceProfileEdgeProcessor deviceProfileEdgeProcessor; - - @Autowired - private EntityViewEdgeProcessor entityViewProcessor; - - @Autowired - private DashboardEdgeProcessor dashboardProcessor; - - @Autowired - private RuleChainEdgeProcessor ruleChainProcessor; - - @Autowired - private UserEdgeProcessor userProcessor; - - @Autowired - private CustomerEdgeProcessor customerProcessor; - - @Autowired - private OtaPackageEdgeProcessor otaPackageProcessor; - - @Autowired - private WidgetBundleEdgeProcessor widgetBundleProcessor; - - @Autowired - private WidgetTypeEdgeProcessor widgetTypeProcessor; - - @Autowired - private QueueEdgeProcessor queueProcessor; - - @Autowired - private TenantEdgeProcessor tenantEdgeProcessor; - - @Autowired - private TenantProfileEdgeProcessor tenantProfileEdgeProcessor; - - @Autowired - private AlarmEdgeProcessor alarmProcessor; - - @Autowired - private RelationEdgeProcessor relationProcessor; - - @Autowired - private ResourceEdgeProcessor resourceEdgeProcessor; - - @Autowired - private NotificationEdgeProcessor notificationEdgeProcessor; - - @Autowired - private OAuth2EdgeProcessor oAuth2EdgeProcessor; - - @Autowired - protected ApplicationEventPublisher eventPublisher; - - @Value("${actors.system.edge_dispatcher_pool_size:4}") - private int edgeDispatcherSize; - - private ExecutorService executor; - - @PostConstruct - public void initExecutor() { - executor = ThingsBoardExecutors.newWorkStealingPool(edgeDispatcherSize, "edge-notifications"); - } - - @PreDestroy - public void shutdownExecutor() { - if (executor != null) { - executor.shutdownNow(); - } - } - - @Override - public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) { - edge.setRootRuleChainId(ruleChainId); - Edge savedEdge = edgeService.saveEdge(edge); - ObjectNode isRootBody = JacksonUtil.newObjectNode(); - isRootBody.put(EDGE_IS_ROOT_BODY_KEY, Boolean.TRUE); - eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edge.getId()).entityId(ruleChainId) - .body(JacksonUtil.toString(isRootBody)).actionType(ActionType.UPDATED).build()); - return savedEdge; - } - - @Override - public void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) { - TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); - log.debug("[{}] Pushing notification to edge {}", tenantId, edgeNotificationMsg); - final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60); - try { - executor.submit(() -> { - try { - if (deadline < System.nanoTime()) { - log.warn("[{}] Skipping notification message because deadline reached {}", tenantId, edgeNotificationMsg); - return; - } - EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); - switch (type) { - case EDGE -> edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg); - case ASSET -> assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case ASSET_PROFILE -> assetProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DEVICE -> deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DEVICE_PROFILE -> deviceProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case ENTITY_VIEW -> entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DASHBOARD -> dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case RULE_CHAIN -> ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case USER -> userProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case CUSTOMER -> customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg); - case OTA_PACKAGE -> otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case WIDGETS_BUNDLE -> widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case WIDGET_TYPE -> widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case QUEUE -> queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case ALARM -> alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg); - case ALARM_COMMENT -> alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg); - case RELATION -> relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); - case TENANT -> tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case TENANT_PROFILE -> tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> - notificationEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case DOMAIN, OAUTH2_CLIENT -> oAuth2EdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); - } - } catch (Exception e) { - callBackFailure(tenantId, edgeNotificationMsg, callback, e); - } - }); - callback.onSuccess(); - } catch (Exception e) { - callBackFailure(tenantId, edgeNotificationMsg, callback, e); - } - } - - private void callBackFailure(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) { - log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable); - callback.onFailure(throwable); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 2d9fa01796..f0f41e3083 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -48,6 +48,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessorFactory; @@ -97,6 +98,9 @@ public class EdgeContextComponent { @Autowired private EdgeService edgeService; + @Autowired(required = false) + private EdgeRpcService edgeRpcService; + @Autowired private EdgeEventService edgeEventService; @@ -224,19 +228,19 @@ public class EdgeContextComponent { private AdminSettingsEdgeProcessor adminSettingsProcessor; @Autowired - private OtaPackageEdgeProcessor otaPackageEdgeProcessor; + private OtaPackageEdgeProcessor otaPackageProcessor; @Autowired - private QueueEdgeProcessor queueEdgeProcessor; + private QueueEdgeProcessor queueProcessor; @Autowired - private TenantEdgeProcessor tenantEdgeProcessor; + private TenantEdgeProcessor tenantProcessor; @Autowired - private TenantProfileEdgeProcessor tenantProfileEdgeProcessor; + private TenantProfileEdgeProcessor tenantProfileProcessor; @Autowired - private ResourceEdgeProcessor resourceEdgeProcessor; + private ResourceEdgeProcessor resourceProcessor; @Autowired private NotificationEdgeProcessor notificationEdgeProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 789320e352..840750a781 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -130,8 +130,7 @@ public class EdgeEventSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) - && ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType())) { + if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) && ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType())) { return; } try { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index 64f96aa4e4..7c8489067c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -94,4 +94,5 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad protected String getBaseDirName() { return INSTALL_DIR; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 275de6bafe..2f783c98ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -74,7 +74,8 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad Optional attributeKvEntryOpt = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get(); if (attributeKvEntryOpt.isPresent()) { String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString()); - return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersion); + String appVersionFormatted = appVersion.replace("-SNAPSHOT", ""); + return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersionFormatted); } return false; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index 2517b615a8..47bde24bc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -31,4 +31,5 @@ public interface EdgeUpgradeInstructionsService { void setAppVersion(String version); boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 46bf3f69a4..5d45e1c3d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -28,14 +28,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -47,12 +50,14 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.EdgeSessionMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; import org.thingsboard.server.gen.edge.v1.RequestMsg; import org.thingsboard.server.gen.edge.v1.ResponseMsg; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.state.DefaultDeviceStateService; @@ -84,7 +89,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); - private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); @Value("${edges.rpc.port}") @@ -111,7 +115,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Value("${edges.send_scheduler_pool_size}") private int sendSchedulerPoolSize; + @Value("${edges.max_high_priority_queue_size_per_session:10000}") + private int maxHighPriorityQueueSizePerSession; + @Autowired + @Lazy private EdgeContextComponent ctx; @Autowired @@ -120,6 +128,12 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Autowired private TbClusterService clusterService; + @Autowired + private TbServiceInfoProvider serviceInfoProvider; + + @Autowired + private TbTransactionalCache edgeIdServiceIdCache; + private Server server; private ScheduledExecutorService edgeEventProcessingExecutorService; @@ -188,79 +202,100 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Override public StreamObserver handleMsgs(StreamObserver outputStream) { - return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService, this.maxInboundMessageSize).getInputStream(); + return new EdgeGrpcSession(ctx, + outputStream, + this::onEdgeConnect, + this::onEdgeDisconnect, + sendDownlinkExecutorService, + this.maxInboundMessageSize, + this.maxHighPriorityQueueSizePerSession).getInputStream(); } @Override public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) { - executorService.execute(() -> { - switch (msg.getMsgType()) { - case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: - EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; - log.trace("[{}] onToEdgeSessionMsg [{}]", tenantId, msg); - onEdgeEvent(tenantId, edgeEventUpdateMsg.getEdgeId()); - break; - case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG: - ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; - log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); - startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId()); - break; - case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG: - FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; - log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); - processSyncResponse(fromEdgeSyncResponse); - break; + switch (msg.getMsgType()) { + case EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG -> { + EdgeHighPriorityMsg edgeHighPriorityMsg = (EdgeHighPriorityMsg) msg; + log.trace("[{}] edgeEventMsg [{}]", tenantId, msg); + onEdgeHighPriorityEvent(edgeHighPriorityMsg); } - }); + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG -> { + EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg; + log.trace("[{}] onToEdgeEventUpdateMsg [{}]", tenantId, msg); + onEdgeEventUpdate(tenantId, edgeEventUpdateMsg.getEdgeId()); + } + case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG -> { + ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg; + log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg); + startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId(), toEdgeSyncRequest.getServiceId()); + } + case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG -> { + FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg; + log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg); + processSyncResponse(fromEdgeSyncResponse); + } + } } @Override public void updateEdge(TenantId tenantId, Edge edge) { - executorService.execute(() -> { - EdgeGrpcSession session = sessions.get(edge.getId()); - if (session != null && session.isConnected()) { - log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - session.onConfigurationUpdate(edge); - } else { - log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); - } - }); + EdgeGrpcSession session = sessions.get(edge.getId()); + if (session != null && session.isConnected()) { + log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + session.onConfigurationUpdate(edge); + } else { + log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); + } } @Override public void deleteEdge(TenantId tenantId, EdgeId edgeId) { - executorService.execute(() -> { - EdgeGrpcSession session = sessions.get(edgeId); - if (session != null && session.isConnected()) { - log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); - session.close(); - sessions.remove(edgeId); - final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); - newEventLock.lock(); - try { - sessionNewEvents.remove(edgeId); - } finally { - newEventLock.unlock(); - } - cancelScheduleEdgeEventsCheck(edgeId); - } - }); - } - - private void onEdgeEvent(TenantId tenantId, EdgeId edgeId) { EdgeGrpcSession session = sessions.get(edgeId); if (session != null && session.isConnected()) { - log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId.getId()); + log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); + session.close(); + sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); try { - if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) { - log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId()); - sessionNewEvents.put(edgeId, true); - } + sessionNewEvents.remove(edgeId); } finally { newEventLock.unlock(); } + cancelScheduleEdgeEventsCheck(edgeId); + } + } + + private void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + log.trace("[{}] onEdgeEventUpdate [{}]", tenantId, edgeId.getId()); + updateSessionEventsFlag(tenantId, edgeId); + } + } + + private void onEdgeHighPriorityEvent(EdgeHighPriorityMsg msg) { + TenantId tenantId = msg.getTenantId(); + EdgeEvent edgeEvent = msg.getEdgeEvent(); + EdgeId edgeId = edgeEvent.getEdgeId(); + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId); + session.addEventToHighPriorityQueue(edgeEvent); + updateSessionEventsFlag(tenantId, edgeId); + } + } + + private void updateSessionEventsFlag(TenantId tenantId, EdgeId edgeId) { + final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); + newEventLock.lock(); + try { + if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) { + log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId()); + sessionNewEvents.put(edgeId, true); + } + } finally { + newEventLock.unlock(); } } @@ -279,37 +314,40 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); long lastConnectTs = System.currentTimeMillis(); save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); + edgeIdServiceIdCache.put(edgeId, serviceInfoProvider.getServiceId()); pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); scheduleEdgeEventsCheck(edgeGrpcSession); } - private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId) { + private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) { EdgeGrpcSession session = sessions.get(edgeId); if (session != null) { if (!session.isSyncCompleted()) { - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment")); + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId); } else { boolean success = false; if (session.isConnected()) { session.startSyncProcess(true); success = true; } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, "")); + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, ""), requestServiceId); } } } @Override - public void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer) { + public void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer) { + ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId, serviceInfoProvider.getServiceId()); + UUID requestId = request.getId(); EdgeGrpcSession session = sessions.get(request.getEdgeId()); if (session != null && !session.isSyncCompleted()) { responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); } else { - log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId()); + log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId()); localSyncEdgeRequests.put(requestId, responseConsumer); - clusterService.pushEdgeSyncRequestToCore(request); + clusterService.pushEdgeSyncRequestToEdge(request); scheduleSyncRequestTimeout(request, requestId); } } @@ -415,6 +453,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } else { log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); } + edgeIdServiceIdCache.evict(edgeId); } private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index c4e8f1ec7c..c397807c5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; @@ -96,6 +97,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -106,6 +108,7 @@ import java.util.function.BiConsumer; public final class EdgeGrpcSession implements Closeable { private static final ReentrantLock downlinkMsgLock = new ReentrantLock(); + private static final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected @@ -125,7 +128,7 @@ public final class EdgeGrpcSession implements Closeable { private StreamObserver inputStream; private StreamObserver outputStream; private boolean connected; - private boolean syncCompleted; + private volatile boolean syncCompleted; private Long newStartTs; private Long previousStartTs; @@ -137,11 +140,12 @@ public final class EdgeGrpcSession implements Closeable { private int maxInboundMessageSize; private int clientMaxInboundMessageSize; + private int maxHighPriorityQueueSizePerSession; private ScheduledExecutorService sendDownlinkExecutorService; EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener, - BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) { + BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { this.sessionId = UUID.randomUUID(); this.ctx = ctx; this.outputStream = outputStream; @@ -149,6 +153,7 @@ public final class EdgeGrpcSession implements Closeable { this.sessionCloseListener = sessionCloseListener; this.sendDownlinkExecutorService = sendDownlinkExecutorService; this.maxInboundMessageSize = maxInboundMessageSize; + this.maxHighPriorityQueueSizePerSession = maxHighPriorityQueueSizePerSession; initInputStream(); } @@ -223,7 +228,7 @@ public final class EdgeGrpcSession implements Closeable { } public void startSyncProcess(boolean fullSync) { - log.trace("[{}][{}][{}] Staring edge sync process", this.tenantId, edge.getId(), this.sessionId); + log.info("[{}][{}][{}] Staring edge sync process", this.tenantId, edge.getId(), this.sessionId); syncCompleted = false; interruptGeneralProcessingOnSync(); doSync(new EdgeSyncCursor(ctx, edge, fullSync)); @@ -255,18 +260,23 @@ public final class EdgeGrpcSession implements Closeable { Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { @Override public void onSuccess(Boolean isInterrupted) { - syncCompleted = true; - ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); + markSyncCompletedSendEdgeEventUpdate(); } @Override public void onFailure(Throwable t) { log.error("[{}][{}] Exception during sending sync complete", tenantId, edge.getId(), t); + markSyncCompletedSendEdgeEventUpdate(); } }, ctx.getGrpcCallbackExecutorService()); } } + private void markSyncCompletedSendEdgeEventUpdate() { + syncCompleted = true; + ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId())); + } + private void onUplinkMsg(UplinkMsg uplinkMsg) { if (isRateLimitViolated(uplinkMsg)) { return; @@ -429,6 +439,9 @@ public final class EdgeGrpcSession implements Closeable { private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { try { + if (!highPriorityQueue.isEmpty()) { + processHighPriorityEvents(); + } PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); if (isConnected() && !pageData.getData().isEmpty()) { log.trace("[{}][{}][{}] event(s) are going to be processed.", this.tenantId, this.sessionId, pageData.getData().size()); @@ -472,6 +485,20 @@ public final class EdgeGrpcSession implements Closeable { } } + private void processHighPriorityEvents() { + try { + List highPriorityEvents = new ArrayList<>(); + EdgeEvent event; + while ((event = highPriorityQueue.poll()) != null) { + highPriorityEvents.add(event); + } + List downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); + sendDownlinkMsgsPack(downlinkMsgsPack).get(); + } catch (Exception e) { + log.error("[{}] Failed to process high priority events", this.sessionId, e); + } + } + private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { interruptPreviousSendDownlinkMsgsTask(); @@ -556,7 +583,9 @@ public final class EdgeGrpcSession implements Closeable { DownlinkMsg downlinkMsg = null; try { switch (edgeEvent.getAction()) { - case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { + case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, + CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, + ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { downlinkMsg = convertEntityEventToDownlink(edgeEvent); if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsgId()); @@ -683,15 +712,15 @@ public final class EdgeGrpcSession implements Closeable { case ADMIN_SETTINGS: return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent, this.edgeVersion); case OTA_PACKAGE: - return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getOtaPackageProcessor().convertOtaPackageEventToDownlink(edgeEvent, this.edgeVersion); case TB_RESOURCE: - return ctx.getResourceEdgeProcessor().convertResourceEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getResourceProcessor().convertResourceEventToDownlink(edgeEvent, this.edgeVersion); case QUEUE: - return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getQueueProcessor().convertQueueEventToDownlink(edgeEvent, this.edgeVersion); case TENANT: - return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getTenantProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion); case TENANT_PROFILE: - return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion); + return ctx.getTenantProfileProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion); case NOTIFICATION_RULE: return ctx.getNotificationEdgeProcessor().convertNotificationRuleToDownlink(edgeEvent); case NOTIFICATION_TARGET: @@ -914,4 +943,15 @@ public final class EdgeGrpcSession implements Closeable { } } + public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { + while (highPriorityQueue.size() > maxHighPriorityQueueSizePerSession) { + EdgeEvent oldestHighPriority = highPriorityQueue.poll(); + if (oldestHighPriority != null) { + log.warn("[{}][{}][{}] High priority queue is full. Removing oldest high priority event from queue {}", + this.tenantId, edge.getId(), this.sessionId, oldestHighPriority); + } + } + highPriorityQueue.add(edgeEvent); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java index 12203db0c2..7c832ca760 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -32,5 +32,6 @@ public interface EdgeRpcService { void deleteEdge(TenantId tenantId, EdgeId edgeId); - void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer); + void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer); + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java index 6218b1f9a6..3f925c3787 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java @@ -30,4 +30,5 @@ public class EdgeSessionState { private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); private SettableFuture sendDownlinkMsgsFuture; private ScheduledFuture scheduledSendDownlinkTask; + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java index 747dc5722f..40fc3559ed 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java @@ -35,7 +35,7 @@ public abstract class BasePageableEdgeEventFetcher implements EdgeEventFetche @Override public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { - log.trace("[{}] start fetching edge events [{}]", tenantId, edge.getId()); + log.trace("[{}][{}] start fetching edge events [{}], pageLink {}", getClass().getSimpleName(), tenantId, edge.getId(), pageLink); PageData entities = fetchEntities(tenantId, edge, pageLink); List result = new ArrayList<>(); if (!entities.getData().isEmpty()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java index 5569c30018..328072aeea 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.rule.RuleChainService; -import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; +import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY; @Slf4j @AllArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 8dee74aac1..3c18e02324 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -374,23 +374,20 @@ public abstract class BaseEdgeProcessor { }, dbCallbackExecutorService); } - private boolean doSaveIfEdgeIsOffline(EdgeEventType type, - EdgeEventActionType action) { + private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) { return switch (action) { case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT, UPDATED_COMMENT -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE, - ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> - true; + ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; default -> false; }; }; } private ListenableFuture doSaveEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventType type, EdgeEventActionType action, EntityId entityId, JsonNode body) { - log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], " + - "action [{}], entityId [{}], body [{}]", + log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], action [{}], entityId [{}], body [{}]", tenantId, edgeId, type, action, entityId, body); EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, type, action, entityId, body); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index b71493fa39..310d8d9b9b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -32,7 +32,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; +import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY; @Slf4j @Component diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 6e4117869b..46f1d7ef57 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -113,10 +113,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { if (ruleChainMetadataRequestMsg.getRuleChainIdMSB() == 0 || ruleChainMetadataRequestMsg.getRuleChainIdLSB() == 0) { return Futures.immediateFuture(null); } - RuleChainId ruleChainId = - new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB())); - return saveEdgeEvent(tenantId, edge.getId(), - EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null); + RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB())); + return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 08b1e57651..863be23e42 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -35,7 +35,6 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -48,6 +47,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; @@ -107,7 +108,7 @@ public class EntityStateSourcingListener { onDeviceProfileUpdate(deviceProfile, event.getOldEntity(), isCreated); } case EDGE -> { - handleEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent); + onEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent); } case TB_RESOURCE -> { TbResource tbResource = (TbResource) event.getEntity(); @@ -221,10 +222,7 @@ public class EntityStateSourcingListener { } private DeviceProfile getOldDeviceProfile(Object oldEntity) { - if (oldEntity instanceof DeviceProfile) { - return (DeviceProfile) oldEntity; - } - return null; + return oldEntity instanceof DeviceProfile ? (DeviceProfile) oldEntity : null; } private void onDeviceProfileDelete(TenantId tenantId, EntityId entityId, DeviceProfile deviceProfile) { @@ -241,11 +239,11 @@ public class EntityStateSourcingListener { tbClusterService.onDeviceUpdated(device, oldDevice); } - private void handleEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) { + private void onEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) { if (entity instanceof Edge) { - tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); - } else if (entity instanceof EdgeEvent) { - tbClusterService.onEdgeEventUpdate(tenantId, (EdgeId) entityId); + tbClusterService.onEdgeStateChangeEvent(new ComponentLifecycleMsg(tenantId, entityId, lifecycleEvent)); + } else if (entity instanceof EdgeEvent edgeEvent) { + tbClusterService.onEdgeEventUpdate(new EdgeEventUpdateMsg(tenantId, edgeEvent.getEdgeId())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java index 9fcd8e2187..5b8fd949bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @AllArgsConstructor @@ -40,7 +39,6 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @Slf4j public class DefaultTbEdgeService extends AbstractTbEntityService implements TbEdgeService { - private final EdgeNotificationService edgeNotificationService; private final RuleChainService ruleChainService; @Override @@ -56,7 +54,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE if (ActionType.ADDED.equals(actionType)) { ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edgeId); - edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); + edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); edgeService.assignDefaultRuleChainsToEdge(tenantId, edgeId); } @@ -143,7 +141,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE TenantId tenantId = edge.getTenantId(); EdgeId edgeId = edge.getId(); try { - Edge updatedEdge = edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, ruleChainId); + Edge updatedEdge = edgeService.setEdgeRootRuleChain(tenantId, edge, ruleChainId); logEntityActionService.logEntityAction(tenantId, edgeId, edge, null, ActionType.UPDATED, user); return updatedEdge; } catch (Exception e) { @@ -152,4 +150,5 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE throw e; } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java index 8dc447b731..3afd32c3fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; public interface TbEdgeService { + Edge save(Edge edge, RuleChain edgeTemplateRootRuleChain, User user) throws Exception; void delete(Edge edge, User user); @@ -36,4 +37,5 @@ public interface TbEdgeService { Edge assignEdgeToPublicCustomer(TenantId tenantId, EdgeId edgeId, User user) throws ThingsboardException; Edge setEdgeRootRuleChain(Edge edge, RuleChainId ruleChainId, User user) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java b/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java index 676dbc1e3d..fc5ce5214c 100644 --- a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java +++ b/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java @@ -90,13 +90,13 @@ public abstract class AbstractPartitionBasedService extends @Override protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { log.debug("onTbApplicationEvent, processing event: {}", partitionChangeEvent); - subscribeQueue.add(partitionChangeEvent.getPartitions()); + subscribeQueue.add(partitionChangeEvent.getCorePartitions()); scheduledExecutor.submit(this::pollInitStateFromDB); } @Override protected boolean filterTbApplicationEvent(PartitionChangeEvent event) { - return getServiceType().equals(event.getServiceType()); + return event.getServiceType() == getServiceType(); } protected void pollInitStateFromDB() { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 7699a4f7c2..301f8e8838 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -23,6 +23,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; @@ -55,6 +56,7 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -66,14 +68,23 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ComponentLifecycleMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.EntityDeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.QueueUpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ResourceDeleteMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ResourceUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; @@ -112,6 +123,8 @@ public class DefaultTbClusterService implements TbClusterService { private final AtomicInteger toRuleEngineMsgs = new AtomicInteger(0); private final AtomicInteger toRuleEngineNfs = new AtomicInteger(0); private final AtomicInteger toTransportNfs = new AtomicInteger(0); + private final AtomicInteger toEdgeMsgs = new AtomicInteger(0); + private final AtomicInteger toEdgeNfs = new AtomicInteger(0); @Autowired @Lazy @@ -133,6 +146,7 @@ public class DefaultTbClusterService implements TbClusterService { private final TbAssetProfileCache assetProfileCache; private final GatewayNotificationsService gatewayNotificationsService; private final EdgeService edgeService; + private final TbTransactionalCache edgeIdServiceIdCache; @Override public void pushMsgToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, TbQueueCallback callback) { @@ -169,7 +183,7 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void pushMsgToVersionControl(TenantId tenantId, TransportProtos.ToVersionControlServiceMsg msg, TbQueueCallback callback) { + public void pushMsgToVersionControl(TenantId tenantId, ToVersionControlServiceMsg msg, TbQueueCallback callback) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_VC_EXECUTOR, TenantId.SYS_TENANT_ID, tenantId); log.trace("PUSHING msg: {} to:{}", msg, tpi); producerProvider.getTbVersionControlMsgProducer().send(tpi, new TbProtoQueueMsg<>(tenantId.getId(), msg), callback); @@ -386,7 +400,7 @@ public class DefaultTbClusterService implements TbClusterService { if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { TenantId tenantId = resource.getTenantId(); log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); - TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder() + ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setResourceType(resource.getResourceType().name()) @@ -401,7 +415,7 @@ public class DefaultTbClusterService implements TbClusterService { public void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback) { if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { log.trace("[{}][{}][{}] Processing delete resource", resource.getTenantId(), resource.getResourceType(), resource.getResourceKey()); - TransportProtos.ResourceDeleteMsg resourceDeleteMsg = TransportProtos.ResourceDeleteMsg.newBuilder() + ResourceDeleteMsg resourceDeleteMsg = ResourceDeleteMsg.newBuilder() .setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits()) .setResourceType(resource.getResourceType().name()) @@ -421,7 +435,7 @@ public class DefaultTbClusterService implements TbClusterService { private void broadcastEntityDeleteToTransport(TenantId tenantId, EntityId entityId, String name, TbQueueCallback callback) { log.trace("[{}][{}][{}] Processing [{}] delete event", tenantId, entityId.getEntityType(), entityId.getId(), name); - TransportProtos.EntityDeleteMsg entityDeleteMsg = TransportProtos.EntityDeleteMsg.newBuilder() + EntityDeleteMsg entityDeleteMsg = EntityDeleteMsg.newBuilder() .setEntityType(entityId.getEntityType().name()) .setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) @@ -453,39 +467,76 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { - log.trace("[{}] Processing edge {} event update ", tenantId, edgeId); - EdgeEventUpdateMsg msg = new EdgeEventUpdateMsg(tenantId, edgeId); - ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setEdgeEventUpdate(toProto(msg)).build(); - pushEdgeSyncMsgToCore(edgeId, toCoreMsg); + public void pushMsgToEdge(TenantId tenantId, EntityId entityId, ToEdgeMsg msg, TbQueueCallback callback) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, DataConstants.EDGE_QUEUE_NAME, tenantId, entityId); + TbQueueProducer> toEdgeProducer = producerProvider.getTbEdgeMsgProducer(); + toEdgeProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); + toEdgeMsgs.incrementAndGet(); } @Override - public void pushEdgeSyncRequestToCore(ToEdgeSyncRequest toEdgeSyncRequest) { - log.trace("[{}] Processing edge sync request {} ", toEdgeSyncRequest.getTenantId(), toEdgeSyncRequest); - ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToEdgeSyncRequest(toProto(toEdgeSyncRequest)).build(); - pushEdgeSyncMsgToCore(toEdgeSyncRequest.getEdgeId(), toCoreMsg); + public void onEdgeHighPriorityMsg(EdgeHighPriorityMsg msg) { + log.trace("[{}] Processing edge event for edgeId: {}", msg.getTenantId(), msg.getEdgeEvent().getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setEdgeHighPriority(toProto(msg)).build(); + processEdgeNotification(msg.getEdgeEvent().getEdgeId(), toEdgeNotificationMsg); } @Override - public void pushEdgeSyncResponseToCore(FromEdgeSyncResponse fromEdgeSyncResponse) { - log.trace("[{}] Processing edge sync response {}", fromEdgeSyncResponse.getTenantId(), fromEdgeSyncResponse); - ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setFromEdgeSyncResponse(toProto(fromEdgeSyncResponse)).build(); - pushEdgeSyncMsgToCore(fromEdgeSyncResponse.getEdgeId(), toCoreMsg); + public void onEdgeEventUpdate(EdgeEventUpdateMsg msg) { + log.trace("[{}] Processing edge event update for edgeId: {}", msg.getTenantId(), msg.getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setEdgeEventUpdate(toProto(msg)).build(); + processEdgeNotification(msg.getEdgeId(), toEdgeNotificationMsg); } - private void pushEdgeSyncMsgToCore(EdgeId edgeId, ToCoreNotificationMsg toCoreMsg) { - TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); - Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); - for (String serviceId : tbCoreServices) { - TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); - toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(edgeId.getId(), toCoreMsg), null); - toCoreNfs.incrementAndGet(); + @Override + public void onEdgeStateChangeEvent(ComponentLifecycleMsg msg) { + log.trace("[{}] Processing {} state change event: {}", msg.getTenantId(), EntityType.EDGE, msg.getEvent()); + ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setComponentLifecycle(componentLifecycleMsgProto).build(); + processEdgeNotification((EdgeId) msg.getEntityId(), toEdgeNotificationMsg); + } + + @Override + public void pushEdgeSyncRequestToEdge(ToEdgeSyncRequest request) { + log.trace("[{}] Processing edge sync request for edgeId: {}", request.getTenantId(), request.getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setToEdgeSyncRequest(toProto(request)).build(); + processEdgeNotification(request.getEdgeId(), toEdgeNotificationMsg); + } + + @Override + public void pushEdgeSyncResponseToCore(FromEdgeSyncResponse response, String requestServiceId) { + log.trace("[{}] Processing edge sync response for edgeId: {}", response.getTenantId(), response.getEdgeId()); + ToEdgeNotificationMsg toEdgeNotificationMsg = ToEdgeNotificationMsg.newBuilder().setFromEdgeSyncResponse(toProto(response)).build(); + pushMsgToEdgeNotification(toEdgeNotificationMsg, requestServiceId); + } + + private void processEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) { + var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId)); + serviceIdOpt.ifPresentOrElse( + serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()), + () -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg) + ); + } + + private void pushMsgToEdgeNotification(ToEdgeNotificationMsg toEdgeNotificationMsg, String serviceId) { + TopicPartitionInfo tpi = topicService.getEdgeNotificationsTopic(serviceId); + TbQueueProducer> toEdgeNotificationProducer = producerProvider.getTbEdgeNotificationsMsgProducer(); + toEdgeNotificationProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), toEdgeNotificationMsg), null); + toEdgeNfs.incrementAndGet(); + } + + private void broadcastEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) { + TbQueueProducer> toEdgeNotificationProducer = producerProvider.getTbEdgeNotificationsMsgProducer(); + Set serviceIds = partitionService.getAllServiceIds(ServiceType.TB_CORE); + for (String serviceId : serviceIds) { + TopicPartitionInfo tpi = topicService.getEdgeNotificationsTopic(serviceId); + toEdgeNotificationProducer.send(tpi, new TbProtoQueueMsg<>(edgeId.getId(), toEdgeNotificationMsg), null); + toEdgeNfs.incrementAndGet(); } } private void broadcast(ComponentLifecycleMsg msg) { - TransportProtos.ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg); + ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); EntityType entityType = msg.getEntityId().getEntityType(); @@ -497,7 +548,6 @@ public class DefaultTbClusterService implements TbClusterService { || entityType.equals(EntityType.API_USAGE_STATE) || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ENTITY_VIEW) - || entityType.equals(EntityType.EDGE) || entityType.equals(EntityType.NOTIFICATION_RULE)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); @@ -526,15 +576,17 @@ public class DefaultTbClusterService implements TbClusterService { int toRuleEngineMsgsCnt = toRuleEngineMsgs.getAndSet(0); int toRuleEngineNfsCnt = toRuleEngineNfs.getAndSet(0); int toTransportNfsCnt = toTransportNfs.getAndSet(0); - if (toCoreMsgCnt > 0 || toCoreNfsCnt > 0 || toRuleEngineMsgsCnt > 0 || toRuleEngineNfsCnt > 0 || toTransportNfsCnt > 0) { - log.info("To TbCore: [{}] messages [{}] notifications; To TbRuleEngine: [{}] messages [{}] notifications; To Transport: [{}] notifications", - toCoreMsgCnt, toCoreNfsCnt, toRuleEngineMsgsCnt, toRuleEngineNfsCnt, toTransportNfsCnt); + int toEdgeMsgCnt = toEdgeMsgs.getAndSet(0); + int toEdgeNfsCnt = toEdgeNfs.getAndSet(0); + if (toCoreMsgCnt > 0 || toCoreNfsCnt > 0 || toRuleEngineMsgsCnt > 0 || toRuleEngineNfsCnt > 0 || toTransportNfsCnt > 0 || toEdgeMsgCnt > 0 || toEdgeNfsCnt > 0) { + log.info("To TbCore: [{}] messages [{}] notifications; To TbRuleEngine: [{}] messages [{}] notifications; To Transport: [{}] notifications;" + + "To Edge: [{}] messages [{}] notifications", toCoreMsgCnt, toCoreNfsCnt, toRuleEngineMsgsCnt, toRuleEngineNfsCnt, toTransportNfsCnt, toEdgeMsgCnt, toEdgeNfsCnt); } } } private void sendDeviceStateServiceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) { - TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder(); + DeviceStateServiceMsgProto.Builder builder = DeviceStateServiceMsgProto.newBuilder(); builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setDeviceIdMSB(deviceId.getId().getMostSignificantBits()); @@ -542,8 +594,8 @@ public class DefaultTbClusterService implements TbClusterService { builder.setAdded(added); builder.setUpdated(updated); builder.setDeleted(deleted); - TransportProtos.DeviceStateServiceMsgProto msg = builder.build(); - pushMsgToCore(tenantId, deviceId, TransportProtos.ToCoreMsg.newBuilder().setDeviceStateServiceMsg(msg).build(), null); + DeviceStateServiceMsgProto msg = builder.build(); + pushMsgToCore(tenantId, deviceId, ToCoreMsg.newBuilder().setDeviceStateServiceMsg(msg).build(), null); } @Override @@ -581,7 +633,7 @@ public class DefaultTbClusterService implements TbClusterService { return; } } - TransportProtos.EdgeNotificationMsgProto.Builder builder = TransportProtos.EdgeNotificationMsgProto.newBuilder(); + EdgeNotificationMsgProto.Builder builder = EdgeNotificationMsgProto.newBuilder(); builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setType(type.name()); @@ -602,9 +654,9 @@ public class DefaultTbClusterService implements TbClusterService { builder.setOriginatorEdgeIdMSB(originatorEdgeId.getId().getMostSignificantBits()); builder.setOriginatorEdgeIdLSB(originatorEdgeId.getId().getLeastSignificantBits()); } - TransportProtos.EdgeNotificationMsgProto msg = builder.build(); + EdgeNotificationMsgProto msg = builder.build(); log.trace("[{}] sending notification to edge service {}", tenantId.getId(), msg); - pushMsgToCore(tenantId, entityId != null ? entityId : tenantId, TransportProtos.ToCoreMsg.newBuilder().setEdgeNotificationMsg(msg).build(), null); + pushMsgToEdge(tenantId, entityId != null ? entityId : tenantId, ToEdgeMsg.newBuilder().setEdgeNotificationMsg(msg).build(), null); if (entityId != null && EntityType.DEVICE.equals(entityId.getEntityType())) { pushDeviceUpdateMessage(tenantId, edgeId, entityId, action); @@ -690,4 +742,5 @@ public class DefaultTbClusterService implements TbClusterService { toTransportNfs.incrementAndGet(); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 4462183baa..d30f03262d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -59,7 +59,6 @@ import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ErrorEventProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.LifecycleEventProto; @@ -86,7 +85,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -142,7 +140,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService tpi.newByTopic(usageStatsConsumer.getConsumer().getTopic())) .collect(Collectors.toSet())); } private void processMsgs(List> msgs, TbQueueConsumer> consumer, CoreQueueConfig config) throws Exception { - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); @@ -282,9 +277,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService 0) { partitionService.updateQueues(toCoreNotification.getQueueUpdateMsgsList()); callback.onSuccess(); @@ -686,13 +669,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbEdgeConsumerService { + + @Value("${queue.edge.pool-interval:25}") + private int pollInterval; + @Value("${queue.edge.pack-processing-timeout:10000}") + private int packProcessingTimeout; + @Value("${queue.core.consumer-per-partition:true}") + private boolean consumerPerPartition; + @Value("${queue.edge.pack-processing-retries:3}") + private int packProcessingRetries; + @Value("${queue.edge.stats.enabled:false}") + private boolean statsEnabled; + + private final TbCoreQueueFactory queueFactory; + private final EdgeContextComponent edgeCtx; + private final EdgeConsumerStats stats; + + private MainQueueConsumerManager, EdgeQueueConfig> mainConsumer; + + public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext, + StatsFactory statsFactory, EdgeContextComponent edgeCtx) { + super(actorContext, null, null, null, null, null, + null, null); + this.edgeCtx = edgeCtx; + this.stats = new EdgeConsumerStats(statsFactory); + this.queueFactory = tbCoreQueueFactory; + } + + @PostConstruct + public void init() { + super.init("tb-edge"); + + this.mainConsumer = MainQueueConsumerManager., EdgeQueueConfig>builder() + .queueKey(new QueueKey(ServiceType.TB_CORE).withQueueName(DataConstants.EDGE_QUEUE_NAME)) + .config(EdgeQueueConfig.of(consumerPerPartition, (int) pollInterval)) + .msgPackProcessor(this::processMsgs) + .consumerCreator((config, partitionId) -> queueFactory.createEdgeMsgConsumer()) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); + } + + @PreDestroy + public void destroy() { + super.destroy(); + } + + @Override + protected void startConsumers() { + super.startConsumers(); + } + + @Override + protected void onTbApplicationEvent(PartitionChangeEvent event) { + if (ServiceType.TB_CORE.equals(event.getServiceType())) { + var partitions = event.getEdgePartitions(); + log.info("Subscribing to partitions: {}", partitions); + mainConsumer.update(partitions); + } + } + + private void processMsgs(List> msgs, TbQueueConsumer> consumer, EdgeQueueConfig edgeQueueConfig) throws InterruptedException { + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); + ConcurrentMap> pendingMap = orderedMsgList.stream().collect( + Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + PendingMsgHolder pendingMsgHolder = new PendingMsgHolder(); + Future submitFuture = consumersExecutor.submit(() -> { + orderedMsgList.forEach((element) -> { + UUID id = element.getUuid(); + TbProtoQueueMsg msg = element.getMsg(); + TbCallback callback = new TbPackCallback<>(id, ctx); + try { + ToEdgeMsg toEdgeMsg = msg.getValue(); + pendingMsgHolder.setToEdgeMsg(toEdgeMsg); + if (toEdgeMsg.hasEdgeNotificationMsg()) { + pushNotificationToEdge(toEdgeMsg.getEdgeNotificationMsg(), 0, packProcessingRetries, callback); + } + if (statsEnabled) { + stats.log(toEdgeMsg); + } + } catch (Throwable e) { + log.warn("[{}] Failed to process message: {}", id, msg, e); + callback.onFailure(e); + } + }); + }); + if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { + if (!submitFuture.isDone()) { + submitFuture.cancel(true); + ToEdgeMsg lastSubmitMsg = pendingMsgHolder.getToEdgeMsg(); + log.info("Timeout to process message: {}", lastSubmitMsg); + } + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); + } + consumer.commit(); + } + + private static class PendingMsgHolder { + @Getter + @Setter + private volatile ToEdgeMsg toEdgeMsg; + } + + @Override + protected ServiceType getServiceType() { + return ServiceType.TB_CORE; + } + + @Override + protected long getNotificationPollDuration() { + return pollInterval; + } + + @Override + protected long getNotificationPackProcessingTimeout() { + return packProcessingTimeout; + } + + @Override + protected int getMgmtThreadPoolSize() { + return Math.max(Runtime.getRuntime().availableProcessors(), 4); + } + + @Override + protected TbQueueConsumer> createNotificationsConsumer() { + return queueFactory.createToEdgeNotificationsMsgConsumer(); + } + + @Override + protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { + ToEdgeNotificationMsg toEdgeNotificationMsg = msg.getValue(); + try { + if (toEdgeNotificationMsg.hasEdgeHighPriority()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeHighPriority()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasEdgeEventUpdate()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeEventUpdate()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasToEdgeSyncRequest()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getToEdgeSyncRequest()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasFromEdgeSyncResponse()) { + EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getFromEdgeSyncResponse()); + edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + callback.onSuccess(); + } else if (toEdgeNotificationMsg.hasComponentLifecycle()) { + ComponentLifecycleMsg componentLifecycle = ProtoUtils.fromProto(toEdgeNotificationMsg.getComponentLifecycle()); + TenantId tenantId = componentLifecycle.getTenantId(); + EdgeId edgeId = new EdgeId(componentLifecycle.getEntityId().getId()); + if (ComponentLifecycleEvent.DELETED.equals(componentLifecycle.getEvent())) { + edgeCtx.getEdgeRpcService().deleteEdge(tenantId, edgeId); + } else if (ComponentLifecycleEvent.UPDATED.equals(componentLifecycle.getEvent())) { + Edge edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edgeId); + edgeCtx.getEdgeRpcService().updateEdge(tenantId, edge); + } + callback.onSuccess(); + } + } catch (Exception e) { + log.error("Error processing edge notification message: {}", e.getMessage(), e); + callback.onFailure(e); + } + + if (statsEnabled) { + stats.log(msg.getValue()); + } + } + + private void pushNotificationToEdge(EdgeNotificationMsgProto edgeNotificationMsg, int retryCount, int retryLimit, TbCallback callback) { + TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); + log.debug("[{}] Pushing notification to edge {}", tenantId, edgeNotificationMsg); + try { + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + ListenableFuture future; + switch (type) { + case EDGE -> future = edgeCtx.getEdgeProcessor().processEdgeNotification(tenantId, edgeNotificationMsg); + case ASSET -> future = edgeCtx.getAssetProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case ASSET_PROFILE -> future = edgeCtx.getAssetProfileProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE -> future = edgeCtx.getDeviceProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DEVICE_PROFILE -> future = edgeCtx.getDeviceProfileProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case ENTITY_VIEW -> future = edgeCtx.getEntityViewProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DASHBOARD -> future = edgeCtx.getDashboardProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case RULE_CHAIN -> future = edgeCtx.getRuleChainProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case USER -> future = edgeCtx.getUserProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case CUSTOMER -> future = edgeCtx.getCustomerProcessor().processCustomerNotification(tenantId, edgeNotificationMsg); + case OTA_PACKAGE -> future = edgeCtx.getOtaPackageProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGETS_BUNDLE -> future = edgeCtx.getWidgetBundleProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case WIDGET_TYPE -> future = edgeCtx.getWidgetTypeProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case QUEUE -> future = edgeCtx.getQueueProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case ALARM -> future = edgeCtx.getAlarmProcessor().processAlarmNotification(tenantId, edgeNotificationMsg); + case ALARM_COMMENT -> future = edgeCtx.getAlarmProcessor().processAlarmCommentNotification(tenantId, edgeNotificationMsg); + case RELATION -> future = edgeCtx.getRelationProcessor().processRelationNotification(tenantId, edgeNotificationMsg); + case TENANT -> future = edgeCtx.getTenantProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case TENANT_PROFILE -> future = edgeCtx.getTenantProfileProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> + future = edgeCtx.getNotificationEdgeProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case TB_RESOURCE -> future = edgeCtx.getResourceProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + case DOMAIN, OAUTH2_CLIENT -> future = edgeCtx.getOAuth2EdgeProcessor().processEntityNotification(tenantId, edgeNotificationMsg); + default -> { + future = Futures.immediateFuture(null); + log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); + } + } + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void unused) { + callback.onSuccess(); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (retryCount < retryLimit) { + log.warn("[{}] Retry {} for message due to failure: {}", tenantId, retryCount + 1, throwable.getMessage()); + pushNotificationToEdge(edgeNotificationMsg, retryCount + 1, retryLimit, callback); + } else { + callBackFailure(tenantId, edgeNotificationMsg, callback, throwable); + } + } + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + if (retryCount < retryLimit) { + log.warn("[{}] Retry {} for message due to exception: {}", tenantId, retryCount + 1, e.getMessage()); + pushNotificationToEdge(edgeNotificationMsg, retryCount + 1, retryLimit, callback); + } else { + callBackFailure(tenantId, edgeNotificationMsg, callback, e); + } + } + } + + private void callBackFailure(TenantId tenantId, EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) { + log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable); + callback.onFailure(throwable); + } + + @Scheduled(fixedDelayString = "${queue.edge.stats.print-interval-ms}") + public void printStats() { + if (statsEnabled) { + stats.printStats(); + stats.reset(); + } + } + + @Override + protected void stopConsumers() { + super.stopConsumers(); + mainConsumer.stop(); + mainConsumer.awaitStop(); + } + + @Data(staticConstructor = "of") + public static class EdgeQueueConfig implements QueueConfig { + private final boolean consumerPerPartition; + private final int pollInterval; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index d36d44e276..c9a6dee588 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -165,7 +165,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } @Override - protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception { + protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToRuleEngineNotificationMsg nfMsg = msg.getValue(); if (nfMsg.hasComponentLifecycle()) { handleComponentLifecycleMsg(id, ProtoUtils.fromProto(nfMsg.getComponentLifecycle())); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java new file mode 100644 index 0000000000..be2d060f03 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/EdgeConsumerStats.java @@ -0,0 +1,99 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.stats.StatsCounter; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class EdgeConsumerStats { + + public static final String TOTAL_MSGS = "totalMsgs"; + public static final String EDGE_NOTIFICATIONS = "edgeNfs"; + public static final String TO_CORE_NF_EDGE_EVENT = "coreNfEdgeHPUpd"; + public static final String TO_CORE_NF_EDGE_EVENT_UPDATE = "coreNfEdgeUpd"; + public static final String TO_CORE_NF_EDGE_SYNC_REQUEST = "coreNfEdgeSyncReq"; + public static final String TO_CORE_NF_EDGE_SYNC_RESPONSE = "coreNfEdgeSyncResp"; + public static final String TO_CORE_NF_EDGE_COMPONENT_LIFECYCLE = "coreNfEdgeCompLfcl"; + + private final StatsCounter totalCounter; + private final StatsCounter edgeNotificationsCounter; + private final StatsCounter edgeHighPriorityCounter; + private final StatsCounter edgeEventUpdateCounter; + private final StatsCounter edgeSyncRequestCounter; + private final StatsCounter edgeSyncResponseCounter; + private final StatsCounter edgeComponentLifecycle; + + private final List counters = new ArrayList<>(7); + + public EdgeConsumerStats(StatsFactory statsFactory) { + String statsKey = StatsType.EDGE.getName(); + + this.totalCounter = register(statsFactory.createStatsCounter(statsKey, TOTAL_MSGS)); + this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); + this.edgeHighPriorityCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_EVENT)); + this.edgeEventUpdateCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_EVENT_UPDATE)); + this.edgeSyncRequestCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_REQUEST)); + this.edgeSyncResponseCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_RESPONSE)); + this.edgeComponentLifecycle = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_COMPONENT_LIFECYCLE)); + } + + private StatsCounter register(StatsCounter counter) { + counters.add(counter); + return counter; + } + + public void log(ToEdgeNotificationMsg msg) { + totalCounter.increment(); + if (msg.hasEdgeHighPriority()) { + edgeHighPriorityCounter.increment(); + } else if (msg.hasEdgeEventUpdate()) { + edgeEventUpdateCounter.increment(); + } else if (msg.hasToEdgeSyncRequest()) { + edgeSyncRequestCounter.increment(); + } else if (msg.hasFromEdgeSyncResponse()) { + edgeSyncResponseCounter.increment(); + } else if (msg.hasComponentLifecycle()) { + edgeComponentLifecycle.increment(); + } + } + + public void log(ToEdgeMsg msg) { + totalCounter.increment(); + edgeNotificationsCounter.increment(); + } + + public void printStats() { + int total = totalCounter.get(); + if (total > 0) { + StringBuilder stats = new StringBuilder(); + counters.forEach(counter -> stats.append(counter.getName()).append(" = [").append(counter.get()).append("] ")); + log.info("Edge Stats: {}", stats); + } + } + + public void reset() { + counters.forEach(StatsCounter::clear); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java index 1bd1cb2d5e..aede1263eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java @@ -36,7 +36,6 @@ public class TbCoreConsumerStats { public static final String DEVICE_CLAIMS = "claimDevice"; public static final String DEVICE_STATES = "deviceState"; public static final String SUBSCRIPTION_MSGS = "subMsgs"; - public static final String EDGE_NOTIFICATIONS = "edgeNfs"; public static final String DEVICE_CONNECTS = "deviceConnect"; public static final String DEVICE_ACTIVITIES = "deviceActivity"; public static final String DEVICE_DISCONNECTS = "deviceDisconnect"; @@ -45,9 +44,6 @@ public class TbCoreConsumerStats { public static final String TO_CORE_NF_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl"; public static final String TO_CORE_NF_DEVICE_RPC_RESPONSE = "coreNfDevRpcRsp"; - public static final String TO_CORE_NF_EDGE_EVENT_UPDATE = "coreNfEdgeUpd"; - public static final String TO_CORE_NF_EDGE_SYNC_REQUEST = "coreNfEdgeSyncReq"; - public static final String TO_CORE_NF_EDGE_SYNC_RESPONSE = "coreNfEdgeSyncResp"; public static final String TO_CORE_NF_NOTIFICATION_RULE_PROCESSOR = "coreNfNfRlProc"; public static final String TO_CORE_NF_QUEUE_UPDATE = "coreNfQueueUpd"; public static final String TO_CORE_NF_QUEUE_DELETE = "coreNfQueueDel"; @@ -65,7 +61,6 @@ public class TbCoreConsumerStats { private final StatsCounter claimDeviceCounter; private final StatsCounter deviceStateCounter; private final StatsCounter subscriptionMsgCounter; - private final StatsCounter edgeNotificationsCounter; private final StatsCounter deviceConnectsCounter; private final StatsCounter deviceActivitiesCounter; private final StatsCounter deviceDisconnectsCounter; @@ -74,9 +69,6 @@ public class TbCoreConsumerStats { private final StatsCounter toCoreNfOtherCounter; private final StatsCounter toCoreNfComponentLifecycleCounter; private final StatsCounter toCoreNfDeviceRpcResponseCounter; - private final StatsCounter toCoreNfEdgeEventUpdateCounter; - private final StatsCounter toCoreNfEdgeSyncRequestCounter; - private final StatsCounter toCoreNfEdgeSyncResponseCounter; private final StatsCounter toCoreNfNotificationRuleProcessorCounter; private final StatsCounter toCoreNfQueueUpdateCounter; private final StatsCounter toCoreNfQueueDeleteCounter; @@ -84,7 +76,7 @@ public class TbCoreConsumerStats { private final StatsCounter toCoreNfSubscriptionManagerCounter; private final StatsCounter toCoreNfVersionControlResponseCounter; - private final List counters = new ArrayList<>(24); + private final List counters = new ArrayList<>(23); public TbCoreConsumerStats(StatsFactory statsFactory) { String statsKey = StatsType.CORE.getName(); @@ -99,7 +91,6 @@ public class TbCoreConsumerStats { this.claimDeviceCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_CLAIMS)); this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES)); this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS)); - this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); this.deviceConnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_CONNECTS)); this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES)); this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS)); @@ -109,9 +100,6 @@ public class TbCoreConsumerStats { this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER)); this.toCoreNfComponentLifecycleCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_COMPONENT_LIFECYCLE)); this.toCoreNfDeviceRpcResponseCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_DEVICE_RPC_RESPONSE)); - this.toCoreNfEdgeEventUpdateCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_EVENT_UPDATE)); - this.toCoreNfEdgeSyncRequestCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_REQUEST)); - this.toCoreNfEdgeSyncResponseCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_EDGE_SYNC_RESPONSE)); this.toCoreNfNotificationRuleProcessorCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_NOTIFICATION_RULE_PROCESSOR)); this.toCoreNfQueueUpdateCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_QUEUE_UPDATE)); this.toCoreNfQueueDeleteCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_QUEUE_DELETE)); @@ -155,11 +143,6 @@ public class TbCoreConsumerStats { deviceStateCounter.increment(); } - public void log(TransportProtos.EdgeNotificationMsgProto msg) { - totalCounter.increment(); - edgeNotificationsCounter.increment(); - } - public void log(TransportProtos.DeviceConnectProto msg) { totalCounter.increment(); deviceConnectsCounter.increment(); @@ -193,12 +176,6 @@ public class TbCoreConsumerStats { toCoreNfDeviceRpcResponseCounter.increment(); } else if (msg.hasComponentLifecycle()) { toCoreNfComponentLifecycleCounter.increment(); - } else if (msg.hasEdgeEventUpdate()) { - toCoreNfEdgeEventUpdateCounter.increment(); - } else if (msg.hasToEdgeSyncRequest()) { - toCoreNfEdgeSyncRequestCounter.increment(); - } else if (msg.hasFromEdgeSyncResponse()) { - toCoreNfEdgeSyncResponseCounter.increment(); } else if (msg.getQueueUpdateMsgsCount() > 0) { toCoreNfQueueUpdateCounter.increment(); } else if (msg.getQueueDeleteMsgsCount() > 0) { @@ -218,9 +195,8 @@ public class TbCoreConsumerStats { int total = totalCounter.get(); if (total > 0) { StringBuilder stats = new StringBuilder(); - counters.forEach(counter -> { - stats.append(counter.getName()).append(" = [").append(counter.get()).append("] "); - }); + counters.forEach(counter -> + stats.append(counter.getName()).append(" = [").append(counter.get()).append("] ")); log.info("Core Stats: {}", stats); } } @@ -228,4 +204,5 @@ public class TbCoreConsumerStats { public void reset() { counters.forEach(StatsCounter::clear); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java new file mode 100644 index 0000000000..6eaac782e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbEdgeConsumerService.java @@ -0,0 +1,23 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue; + +import org.springframework.context.ApplicationListener; +import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; + +public interface TbEdgeConsumerService extends ApplicationListener { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index a264f912cb..1b7e5d53a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -124,7 +124,7 @@ public abstract class AbstractConsumerService> createNotificationsConsumer(); protected void processNotifications(List> msgs, TbQueueConsumer> consumer) throws Exception { - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); CountDownLatch processingTimeoutLatch = new CountDownLatch(1); @@ -211,4 +211,5 @@ public abstract class AbstractConsumerService { softly.assertThat(name).as("cache name").isNotEmpty(); - softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0); + if (name.equals("edgeSessions")) { + softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isEqualTo(0); + } else { + softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0); + } softly.assertThat(cacheSpecs.getMaxSize()).as("cache %s max size", name).isGreaterThan(0); }); softly.assertAll(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index d549407c44..238203cdb0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.model.ModelConstants; @@ -230,6 +231,19 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.reset(tbClusterService, auditLogService); } + protected void testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(HasName entity, EntityId entityId, EntityId originatorId, + TenantId tenantId, CustomerId customerId, UserId userId, String userName, + ActionType actionType, int cntTimeBroadcast, Object... additionalInfo) { + int cntTime = 1; + testNotificationMsgToEdgeServiceNeverWithActionType(entityId, actionType); + testLogEntityAction(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); + ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); + testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime); + testBroadcastEdgeStateChangeEventTime(tenantId, cntTimeBroadcast); + Mockito.reset(tbClusterService, auditLogService); + } + + protected void testNotifyEntityBroadcastEntityStateChangeEventMany(HasName entity, HasName originator, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, @@ -351,11 +365,17 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { protected void testBroadcastEntityStateChangeEventTime(EntityId entityId, TenantId tenantId, int cntTime) { ArgumentMatcher matcherTenantIdId = cntTime > 1 || tenantId == null ? argument -> argument.getClass().equals(TenantId.class) : - argument -> argument.equals(tenantId) ; + argument -> argument.equals(tenantId); Mockito.verify(tbClusterService, times(cntTime)).broadcastEntityStateChangeEvent(Mockito.argThat(matcherTenantIdId), Mockito.any(entityId.getClass()), Mockito.any(ComponentLifecycleEvent.class)); } + protected void testBroadcastEdgeStateChangeEventTime(TenantId tenantId, int cntTime) { + ArgumentMatcher matcherTenantIdId = cntTime > 1 || tenantId == null ? argument -> argument.getClass().equals(TenantId.class) : + argument -> argument.equals(tenantId); + Mockito.verify(tbClusterService, times(cntTime)).onEdgeStateChangeEvent(Mockito.any(ComponentLifecycleMsg.class)); + } + private void testPushMsgToCoreTime(int cntTime) { Mockito.verify(tbClusterService, times(cntTime)).pushMsgToCore(Mockito.any(ToDeviceActorNotificationMsg.class), Mockito.isNull()); } @@ -608,8 +628,8 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { } private String entityClassToEntityTypeName(HasName entity) { - String entityType = entityClassToString(entity); - return "SAVE_OTA_PACKAGE_INFO_REQUEST".equals(entityType) || "OTA_PACKAGE_INFO".equals(entityType)? + String entityType = entityClassToString(entity); + return "SAVE_OTA_PACKAGE_INFO_REQUEST".equals(entityType) || "OTA_PACKAGE_INFO".equals(entityType) ? EntityType.OTA_PACKAGE.name().toUpperCase(Locale.ENGLISH) : entityType; } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index b636849774..4d38934dcb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -40,8 +40,8 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUpgradeInfo; @@ -62,6 +62,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -166,7 +167,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); Assert.assertEquals(edge.getName(), savedEdge.getName()); - testNotifyEntityBroadcastEntityStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.ADDED, 2); @@ -176,7 +177,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); - testNotifyEntityBroadcastEntityStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), ActionType.UPDATED, 1); } @@ -876,7 +877,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Asset asset = new Asset(); asset.setName("Test Sync Edge Asset 1"); - asset.setType("test"); + asset.setType("default"); Asset savedAsset = doPost("/api/asset", asset, Asset.class); Device device = new Device(); @@ -896,7 +897,7 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); - edgeImitator.expectMessageAmount(27); + edgeImitator.expectMessageAmount(26); edgeImitator.connect(); waitForMessages(edgeImitator); @@ -905,16 +906,16 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); - Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); - Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); + printQueueMsgsIfNotEmpty(edgeImitator); - edgeImitator.expectMessageAmount(22); - doPost("/api/edge/sync/" + edge.getId()); + edgeImitator.expectMessageAmount(21); + doPost("/api/edge/sync/" + edge.getId()).andExpect(status().isOk()); waitForMessages(edgeImitator); verifyFetchersMsgs(edgeImitator, savedDevice); - Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); + printQueueMsgsIfNotEmpty(edgeImitator); edgeImitator.allowIgnoredTypes(); try { @@ -930,6 +931,15 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + private static void printQueueMsgsIfNotEmpty(EdgeImitator edgeImitator) { + if (!edgeImitator.getDownlinkMsgs().isEmpty()) { + for (AbstractMessage downlinkMsg : edgeImitator.getDownlinkMsgs()) { + log.warn("Unexpected message in the queue: {}", downlinkMsg); + } + } + Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); + } + private RuleChainId getEdgeRootRuleChainId(EdgeImitator edgeImitator) { try { EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB())); @@ -948,11 +958,18 @@ public class EdgeControllerTest extends AbstractControllerTest { } private void simulateEdgeActivation(Edge edge) throws Exception { + Awaitility.await() + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + List ruleChains = getEdgeRuleChains(edge.getId()); + return ruleChains.size() == 1 && ruleChains.get(0).getId().equals(edge.getRootRuleChainId()); + }); + ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); - doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes); + doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + AttributeScope.SERVER_SCOPE, attributes); Awaitility.await() - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); @@ -960,8 +977,9 @@ public class EdgeControllerTest extends AbstractControllerTest { if (activeAttrOpt.isEmpty()) { return false; } + List ruleChains = getEdgeRuleChains(edge.getId()); Map activeAttr = activeAttrOpt.get(); - return "true".equals(activeAttr.get("value").toString()); + return "true".equals(activeAttr.get("value").toString()) && ruleChains.size() == 1; }); } @@ -988,13 +1006,12 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); - Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); Assert.assertTrue(popUserMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, TENANT_ADMIN_EMAIL, Authority.TENANT_ADMIN)); Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); - Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); + Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId)); Assert.assertTrue(popTenantProfileMsg(edgeImitator.getDownlinkMsgs(), tenantProfileId)); @@ -1223,6 +1240,12 @@ public class EdgeControllerTest extends AbstractControllerTest { return doPost("/api/edge", edge, Edge.class); } + private List getEdgeRuleChains(EdgeId edgeId) throws Exception { + return doGetTypedWithTimePageLink("/api/edge/" + edgeId + "/ruleChains?", + new TypeReference>() { + }, new TimePageLink(10)).getData(); + } + @Test public void testGetEdgeInstallInstructions() throws Exception { Edge edge = constructEdge(tenantId, "Edge for Test Docker Install Instructions", "default", "7390c3a6-69b0-9910-d155-b90aca4b772e", "l7q4zsjplzwhk16geqxy"); @@ -1271,6 +1294,8 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.1.5"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); + edgeUpgradeInstructionsService.setAppVersion("3.6.1.6-SNAPSHOT"); + Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.2"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); @@ -1281,6 +1306,8 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.2"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); + edgeUpgradeInstructionsService.setAppVersion("3.6.2-SNAPSHOT"); + Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); edgeUpgradeInstructionsService.setAppVersion("3.6.2.6"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 17de555bdc..5d04b16dfa 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -629,7 +629,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { Awaitility.await() .alias("data by query") - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {}); var loadedEntities = new ArrayList<>(data.getData()); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index e1beda28d8..c8cc72c5b5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -664,7 +664,7 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); return !tpi.getTenantId().get().isSysTenantId(); @@ -678,7 +678,7 @@ public class TenantControllerTest extends AbstractControllerTest { tenantProfile.setIsolatedTbRuleEngine(false); tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList()); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId) .getTenantId().get().isSysTenantId()); @@ -690,7 +690,7 @@ public class TenantControllerTest extends AbstractControllerTest { submittedMsgs.add(tbMsg.getId()); Thread.sleep(timeLeft / msgs); } - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic)); }); @@ -719,13 +719,13 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNotNull(); }); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId); assertThat(tpi.getTenantId()).hasValue(tenantId); TbMsg tbMsg = publishTbMsg(tenantId, tpi); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); })); @@ -733,7 +733,7 @@ public class TenantControllerTest extends AbstractControllerTest { deleteDifferentTenant(); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNull(); assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId)) .isInstanceOf(TenantNotFoundException.class); @@ -753,7 +753,7 @@ public class TenantControllerTest extends AbstractControllerTest { } private void verifyUsedQueueAndMessage(String queue, TenantId tenantId, EntityId entityId, String msgType, Runnable action, Consumer tpiAssert) { - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, queue, tenantId, entityId); tpiAssert.accept(tpi); diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index bd8a59a975..b3a1dd790e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edge; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; @@ -27,7 +28,6 @@ import org.junit.Before; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -127,9 +127,6 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { @Autowired protected EdgeEventService edgeEventService; - @Autowired - protected TbClusterService clusterService; - @Before public void setupEdgeTest() throws Exception { loginSysAdmin(); @@ -207,7 +204,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { + "/asset/" + savedAsset.getUuidId(), Asset.class); // wait until assign device and asset events are fully processed by edge notification service - TimeUnit.MILLISECONDS.sleep(500); + TimeUnit.MILLISECONDS.sleep(1000); } protected void extendDeviceProfileData(DeviceProfile deviceProfile) { @@ -697,4 +694,25 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { .andExpect(status().isOk()); } + + protected ObjectNode createDefaultRpc() { + return createDefaultRpc(1); + } + + protected ObjectNode createDefaultRpc(Integer value) { + ObjectNode rpc = JacksonUtil.newObjectNode(); + rpc.put("method", "setGpio"); + + ObjectNode params = JacksonUtil.newObjectNode(); + + params.put("pin", 7); + params.put("value", value); + + rpc.set("params", params); + rpc.put("persistent", true); + rpc.put("timeout", 5000); + + return rpc; + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 071d6b1391..387011717a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -51,7 +51,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -79,9 +78,13 @@ import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; @TestPropertySource(properties = { "transport.mqtt.enabled=true" @@ -107,7 +110,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(savedDevice.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getIdMSB()); Assert.assertEquals(savedDevice.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getIdLSB()); @@ -184,7 +187,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(savedDevice.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getIdMSB()); Assert.assertEquals(savedDevice.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getIdLSB()); @@ -399,7 +402,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder(); - deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + deviceDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE); deviceDeleteMsgBuilder.setIdMSB(savedDevice.getId().getId().getMostSignificantBits()); deviceDeleteMsgBuilder.setIdLSB(savedDevice.getId().getId().getLeastSignificantBits()); testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder); @@ -624,17 +627,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { if (keyValueProto.getKey().equals(expectedKey)) { Assert.assertEquals(expectedKey, keyValueProto.getKey()); switch (keyValueProto.getType()) { - case STRING_V: - Assert.assertEquals(expectedValue, keyValueProto.getStringV()); - break; - case LONG_V: - Assert.assertEquals(Long.parseLong(expectedValue), keyValueProto.getLongV()); - break; - case JSON_V: - Assert.assertEquals(JacksonUtil.toJsonNode(expectedValue), JacksonUtil.toJsonNode(keyValueProto.getJsonV())); - break; - default: - Assert.fail("Unexpected data type: " + keyValueProto.getType()); + case STRING_V -> Assert.assertEquals(expectedValue, keyValueProto.getStringV()); + case LONG_V -> Assert.assertEquals(Long.parseLong(expectedValue), keyValueProto.getLongV()); + case JSON_V -> Assert.assertEquals(JacksonUtil.toJsonNode(expectedValue), JacksonUtil.toJsonNode(keyValueProto.getJsonV())); + default -> Assert.fail("Unexpected data type: " + keyValueProto.getType()); } found = true; } @@ -768,9 +764,6 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Assert.assertEquals(1, relatedEdgeIds.size()); Assert.assertEquals(tmpEdge.getId(), relatedEdgeIds.get(0)); - // clean up stored edge events - edgeEventService.cleanupEvents(1); - // edge is disconnected: perform rpc call - no edge event saved doPostAsync( "/api/rpc/oneway/" + device.getId().getId().toString(), @@ -779,10 +772,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { status().isOk()); Awaitility.await() .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - PageData result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1)); - return result.getTotalElements() == 0; - }); + .untilAsserted(() -> verify(tbClusterService, never()).onEdgeHighPriorityMsg(any())); // edge is connected: perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly simulateEdgeActivation(tmpEdge); @@ -792,21 +782,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest { String.class, status().isOk()); - final AtomicReference> resultRef = new AtomicReference<>(); Awaitility.await() .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - PageData result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1)); - resultRef.set(result); - return result != null && result.getData().size() == 1; - }); - - PageData result = resultRef.get(); - EdgeEvent edgeEvent = result.getData().get(0); - Assert.assertEquals(EdgeEventActionType.RPC_CALL, edgeEvent.getAction()); - Assert.assertEquals(EdgeEventType.DEVICE, edgeEvent.getType()); - Assert.assertEquals(tmpEdge.getId(), edgeEvent.getEdgeId()); - Assert.assertEquals(device.getId().getId(), edgeEvent.getEntityId()); + .untilAsserted(() -> verify(tbClusterService, times(1)).onEdgeHighPriorityMsg(any())); // clean up tmp edge doDelete("/api/edge/" + tmpEdge.getId().getId().toString()).andExpect(status().isOk()); @@ -824,27 +802,12 @@ public class DeviceEdgeTest extends AbstractEdgeTest { private DeviceCredentials buildDeviceCredentialsForUplinkMsg(DeviceId deviceId) { DeviceCredentials deviceCredentials = new DeviceCredentials(); deviceCredentials.setDeviceId(deviceId); + deviceCredentials.setCredentialsId(String.valueOf(UUID.randomUUID())); deviceCredentials.setCredentialsValue("NEW_TOKEN"); deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); return deviceCredentials; } - private ObjectNode createDefaultRpc() { - ObjectNode rpc = JacksonUtil.newObjectNode(); - rpc.put("method", "setGpio"); - - ObjectNode params = JacksonUtil.newObjectNode(); - - params.put("pin", 7); - params.put("value", 1); - - rpc.set("params", params); - rpc.put("persistent", true); - rpc.put("timeout", 5000); - - return rpc; - } - private void simulateEdgeActivation(Edge edge) throws Exception { ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); diff --git a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java index b77e0b5ccc..2afe17f935 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java @@ -33,33 +33,46 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.edge.v1.RpcRequestMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.List; import java.util.concurrent.TimeUnit; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @DaoSqlTest public class TelemetryEdgeTest extends AbstractEdgeTest { @Test public void testTimeseriesWithFailures() throws Exception { int numberOfTimeseriesToSend = 333; + int numberOfHighPriorityRpcRequests = 13; + int frequencyOfRpcRequestsPerTimeseries = 25; Device device = findDeviceByName("Edge Device 1"); edgeImitator.setRandomFailuresOnTimeseriesDownlink(true); // imitator will generate failure in 5% of cases edgeImitator.setFailureProbability(5.0); - edgeImitator.expectMessageAmount(numberOfTimeseriesToSend); + edgeImitator.expectMessageAmount(numberOfTimeseriesToSend + numberOfHighPriorityRpcRequests); for (int idx = 1; idx <= numberOfTimeseriesToSend; idx++) { String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}"; JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); edgeEventService.saveAsync(edgeEvent).get(); + if (idx % frequencyOfRpcRequestsPerTimeseries == 0) { + doPostAsync( + "/api/rpc/oneway/" + device.getId().getId().toString(), + JacksonUtil.toString(createDefaultRpc(idx)), + String.class, + status().isOk()); + } } Assert.assertTrue(edgeImitator.waitForMessages(120)); @@ -71,6 +84,14 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { Assert.assertTrue(isIdxExistsInTheDownlinkList(idx, allTelemetryMsgs)); } + List allDeviceRpcCallMsgs = edgeImitator.findAllMessagesByType(DeviceRpcCallMsg.class); + Assert.assertEquals(numberOfHighPriorityRpcRequests, allDeviceRpcCallMsgs.size()); + + for (int idx = 1; idx <= numberOfHighPriorityRpcRequests; idx++) { + int pinValue = idx * frequencyOfRpcRequestsPerTimeseries; + Assert.assertTrue(isPinValueExistsInTheRpcDownlinkList(pinValue, allDeviceRpcCallMsgs)); + } + edgeImitator.setRandomFailuresOnTimeseriesDownlink(false); } @@ -175,6 +196,16 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { return false; } + private boolean isPinValueExistsInTheRpcDownlinkList(int idx, List rpcDownlinkMsgs) { + for (DeviceRpcCallMsg proto : rpcDownlinkMsgs) { + RpcRequestMsg rpcRequestMsg = proto.getRequestMsg(); + if ((JacksonUtil.toJsonNode(rpcRequestMsg.getParams())).get("value").intValue() == idx) { + return true; + } + } + return false; + } + @Test public void testTimeseriesDeliveryFailuresForever_deliverOnlyDeviceUpdateMsgs() throws Exception { int numberOfMsgsToSend = 100; diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index fc9eeaa36e..1c3b5ccb7a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -46,10 +45,9 @@ public class UserEdgeTest extends AbstractEdgeTest { private BCryptPasswordEncoder passwordEncoder; @Test - @Ignore("Ignored until fix") public void testCreateUpdateDeleteTenantUser() throws Exception { // create user - edgeImitator.expectMessageAmount(4); + edgeImitator.expectMessageAmount(6); User newTenantAdmin = new User(); newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); newTenantAdmin.setTenantId(tenantId); @@ -57,7 +55,9 @@ public class UserEdgeTest extends AbstractEdgeTest { newTenantAdmin.setFirstName("Boris"); newTenantAdmin.setLastName("Johnson"); User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 4 messages - x2 user update msg and x2 user credentials update msgs + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); @@ -133,7 +133,7 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForMessages()); // create user - edgeImitator.expectMessageAmount(4); + edgeImitator.expectMessageAmount(6); User customerUser = new User(); customerUser.setAuthority(Authority.CUSTOMER_USER); customerUser.setTenantId(tenantId); @@ -142,7 +142,9 @@ public class UserEdgeTest extends AbstractEdgeTest { customerUser.setFirstName("John"); customerUser.setLastName("Edwards"); User savedCustomerUser = createUser(customerUser, "customer"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 4 messages - x2 user update msg and x2 user credentials update msgs + Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); @@ -253,4 +255,5 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } + } diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index c468387c55..52cbaa4add 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -429,6 +429,8 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(partitionService, "vcTopic", "tb.vc"); ReflectionTestUtils.setField(partitionService, "vcPartitions", 10); ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName); + ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge"); + ReflectionTestUtils.setField(partitionService, "edgePartitions", 10); partitionService.init(); partitionService.partitionsInit(); return partitionService; diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 9981b7935d..53ae5f36ae 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -904,7 +904,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { String expectedSubject = "Comment on 'test' alarm"; String expectedBody = TENANT_ADMIN_EMAIL + " added comment: text"; ArgumentCaptor> msgCaptor = ArgumentCaptor.forClass(Map.class); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), eq(TEST_MOBILE_TOKEN), eq(expectedSubject), eq(expectedBody), @@ -959,7 +959,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { } private NotificationRequestStats awaitNotificationRequest(NotificationRequestId requestId) { - return await().atMost(30, TimeUnit.SECONDS) + return await().atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> getStats(requestId), Objects::nonNull); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index 9cf73894c2..852ddd757c 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -780,7 +780,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(getMyNotifications(false, 100)).size().isZero(); createDevice("Device 1", "default", "111"); - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { assertThat(getMyNotifications(false, 100)).size().isEqualTo(1); }); @@ -798,7 +798,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRulesCache.evict(tenantId); createDevice("Device 3", "default", "333"); - await().atMost(30, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { assertThat(getMyNotifications(false, 100)).size().isEqualTo(2); }); diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 59cca779f5..25fe589a08 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -25,6 +25,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -98,6 +100,8 @@ public class DefaultTbClusterServiceTest { protected TbQueueProducerProvider producerProvider; @MockBean protected TbRuleEngineProducerService ruleEngineProducerService; + @MockBean + protected TbTransactionalCache edgeCache; @SpyBean protected TopicService topicService; @@ -400,4 +404,5 @@ public class DefaultTbClusterServiceTest { ((DefaultTbClusterService) clusterService).getRuleEngineProfileForEntityOrElseNull(tenantId, assetId, tbMsg); verify(assetProfileCache, times(1)).get(tenantId, assetProfileId); } -} \ No newline at end of file + +} diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index 76f8ff654e..9e3b6221b1 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -69,14 +69,14 @@ class TbelInvokeServiceTest extends AbstractControllerTest { for (int i = 0; i < 100; i++) { msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); Assert.assertEquals(expected, result); } long startTs = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); Assert.assertEquals(expected, result); } long duration = System.currentTimeMillis() - startTs; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 4f24c58916..b78e738d72 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -95,7 +95,7 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED); List ts = await("await on timeseries") - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/values/timeseries?orderBy=ASC&keys=fw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() { @@ -127,7 +127,7 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); List ts = await("await on timeseries") - .atMost(30, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> getSwStateTelemetryFromAPI(device.getId().getId()), this::predicateForStatuses); log.warn("Object9: Got the ts: {}", ts); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index 3531c3846e..fbf88bb5e4 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -72,9 +72,10 @@ public abstract class RedisTbTransactionalCache x.get(cacheName)) + .map(specs -> specs.get(cacheName)) .map(CacheSpecs::getTimeToLiveInMinutes) - .map(t -> Expiration.from(t, TimeUnit.MINUTES)) + .filter(ttl -> !ttl.equals(0)) + .map(ttl -> Expiration.from(ttl, TimeUnit.MINUTES)) .orElseGet(Expiration::persistent); this.cacheEnabled = Optional.ofNullable(cacheSpecsMap) .map(CacheSpecsMap::getSpecs) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index fe47a7a02d..be3a264720 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -75,8 +75,10 @@ public class TbCaffeineCacheConfiguration { = Caffeine.newBuilder() .weigher(collectionSafeWeigher()) .maximumWeight(cacheSpec.getMaxSize()) - .expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES) .ticker(ticker()); + if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { + caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); + } return new CaffeineCache(name, caffeineBuilder.build()); } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 399a27f07e..5112e93da7 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -29,13 +29,17 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; @@ -52,7 +56,7 @@ public interface TbClusterService extends TbQueueClusterService { void pushMsgToCore(ToDeviceActorNotificationMsg msg, TbQueueCallback callback); - void broadcastToCore(TransportProtos.ToCoreNotificationMsg msg); + void broadcastToCore(ToCoreNotificationMsg msg); void pushMsgToVersionControl(TenantId tenantId, ToVersionControlServiceMsg msg, TbQueueCallback callback); @@ -96,11 +100,17 @@ public interface TbClusterService extends TbQueueClusterService { void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback); - void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId); + void onEdgeHighPriorityMsg(EdgeHighPriorityMsg msg); - void pushEdgeSyncRequestToCore(ToEdgeSyncRequest toEdgeSyncRequest); + void onEdgeEventUpdate(EdgeEventUpdateMsg msg); - void pushEdgeSyncResponseToCore(FromEdgeSyncResponse fromEdgeSyncResponse); + void onEdgeStateChangeEvent(ComponentLifecycleMsg msg); + + void pushEdgeSyncRequestToEdge(ToEdgeSyncRequest request); + + void pushEdgeSyncResponseToCore(FromEdgeSyncResponse response, String requestServiceId); + + void pushMsgToEdge(TenantId tenantId, EntityId entityId, ToEdgeMsg msg, TbQueueCallback callback); void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId sourceEdgeId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java index 278232d2f3..c18f42ace7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; @@ -97,6 +98,8 @@ public interface EdgeService extends EntityDaoService { String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName); + Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws Exception; + ListenableFuture isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String activityState); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index adf626a8d0..54a5bc755d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -25,6 +25,7 @@ public class CacheConstants { public static final String USER_CACHE = "users"; public static final String ENTITY_VIEW_CACHE = "entityViews"; public static final String EDGE_CACHE = "edges"; + public static final String EDGE_SESSIONS_CACHE = "edgeSessions"; public static final String RELATED_EDGES_CACHE = "relatedEdges"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 9641b5e69f..aef3751f0c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -140,4 +140,6 @@ public class DataConstants { public static final String SQ_QUEUE_TOPIC = "tb_rule_engine.sq"; public static final String QUEUE_NAME = "queueName"; + public static final String EDGE_QUEUE_NAME = "Edge"; + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 68f2db88b2..b05bb75032 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.msg; import lombok.Getter; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -36,7 +37,7 @@ public enum MsgType { /** * ADDED/UPDATED/DELETED events for main entities. * - * See {@link org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg} + * See {@link ComponentLifecycleMsg} */ COMPONENT_LIFE_CYCLE_MSG, @@ -126,6 +127,7 @@ public enum MsgType { * Message that is sent on Edge Event to Edge Session */ EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG, + EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG, /** * Messages that are sent to and from edge session to start edge synchronization process @@ -143,4 +145,5 @@ public enum MsgType { MsgType(boolean ignoreOnStart) { this.ignoreOnStart = ignoreOnStart; } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java index 3fd70f90c7..892b2a57dd 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java @@ -32,4 +32,5 @@ public class EdgeEventUpdateMsg implements EdgeSessionMsg { public MsgType getMsgType() { return MsgType.EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java similarity index 55% rename from application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java rename to common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java index 4f90023f68..03f039f2e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeHighPriorityMsg.java @@ -13,17 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge; +package org.thingsboard.server.common.msg.edge; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.id.RuleChainId; +import lombok.Data; +import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.common.msg.MsgType; -public interface EdgeNotificationService { +@Data +public class EdgeHighPriorityMsg implements EdgeSessionMsg { - Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws Exception; + private final TenantId tenantId; + private final EdgeEvent edgeEvent; + + @Override + public MsgType getMsgType() { + return MsgType.EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG; + } - void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback); } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java index 58e31234fc..4e658d6dd4 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeSessionMsg.java @@ -15,8 +15,13 @@ */ package org.thingsboard.server.common.msg.edge; -import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; + +public interface EdgeSessionMsg { + + TenantId getTenantId(); + + MsgType getMsgType(); -public interface EdgeSessionMsg extends TenantAwareMsg, ToAllNodesMsg { } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java index baf483310c..1e8beea443 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java @@ -37,4 +37,5 @@ public class FromEdgeSyncResponse implements EdgeSessionMsg { public MsgType getMsgType() { return MsgType.EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG; } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java index 6c9ac545b4..04516b58c5 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/ToEdgeSyncRequest.java @@ -30,9 +30,11 @@ public class ToEdgeSyncRequest implements EdgeSessionMsg { private final UUID id; private final TenantId tenantId; private final EdgeId edgeId; + private final String serviceId; @Override public MsgType getMsgType() { return MsgType.EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG; } + } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 5646a37a68..b61dc16384 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.util; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; @@ -36,6 +38,8 @@ import org.thingsboard.server.common.data.device.data.CoapDeviceTransportConfigu import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -68,6 +72,7 @@ import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -140,6 +145,7 @@ public class ProtoUtils { .setRequestIdLSB(request.getId().getLeastSignificantBits()) .setEdgeIdMSB(request.getEdgeId().getId().getMostSignificantBits()) .setEdgeIdLSB(request.getEdgeId().getId().getLeastSignificantBits()) + .setServiceId(request.getServiceId()) .build(); } @@ -147,7 +153,8 @@ public class ProtoUtils { return new ToEdgeSyncRequest( new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB()), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), - new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())) + EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), + proto.getServiceId() ); } @@ -168,12 +175,56 @@ public class ProtoUtils { return new FromEdgeSyncResponse( new UUID(proto.getResponseIdMSB(), proto.getResponseIdLSB()), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), - new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), + EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), proto.getSuccess(), proto.getError() ); } + public static TransportProtos.EdgeHighPriorityMsgProto toProto(EdgeHighPriorityMsg msg) { + TransportProtos.EdgeHighPriorityMsgProto.Builder builder = TransportProtos.EdgeHighPriorityMsgProto.newBuilder() + .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(msg.getTenantId().getId().getLeastSignificantBits()) + .setType(msg.getEdgeEvent().getType().name()) + .setAction(msg.getEdgeEvent().getAction().name()); + + if (msg.getEdgeEvent().getEntityId() != null) { + builder.setEntityIdMSB(msg.getEdgeEvent().getEntityId().getMostSignificantBits()); + builder.setEntityIdLSB(msg.getEdgeEvent().getEntityId().getLeastSignificantBits()); + } + if (msg.getEdgeEvent().getEdgeId() != null) { + builder.setEdgeIdMSB(msg.getEdgeEvent().getEdgeId().getId().getMostSignificantBits()); + builder.setEdgeIdLSB(msg.getEdgeEvent().getEdgeId().getId().getLeastSignificantBits()); + } + if (msg.getEdgeEvent().getBody() != null) { + builder.setBody(JacksonUtil.toString(msg.getEdgeEvent().getBody())); + } + + return builder.build(); + } + + public static EdgeHighPriorityMsg fromProto(TransportProtos.EdgeHighPriorityMsgProto proto) { + EdgeEventType type = EdgeEventType.valueOf(proto.getType()); + EdgeEventActionType actionType = EdgeEventActionType.valueOf(proto.getAction()); + JsonNode body = proto.hasBody() ? JacksonUtil.toJsonNode(proto.getBody()) : null; + + EdgeId edgeId = null; + if (proto.hasEdgeIdMSB() && proto.hasEdgeIdLSB()) { + edgeId = EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())); + } + + EntityId entityId = null; + if (proto.hasEntityIdMSB() && proto.hasEntityIdLSB()) { + entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); + } + + return new EdgeHighPriorityMsg( + TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), + EdgeUtils.constructEdgeEvent(TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), + edgeId, type, actionType, entityId, body) + ); + } + public static TransportProtos.EdgeEventUpdateMsgProto toProto(EdgeEventUpdateMsg msg) { return TransportProtos.EdgeEventUpdateMsgProto.newBuilder() .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) @@ -186,7 +237,7 @@ public class ProtoUtils { public static EdgeEventUpdateMsg fromProto(TransportProtos.EdgeEventUpdateMsgProto proto) { return new EdgeEventUpdateMsg( TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), - new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())) + EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())) ); } @@ -208,7 +259,7 @@ public class ProtoUtils { private static DeviceEdgeUpdateMsg fromProto(TransportProtos.DeviceEdgeUpdateMsgProto proto) { EdgeId edgeId = null; if (proto.hasEdgeIdMSB() && proto.hasEdgeIdLSB()) { - edgeId = new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())); + edgeId = EdgeId.fromUUID(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())); } return new DeviceEdgeUpdateMsg( TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 74f1d0560f..ac29ff4d0b 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -146,9 +146,9 @@ message KeyValueProto { } enum AttributeScopeProto { - CLIENT_SCOPE = 0; - SERVER_SCOPE = 1; - SHARED_SCOPE = 2; + CLIENT_SCOPE = 0; + SERVER_SCOPE = 1; + SHARED_SCOPE = 2; } message AttributeKey { @@ -421,7 +421,7 @@ message GatewayDisconnectDeviceMsg { } enum TransportApiRequestErrorCode { - UNKNOWN_TRANSPORT_API_ERROR = 0; + UNKNOWN_TRANSPORT_API_ERROR = 0; ENTITY_LIMIT = 1; } @@ -1140,6 +1140,18 @@ message EdgeNotificationMsgProto { int64 originatorEdgeIdLSB = 14; } +message EdgeHighPriorityMsgProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + optional int64 edgeIdMSB = 3; + optional int64 edgeIdLSB = 4; + string type = 5; + string action = 6; + optional string body = 7; + optional int64 entityIdMSB = 8; + optional int64 entityIdLSB = 9; +} + message EdgeEventUpdateMsgProto { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; @@ -1154,6 +1166,7 @@ message ToEdgeSyncRequestMsgProto { int64 requestIdLSB = 4; int64 edgeIdMSB = 5; int64 edgeIdLSB = 6; + string serviceId = 7; } message FromEdgeSyncResponseMsgProto { @@ -1475,7 +1488,7 @@ message ToCoreMsg { DeviceStateServiceMsgProto deviceStateServiceMsg = 2; SubscriptionMgrMsgProto toSubscriptionMgrMsg = 3; bytes toDeviceActorNotificationMsg = 4 [deprecated = true]; - EdgeNotificationMsgProto edgeNotificationMsg = 5; + EdgeNotificationMsgProto edgeNotificationMsg = 5 [deprecated = true]; DeviceActivityProto deviceActivityMsg = 6; NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7; LifecycleEventProto lifecycleEventMsg = 8; @@ -1498,13 +1511,27 @@ message ToCoreNotificationMsg { NotificationRuleProcessorMsg notificationRuleProcessorMsg = 7; ComponentLifecycleMsgProto componentLifecycle = 8; CoreStartupMsg coreStartupMsg = 9; - EdgeEventUpdateMsgProto edgeEventUpdate = 10; - ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 11; - FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 12; + EdgeEventUpdateMsgProto edgeEventUpdate = 10 [deprecated = true]; + ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 11 [deprecated = true]; + FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 12 [deprecated = true]; ResourceCacheInvalidateMsg resourceCacheInvalidateMsg = 13; RestApiCallResponseMsgProto restApiCallResponseMsg = 50; } + +/* Messages to Edge queue that are handled by ThingsBoard Core Service */ +message ToEdgeMsg { + EdgeNotificationMsgProto edgeNotificationMsg = 1; +} + +message ToEdgeNotificationMsg { + EdgeHighPriorityMsgProto edgeHighPriority = 1; + EdgeEventUpdateMsgProto edgeEventUpdate = 2; + ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 3; + FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 4; + ComponentLifecycleMsgProto componentLifecycle = 5; +} + /* Messages that are handled by ThingsBoard RuleEngine Service */ message ToRuleEngineMsg { int64 tenantIdMSB = 1; diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index a513d3e311..b253b4e444 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; @@ -34,6 +35,8 @@ import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConf import org.thingsboard.server.common.data.device.data.DeviceConfiguration; import org.thingsboard.server.common.data.device.data.DeviceTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; @@ -54,6 +57,7 @@ import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; +import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -110,6 +114,13 @@ class ProtoUtilsTest { } } + @Test + void protoEdgeHighPrioritySerialization() { + EdgeHighPriorityMsg msg = new EdgeHighPriorityMsg(tenantId, EdgeUtils.constructEdgeEvent(tenantId, edgeId, + EdgeEventType.DEVICE, EdgeEventActionType.ADDED, deviceId, JacksonUtil.newObjectNode())); + assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg); + } + @Test void protoEdgeEventUpdateSerialization() { EdgeEventUpdateMsg msg = new EdgeEventUpdateMsg(tenantId, edgeId); @@ -118,7 +129,7 @@ class ProtoUtilsTest { @Test void protoToEdgeSyncRequestSerialization() { - ToEdgeSyncRequest msg = new ToEdgeSyncRequest(id, tenantId, edgeId); + ToEdgeSyncRequest msg = new ToEdgeSyncRequest(id, tenantId, edgeId, "serviceId"); assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java index 45d39d1cd6..80da7846ea 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java @@ -27,6 +27,7 @@ import java.util.Map; @Component @ConditionalOnExpression("'${queue.type:null}'=='service-bus'") public class TbServiceBusQueueConfigs { + @Value("${queue.service-bus.queue-properties.core:}") private String coreProperties; @Value("${queue.service-bus.queue-properties.rule-engine:}") @@ -39,6 +40,9 @@ public class TbServiceBusQueueConfigs { private String jsExecutorProperties; @Value("${queue.service-bus.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.service-bus.queue-properties.edge:}") + private String edgeProperties; + @Getter private Map coreConfigs; @Getter @@ -51,6 +55,8 @@ public class TbServiceBusQueueConfigs { private Map jsExecutorConfigs; @Getter private Map vcConfigs; + @Getter + private Map edgeConfigs; @PostConstruct private void init() { @@ -60,6 +66,7 @@ public class TbServiceBusQueueConfigs { notificationsConfigs = PropertyUtils.getProps(notificationsProperties); jsExecutorConfigs = PropertyUtils.getProps(jsExecutorProperties); vcConfigs = PropertyUtils.getProps(vcProperties); + edgeConfigs = PropertyUtils.getProps(edgeProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 4caa3b92b3..1ceb52b3d6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -88,7 +88,7 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { assignedTenantProfiles = Collections.emptySet(); } - generateNewServiceInfoWithCurrentSystemInfo(); + generateNewServiceInfoWithCurrentSystemInfo(); } @AfterContextReady diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 53a2152782..37e519e3f2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -51,6 +51,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.DataConstants.EDGE_QUEUE_NAME; import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; @Service @@ -59,12 +60,16 @@ public class HashPartitionService implements PartitionService { @Value("${queue.core.topic}") private String coreTopic; - @Value("${queue.core.partitions:100}") + @Value("${queue.core.partitions:10}") private Integer corePartitions; @Value("${queue.vc.topic:tb_version_control}") private String vcTopic; @Value("${queue.vc.partitions:10}") private Integer vcPartitions; + @Value("${queue.edge.topic:tb_edge}") + private String edgeTopic; + @Value("${queue.edge.partitions:10}") + private Integer edgePartitions; @Value("${queue.partitions.hash_function_name:murmur3_128}") private String hashFunctionName; @@ -114,6 +119,10 @@ public class HashPartitionService implements PartitionService { if (!isTransport(serviceInfoProvider.getServiceType())) { doInitRuleEnginePartitions(); } + + QueueKey edgeKey = coreKey.withQueueName(EDGE_QUEUE_NAME); + partitionSizesMap.put(edgeKey, edgePartitions); + partitionTopicsMap.put(edgeKey, edgeTopic); } @AfterStartUp(order = AfterStartUp.QUEUE_INFO_INITIALIZATION) @@ -195,8 +204,7 @@ public class HashPartitionService implements PartitionService { .map(queueDeleteMsg -> { TenantId tenantId = TenantId.fromUUID(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); return new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); - }) - .collect(Collectors.toList()); + }).toList(); queueKeys.forEach(queueKey -> { removeQueue(queueKey); evictTenantInfo(queueKey.getTenantId()); @@ -210,8 +218,7 @@ public class HashPartitionService implements PartitionService { @Override public void removeTenant(TenantId tenantId) { List queueKeys = partitionSizesMap.keySet().stream() - .filter(queueKey -> tenantId.equals(queueKey.getTenantId())) - .collect(Collectors.toList()); + .filter(queueKey -> tenantId.equals(queueKey.getTenantId())).toList(); queueKeys.forEach(this::removeQueue); evictTenantInfo(tenantId); } @@ -546,12 +553,10 @@ public class HashPartitionService implements PartitionService { if (routingInfo == null) { throw new TenantNotFoundException(tenantId); } - switch (serviceType) { - case TB_RULE_ENGINE: - return routingInfo.isIsolated(); - default: - return false; + if (serviceType == ServiceType.TB_RULE_ENGINE) { + return routingInfo.isIsolated(); } + return false; } private TenantRoutingInfo getRoutingInfo(TenantId tenantId) { @@ -588,7 +593,10 @@ public class HashPartitionService implements PartitionService { responsibleServices.computeIfAbsent(profileId, k -> new ArrayList<>()).add(instance); } } - } else if (ServiceType.TB_CORE.equals(serviceType) || ServiceType.TB_VC_EXECUTOR.equals(serviceType)) { + } else if (ServiceType.TB_CORE.equals(serviceType)) { + queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); + queueServiceList.computeIfAbsent(new QueueKey(serviceType).withQueueName(EDGE_QUEUE_NAME), key -> new ArrayList<>()).add(instance); + } else if (ServiceType.TB_VC_EXECUTOR.equals(serviceType)) { queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); } } @@ -649,16 +657,12 @@ public class HashPartitionService implements PartitionService { } public static HashFunction forName(String name) { - switch (name) { - case "murmur3_32": - return Hashing.murmur3_32(); - case "murmur3_128": - return Hashing.murmur3_128(); - case "sha256": - return Hashing.sha256(); - default: - throw new IllegalArgumentException("Can't find hash function with name " + name); - } + return switch (name) { + case "murmur3_32" -> Hashing.murmur3_32(); + case "murmur3_128" -> Hashing.murmur3_128(); + case "sha256" -> Hashing.sha256(); + default -> throw new IllegalArgumentException("Can't find hash function with name " + name); + }; } private List toServiceIds(Collection serviceInfos) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java index f0ae5af43c..b991e4614c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.discovery; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.With; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; public class QueueKey { private final ServiceType type; + @With private final String queueName; private final TenantId tenantId; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 960be231c6..d8a409e704 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -30,8 +30,9 @@ public class TopicService { @Value("${queue.prefix:}") private String prefix; - private Map tbCoreNotificationTopics = new HashMap<>(); - private Map tbRuleEngineNotificationTopics = new HashMap<>(); + private final Map tbCoreNotificationTopics = new HashMap<>(); + private final Map tbRuleEngineNotificationTopics = new HashMap<>(); + private final Map tbEdgeNotificationTopics = new HashMap<>(); /** * Each Service should start a consumer for messages that target individual service instance based on serviceId. @@ -41,16 +42,21 @@ public class TopicService { * @return */ public TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId) { - switch (serviceType) { - case TB_CORE: - return tbCoreNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - case TB_RULE_ENGINE: - return tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - default: - return buildNotificationsTopicPartitionInfo(serviceType, serviceId); - } + return switch (serviceType) { + case TB_CORE -> tbCoreNotificationTopics.computeIfAbsent(serviceId, + id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + case TB_RULE_ENGINE -> tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, + id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + default -> buildNotificationsTopicPartitionInfo(serviceType, serviceId); + }; + } + + public TopicPartitionInfo getEdgeNotificationsTopic(String serviceId) { + return tbEdgeNotificationTopics.computeIfAbsent(serviceId, id -> buildEdgeNotificationsTopicPartitionInfo(serviceId)); + } + + private TopicPartitionInfo buildEdgeNotificationsTopicPartitionInfo(String serviceId) { + return buildTopicPartitionInfo("tb_edge.notifications." + serviceId, null, null, false); } private TopicPartitionInfo buildNotificationsTopicPartitionInfo(ServiceType serviceType, String serviceId) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index b5ad181a00..88ceb4aa08 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -17,17 +17,20 @@ package org.thingsboard.server.queue.discovery.event; import lombok.Getter; import lombok.ToString; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.discovery.QueueKey; -import java.util.Collections; +import java.io.Serial; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; @ToString(callSuper = true) public class PartitionChangeEvent extends TbApplicationEvent { + @Serial private static final long serialVersionUID = -8731788167026510559L; @Getter @@ -41,9 +44,19 @@ public class PartitionChangeEvent extends TbApplicationEvent { this.partitionsMap = partitionsMap; } - // only for service types that have single QueueKey - public Set getPartitions() { - return partitionsMap.values().stream().findAny().orElse(Collections.emptySet()); + public Set getCorePartitions() { + return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_CORE, DataConstants.MAIN_QUEUE_NAME); } + public Set getEdgePartitions() { + return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_CORE, DataConstants.EDGE_QUEUE_NAME); + } + + private Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { + return partitionsMap.entrySet() + .stream() + .filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName())) + .flatMap(entry -> entry.getValue().stream()) + .collect(Collectors.toSet()); + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index 6f237894c2..300548e081 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -27,6 +27,7 @@ import java.util.Map; @Component @ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") public class TbKafkaTopicConfigs { + public static final String NUM_PARTITIONS_SETTING = "partitions"; @Value("${queue.kafka.topic-properties.core:}") @@ -43,6 +44,8 @@ public class TbKafkaTopicConfigs { private String fwUpdatesProperties; @Value("${queue.kafka.topic-properties.version-control:}") private String vcProperties; + @Value("${queue.kafka.topic-properties.edge:}") + private String edgeProperties; @Value("${queue.kafka.topic-properties.housekeeper:}") private String housekeeperProperties; @Value("${queue.kafka.topic-properties.housekeeper-reprocessing:}") @@ -70,6 +73,8 @@ public class TbKafkaTopicConfigs { private Map housekeeperConfigs; @Getter private Map housekeeperReprocessingConfigs; + @Getter + private Map edgeConfigs; @PostConstruct private void init() { @@ -86,6 +91,7 @@ public class TbKafkaTopicConfigs { vcConfigs = PropertyUtils.getProps(vcProperties); housekeeperConfigs = PropertyUtils.getProps(housekeeperProperties); housekeeperReprocessingConfigs = PropertyUtils.getProps(housekeeperReprocessingProperties); + edgeConfigs = PropertyUtils.getProps(edgeProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index d40a8bac85..005e9aee17 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -24,15 +24,17 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -45,6 +47,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -70,6 +73,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbAwsSqsSettings sqsSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueAdmin coreAdmin; @@ -79,6 +83,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin otaAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public AwsSqsMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -87,6 +92,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng TbQueueTransportNotificationSettings transportNotificationSettings, TbAwsSqsSettings sqsSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, TbQueueRemoteJsInvokeSettings jsInvokeSettings) { this.topicService = topicService; @@ -97,6 +103,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.transportNotificationSettings = transportNotificationSettings; this.sqsSettings = sqsSettings; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.jsInvokeSettings = jsInvokeSettings; this.coreAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getCoreAttributes()); @@ -106,6 +113,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.notificationAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getNotificationsAttributes()); this.otaAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getOtaAttributes()); this.vcAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getVcAttributes()); + this.edgeAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getEdgeAttributes()); } @Override @@ -130,13 +138,14 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -223,11 +232,10 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbAwsSqsProducerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic())); } - @Override public TbQueueProducer> createHousekeeperMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @@ -249,6 +257,30 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbAwsSqsProducerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -272,5 +304,8 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index 05e435d7fb..515c1bba44 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -22,15 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -43,6 +45,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -69,6 +72,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,6 +81,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin otaAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public AwsSqsTbCoreQueueFactory(TbAwsSqsSettings sqsSettings, TbQueueCoreSettings coreSettings, @@ -84,6 +89,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueRuleEngineSettings ruleEngineSettings, TopicService topicService, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, @@ -92,6 +98,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { this.coreSettings = coreSettings; this.transportApiSettings = transportApiSettings; this.ruleEngineSettings = ruleEngineSettings; + this.edgeSettings = edgeSettings; this.topicService = topicService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; @@ -105,6 +112,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { this.notificationAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getNotificationsAttributes()); this.otaAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getOtaAttributes()); this.vcAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getVcAttributes()); + this.edgeAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getEdgeAttributes()); } @Override @@ -129,7 +137,8 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -202,7 +211,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbAwsSqsProducerTemplate<>(vcAdmin, sqsSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -228,6 +237,30 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbAwsSqsProducerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -251,5 +284,8 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index 7e6129a748..37319e378b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -25,8 +25,14 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -37,6 +43,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -59,12 +66,14 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final TbAwsSqsSettings sqsSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin otaAdmin; + private final TbQueueAdmin edgeAdmin; public AwsSqsTbRuleEngineQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -72,7 +81,8 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory TbAwsSqsSettings sqsSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, TbQueueRemoteJsInvokeSettings jsInvokeSettings, - TbQueueTransportNotificationSettings transportNotificationSettings) { + TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings) { this.topicService = topicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; @@ -80,12 +90,14 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory this.sqsSettings = sqsSettings; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getCoreAttributes()); this.ruleEngineAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getRuleEngineAttributes()); this.jsExecutorAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getJsExecutorAttributes()); this.notificationAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getNotificationsAttributes()); this.otaAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getOtaAttributes()); + this.edgeAdmin = new TbAwsSqsAdmin(sqsSettings, sqsQueueAttributes.getEdgeAttributes()); } @Override @@ -99,7 +111,7 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { + public TbQueueProducer> createRuleEngineNotificationsMsgProducer() { return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(ruleEngineSettings.getTopic())); } @@ -109,10 +121,20 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createTbCoreNotificationsMsgProducer() { + public TbQueueProducer> createTbCoreNotificationsMsgProducer() { return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbAwsSqsProducerTemplate<>(edgeAdmin, sqsSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbAwsSqsConsumerTemplate<>(ruleEngineAdmin, sqsSettings, topicService.buildTopicName(configuration.getTopic()), @@ -120,10 +142,10 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { + public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, topicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override @@ -150,17 +172,17 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory } @Override - public TbQueueProducer> createToUsageStatsServiceMsgProducer() { + public TbQueueProducer> createToUsageStatsServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getUsageStatsTopic())); } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { return new TbAwsSqsProducerTemplate<>(otaAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java index b14bb8d8e7..67ff0393bd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbVersionControlQueueFactory.java @@ -41,7 +41,6 @@ public class AwsSqsTbVersionControlQueueFactory implements TbVersionControlQueue private final TbQueueVersionControlSettings vcSettings; private final TopicService topicService; - private final TbQueueAdmin coreAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index 225290ed63..c4281bc390 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -34,6 +34,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; import org.thingsboard.server.queue.memory.InMemoryTbQueueProducer; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -51,6 +52,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueVersionControlSettings vcSettings; private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final InMemoryStorage storage; public InMemoryMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, @@ -59,6 +61,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE TbServiceInfoProvider serviceInfoProvider, TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, InMemoryStorage storage) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -67,6 +70,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.ruleEngineSettings = ruleEngineSettings; this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.storage = storage; } @@ -92,7 +96,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(coreSettings.getTopic())); + return new InMemoryTbQueueProducer<>(storage, topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -180,10 +184,29 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new InMemoryTbQueueConsumer<>(storage, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new InMemoryTbQueueProducer<>(storage, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") private void printInMemoryStats() { storage.printStats(); } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 87be03c8c7..46d94f094f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -23,15 +23,17 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -50,6 +52,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -72,6 +75,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueAdmin coreAdmin; @@ -85,6 +89,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin vcAdmin; private final TbQueueAdmin housekeeperAdmin; private final TbQueueAdmin housekeeperReprocessingAdmin; + private final TbQueueAdmin edgeAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -96,6 +101,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbQueueTransportNotificationSettings transportNotificationSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; @@ -108,6 +114,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.jsInvokeSettings = jsInvokeSettings; this.vcSettings = vcSettings; this.consumerStatsService = consumerStatsService; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -120,6 +127,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); + this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); } @Override @@ -167,19 +175,19 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-core-notifications-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getTopic())); + requestBuilder.defaultTopic(topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { - TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + public TbQueueConsumer> createToVersionControlMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(vcSettings.getTopic())); consumerBuilder.clientId("monolith-vc-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId(topicService.buildTopicName("monolith-vc-node")); - consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(vcAdmin); consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); @@ -353,8 +361,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi } @Override - public TbQueueProducer> createVersionControlMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createVersionControlMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("monolith-vc-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(vcSettings.getTopic())); @@ -408,6 +416,52 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .build(); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.buildTopicName(edgeSettings.getTopic())); + consumerBuilder.clientId("monolith-edge-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-consumer")); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(edgeAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("monolith-to-edge-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.buildTopicName(edgeSettings.getTopic())); + requestBuilder.admin(edgeAdmin); + return requestBuilder.build(); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.clientId("monolith-edge-notifications-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-notifications-consumer-" + serviceInfoProvider.getServiceId())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("monolith-to-edge-notifications-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + requestBuilder.admin(notificationAdmin); + return requestBuilder.build(); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -437,5 +491,8 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index f16f21ad66..b8b51706b2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -24,6 +24,8 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -49,6 +51,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -72,6 +75,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueVersionControlSettings vcSettings; private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -84,6 +88,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin vcAdmin; private final TbQueueAdmin housekeeperAdmin; private final TbQueueAdmin housekeeperReprocessingAdmin; + private final TbQueueAdmin edgeAdmin; private final AtomicLong consumerCount = new AtomicLong(); @@ -95,6 +100,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueTransportApiSettings transportApiSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { @@ -108,6 +114,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.vcSettings = vcSettings; this.consumerStatsService = consumerStatsService; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -120,6 +127,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.vcAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getVcConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); this.housekeeperReprocessingAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperReprocessingConfigs()); + this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); } @Override @@ -167,7 +175,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-core-to-core-notifications-" + serviceInfoProvider.getServiceId()); - requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getTopic())); + requestBuilder.defaultTopic(topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); requestBuilder.admin(notificationAdmin); return requestBuilder.build(); } @@ -357,6 +365,51 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { .build(); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.buildTopicName(edgeSettings.getTopic())); + consumerBuilder.clientId("tb-core-edge-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-consumer")); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(edgeAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-core-to-edge-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.buildTopicName(edgeSettings.getTopic())); + requestBuilder.admin(edgeAdmin); + return requestBuilder.build(); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.clientId("tb-edge-notifications-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId(topicService.buildTopicName("tb-edge-notifications-node-" + serviceInfoProvider.getServiceId())); + consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-core-to-edge-notifications-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + requestBuilder.admin(notificationAdmin); + return requestBuilder.build(); + } @PreDestroy private void destroy() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 0b320ba617..485830caf4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -26,7 +26,9 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -47,6 +49,7 @@ import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -66,6 +69,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbKafkaAdmin ruleEngineAdmin; @@ -74,6 +78,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin fwUpdatesAdmin; private final TbQueueAdmin housekeeperAdmin; + private final TbQueueAdmin edgeAdmin; private final AtomicLong consumerCount = new AtomicLong(); public KafkaTbRuleEngineQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -83,6 +88,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; @@ -92,6 +98,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.jsInvokeSettings = jsInvokeSettings; this.consumerStatsService = consumerStatsService; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -100,6 +107,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.notificationAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getNotificationsConfigs()); this.fwUpdatesAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getFwUpdatesConfigs()); this.housekeeperAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getHousekeeperConfigs()); + this.edgeAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeConfigs()); } @Override @@ -143,8 +151,8 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { - TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); requestBuilder.settings(kafkaSettings); requestBuilder.clientId("tb-rule-engine-ota-producer-" + serviceInfoProvider.getServiceId()); requestBuilder.defaultTopic(topicService.buildTopicName(coreSettings.getOtaPackageTopic())); @@ -162,6 +170,26 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { return requestBuilder.build(); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-rule-engine-to-edge-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.buildTopicName(edgeSettings.getTopic())); + requestBuilder.admin(edgeAdmin); + return requestBuilder.build(); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("tb-rule-engine-to-edge-notifications-" + serviceInfoProvider.getServiceId()); + requestBuilder.defaultTopic(topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + requestBuilder.admin(notificationAdmin); + return requestBuilder.build(); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { throw new UnsupportedOperationException("Rule engine consumer should use a partitionId"); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 00f4a1ec4e..3df48e8fb8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -24,15 +24,17 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -50,6 +52,7 @@ import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubSettings; import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -71,6 +74,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -78,6 +82,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public PubSubMonolithQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, @@ -88,7 +93,8 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng TbServiceInfoProvider serviceInfoProvider, TbPubSubSubscriptionSettings pubSubSubscriptionSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, - TbQueueVersionControlSettings vcSettings) { + TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; @@ -97,6 +103,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.topicService = topicService; this.serviceInfoProvider = serviceInfoProvider; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); @@ -104,6 +111,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.transportApiAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getTransportApiSettings()); this.notificationAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getNotificationsSettings()); this.vcAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getVcSettings()); + this.edgeAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getEdgeSettings()); this.jsInvokeSettings = jsInvokeSettings; } @@ -135,9 +143,9 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbPubSubConsumerTemplate<>(vcAdmin, pubSubSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -224,7 +232,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbPubSubProducerTemplate<>(vcAdmin, pubSubSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -250,6 +258,30 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbPubSubProducerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbPubSubConsumerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -270,5 +302,8 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 091909d8f2..919d895a97 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -22,15 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -48,6 +50,7 @@ import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubSettings; import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -67,12 +70,14 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueRuleEngineSettings ruleEngineSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin ruleEngineAdmin; + private final TbQueueAdmin edgeAdmin; public PubSubTbCoreQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, @@ -82,6 +87,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, TbQueueRuleEngineSettings ruleEngineSettings, + TbQueueEdgeSettings edgeSettings, TbPubSubSubscriptionSettings pubSubSubscriptionSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; @@ -91,12 +97,14 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; this.ruleEngineSettings = ruleEngineSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.jsExecutorAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getJsExecutorSettings()); this.transportApiAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getTransportApiSettings()); this.notificationAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getNotificationsSettings()); this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); + this.edgeAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getEdgeSettings()); } @Override @@ -121,7 +129,8 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -194,7 +203,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { //TODO: version-control return null; } @@ -221,6 +230,30 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbPubSubConsumerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbPubSubProducerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -238,5 +271,8 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { if (ruleEngineAdmin != null) { ruleEngineAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index 3e607d0bf9..bb46610de5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -23,8 +23,10 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -46,6 +48,7 @@ import org.thingsboard.server.queue.pubsub.TbPubSubProducerTemplate; import org.thingsboard.server.queue.pubsub.TbPubSubSettings; import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -63,11 +66,13 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public PubSubTbRuleEngineQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, @@ -76,7 +81,8 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, - TbPubSubSubscriptionSettings pubSubSubscriptionSettings) { + TbPubSubSubscriptionSettings pubSubSubscriptionSettings, + TbQueueEdgeSettings edgeSettings) { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; @@ -84,11 +90,13 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); this.ruleEngineAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); this.jsExecutorAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getJsExecutorSettings()); this.notificationAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getNotificationsSettings()); + this.edgeAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getEdgeSettings()); } @Override @@ -116,6 +124,16 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbPubSubProducerTemplate<>(edgeAdmin, pubSubSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, topicService.buildTopicName(configuration.getTopic()), diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index cf60012449..8ad400d9e3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -24,15 +24,17 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -50,6 +52,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -71,6 +74,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbRabbitMqSettings rabbitMqSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -78,6 +82,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public RabbitMqMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -87,7 +92,8 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE TbRabbitMqSettings rabbitMqSettings, TbRabbitMqQueueArguments queueArguments, TbQueueRemoteJsInvokeSettings jsInvokeSettings, - TbQueueVersionControlSettings vcSettings) { + TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings) { this.topicService = topicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; @@ -97,6 +103,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.rabbitMqSettings = rabbitMqSettings; this.jsInvokeSettings = jsInvokeSettings; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); @@ -104,6 +111,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE this.transportApiAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getTransportApiArgs()); this.notificationAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getNotificationsArgs()); this.vcAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getVcArgs()); + this.edgeAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getEdgeArgs()); } @Override @@ -128,13 +136,14 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(vcAdmin, rabbitMqSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -221,7 +230,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbRabbitMqProducerTemplate<>(vcAdmin, rabbitMqSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -247,6 +256,30 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbRabbitMqProducerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -267,5 +300,8 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index 57440422be..16f9838384 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -22,14 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -47,6 +50,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -66,12 +70,14 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public RabbitMqTbCoreQueueFactory(TbRabbitMqSettings rabbitMqSettings, TbQueueCoreSettings coreSettings, @@ -81,6 +87,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbRabbitMqQueueArguments queueArguments) { this.rabbitMqSettings = rabbitMqSettings; this.coreSettings = coreSettings; @@ -90,12 +97,14 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); this.jsExecutorAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getJsExecutorArgs()); this.transportApiAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getTransportApiArgs()); this.notificationAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getNotificationsArgs()); + this.edgeAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getEdgeArgs()); } @Override @@ -120,7 +129,8 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -136,6 +146,30 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbRabbitMqProducerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createTransportApiRequestConsumer() { return new TbRabbitMqConsumerTemplate<>(transportApiAdmin, rabbitMqSettings, topicService.buildTopicName(transportApiSettings.getRequestsTopic()), @@ -171,7 +205,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { //TODO: version-control return null; } @@ -199,25 +233,25 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } @Override - public TbQueueConsumer> createHousekeeperMsgConsumer() { + public TbQueueConsumer> createHousekeeperMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @Override - public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { + public TbQueueProducer> createHousekeeperReprocessingMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic())); } @Override - public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { + public TbQueueConsumer> createHousekeeperReprocessingMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperReprocessingTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } @PreDestroy @@ -237,5 +271,8 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index a0a87714ca..3dabaf100b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -26,6 +26,9 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -45,6 +48,7 @@ import org.thingsboard.server.queue.rabbitmq.TbRabbitMqProducerTemplate; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -62,11 +66,13 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor private final TbRabbitMqSettings rabbitMqSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public RabbitMqTbRuleEngineQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -74,6 +80,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor TbRabbitMqSettings rabbitMqSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbRabbitMqQueueArguments queueArguments) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -82,11 +89,13 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor this.rabbitMqSettings = rabbitMqSettings; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getCoreArgs()); this.ruleEngineAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getRuleEngineArgs()); this.jsExecutorAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getJsExecutorArgs()); this.notificationAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getNotificationsArgs()); + this.edgeAdmin = new TbRabbitMqAdmin(rabbitMqSettings, queueArguments.getEdgeArgs()); } @Override @@ -114,6 +123,16 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbRabbitMqProducerTemplate<>(edgeAdmin, rabbitMqSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbRabbitMqProducerTemplate<>(notificationAdmin, rabbitMqSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbRabbitMqConsumerTemplate<>(ruleEngineAdmin, rabbitMqSettings, topicService.buildTopicName(configuration.getTopic()), @@ -156,12 +175,12 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java index 9b8a3d3c49..ea922557bd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTransportQueueFactory.java @@ -19,9 +19,9 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; @@ -132,7 +132,7 @@ public class RabbitMqTransportQueueFactory implements TbTransportQueueFactory { } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbRabbitMqProducerTemplate<>(coreAdmin, rabbitMqSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 2ec807498c..fa7e73b879 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -23,15 +23,17 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -49,6 +51,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -70,6 +73,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul private final TbServiceBusSettings serviceBusSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueVersionControlSettings vcSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -77,6 +81,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin vcAdmin; + private final TbQueueAdmin edgeAdmin; public ServiceBusMonolithQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -86,6 +91,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul TbServiceBusSettings serviceBusSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueVersionControlSettings vcSettings, + TbQueueEdgeSettings edgeSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -96,6 +102,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul this.serviceBusSettings = serviceBusSettings; this.jsInvokeSettings = jsInvokeSettings; this.vcSettings = vcSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); @@ -103,6 +110,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul this.transportApiAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getTransportApiConfigs()); this.notificationAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getNotificationsConfigs()); this.vcAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getVcConfigs()); + this.edgeAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getEdgeConfigs()); } @Override @@ -127,13 +135,14 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override - public TbQueueConsumer> createToVersionControlMsgConsumer() { + public TbQueueConsumer> createToVersionControlMsgConsumer() { return new TbServiceBusConsumerTemplate<>(vcAdmin, serviceBusSettings, topicService.buildTopicName(vcSettings.getTopic()), - msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToVersionControlServiceMsg.parseFrom(msg.getData()), msg.getHeaders()) ); } @@ -220,7 +229,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { return new TbServiceBusProducerTemplate<>(vcAdmin, serviceBusSettings, topicService.buildTopicName(vcSettings.getTopic())); } @@ -246,6 +255,30 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbServiceBusProducerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -266,5 +299,8 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul if (vcAdmin != null) { vcAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index 493e58dba7..71a7efe50b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -22,15 +22,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; @@ -48,6 +50,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportApiSettings; @@ -67,12 +70,14 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public ServiceBusTbCoreQueueFactory(TbServiceBusSettings serviceBusSettings, TbQueueCoreSettings coreSettings, @@ -82,6 +87,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.serviceBusSettings = serviceBusSettings; this.coreSettings = coreSettings; @@ -91,12 +97,14 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); this.jsExecutorAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getJsExecutorConfigs()); this.transportApiAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getTransportApiConfigs()); this.notificationAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getNotificationsConfigs()); + this.edgeAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getEdgeConfigs()); } @Override @@ -121,7 +129,8 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueProducer> createTbCoreNotificationsMsgProducer() { - return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getTopic())); + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -194,7 +203,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { } @Override - public TbQueueProducer> createVersionControlMsgProducer() { + public TbQueueProducer> createVersionControlMsgProducer() { //TODO: version-control return null; } @@ -221,6 +230,30 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { msg -> new TbProtoQueueMsg<>(msg.getKey(), ToHousekeeperServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); } + @Override + public TbQueueConsumer> createEdgeMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic()), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbServiceBusProducerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueConsumer> createToEdgeNotificationsMsgConsumer() { + return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName(), + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, + topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @PreDestroy private void destroy() { if (coreAdmin != null) { @@ -238,5 +271,8 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { if (notificationAdmin != null) { notificationAdmin.destroy(); } + if (edgeAdmin != null) { + edgeAdmin.destroy(); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index 92ef01fa00..643a9dee3b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -26,6 +26,9 @@ import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -45,6 +48,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; +import org.thingsboard.server.queue.settings.TbQueueEdgeSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.settings.TbQueueTransportNotificationSettings; @@ -62,11 +66,13 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact private final TbServiceBusSettings serviceBusSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbQueueEdgeSettings edgeSettings; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; + private final TbQueueAdmin edgeAdmin; public ServiceBusTbRuleEngineQueueFactory(TopicService topicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -74,6 +80,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact TbServiceBusSettings serviceBusSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbQueueEdgeSettings edgeSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { this.topicService = topicService; this.coreSettings = coreSettings; @@ -82,11 +89,13 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact this.serviceBusSettings = serviceBusSettings; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; + this.edgeSettings = edgeSettings; this.coreAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); this.jsExecutorAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getJsExecutorConfigs()); this.notificationAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getNotificationsConfigs()); + this.edgeAdmin = new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getEdgeConfigs()); } @Override @@ -114,6 +123,16 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getTopic())); } + @Override + public TbQueueProducer> createEdgeMsgProducer() { + return new TbServiceBusProducerTemplate<>(edgeAdmin, serviceBusSettings, topicService.buildTopicName(edgeSettings.getTopic())); + } + + @Override + public TbQueueProducer> createEdgeNotificationsMsgProducer() { + return new TbServiceBusProducerTemplate<>(notificationAdmin, serviceBusSettings, topicService.getEdgeNotificationsTopic(serviceInfoProvider.getServiceId()).getFullTopicName()); + } + @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { return new TbServiceBusConsumerTemplate<>(ruleEngineAdmin, serviceBusSettings, topicService.buildTopicName(configuration.getTopic()), @@ -156,12 +175,12 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact } @Override - public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { + public TbQueueProducer> createToOtaPackageStateServiceMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getOtaPackageTopic())); } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java index fd493eb2e1..7f3e246eec 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTransportQueueFactory.java @@ -19,9 +19,9 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; @@ -133,7 +133,7 @@ public class ServiceBusTransportQueueFactory implements TbTransportQueueFactory } @Override - public TbQueueProducer> createHousekeeperMsgProducer() { + public TbQueueProducer> createHousekeeperMsgProducer() { return new TbServiceBusProducerTemplate<>(coreAdmin, serviceBusSettings, topicService.buildTopicName(coreSettings.getHousekeeperTopic())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java index 20a67c2e09..cecfddf7a7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueFactory.java @@ -18,6 +18,8 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -138,4 +140,12 @@ public interface TbCoreQueueFactory extends TbUsageStatsClientQueueFactory, Hous TbQueueConsumer> createHousekeeperReprocessingMsgConsumer(); + TbQueueConsumer> createEdgeMsgConsumer(); + + TbQueueProducer> createEdgeMsgProducer(); + + TbQueueConsumer> createToEdgeNotificationsMsgConsumer(); + + TbQueueProducer> createEdgeNotificationsMsgProducer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 3fcb49f47c..cf94bf68d0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -19,6 +19,8 @@ import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -39,6 +41,8 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toTbCore; private TbQueueProducer> toRuleEngineNotifications; private TbQueueProducer> toTbCoreNotifications; + private TbQueueProducer> toEdge; + private TbQueueProducer> toEdgeNotifications; private TbQueueProducer> toUsageStats; private TbQueueProducer> toVersionControl; private TbQueueProducer> toHousekeeper; @@ -57,6 +61,8 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toVersionControl = tbQueueProvider.createVersionControlMsgProducer(); this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); + this.toEdge = tbQueueProvider.createEdgeMsgProducer(); + this.toEdgeNotifications = tbQueueProvider.createEdgeNotificationsMsgProducer(); } @Override @@ -99,4 +105,15 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { return toHousekeeper; } + + @Override + public TbQueueProducer> getTbEdgeMsgProducer() { + return toEdge; + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + return toEdgeNotifications; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java index a752301cd8..a0c43a9e4b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbQueueProducerProvider.java @@ -17,6 +17,8 @@ package org.thingsboard.server.queue.provider; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; @@ -73,7 +75,7 @@ public interface TbQueueProducerProvider { */ TbQueueProducer> getTbUsageStatsMsgProducer(); - /** + /** * Used to push messages to other instances of TB Core Service * * @return @@ -82,4 +84,8 @@ public interface TbQueueProducerProvider { TbQueueProducer> getHousekeeperMsgProducer(); + TbQueueProducer> getTbEdgeMsgProducer(); + + TbQueueProducer> getTbEdgeNotificationsMsgProducer(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java index 908369ea15..8797064204 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineProducerProvider.java @@ -18,14 +18,16 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -41,6 +43,8 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { private TbQueueProducer> toTbCoreNotifications; private TbQueueProducer> toUsageStats; private TbQueueProducer> toHousekeeper; + private TbQueueProducer> toEdge; + private TbQueueProducer> toEdgeNotifications; public TbRuleEngineProducerProvider(TbRuleEngineQueueFactory tbQueueProvider) { this.tbQueueProvider = tbQueueProvider; @@ -55,6 +59,8 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { this.toTbCoreNotifications = tbQueueProvider.createTbCoreNotificationsMsgProducer(); this.toUsageStats = tbQueueProvider.createToUsageStatsServiceMsgProducer(); this.toHousekeeper = tbQueueProvider.createHousekeeperMsgProducer(); + this.toEdge = tbQueueProvider.createEdgeMsgProducer(); + this.toEdgeNotifications = tbQueueProvider.createEdgeNotificationsMsgProducer(); } @Override @@ -82,13 +88,23 @@ public class TbRuleEngineProducerProvider implements TbQueueProducerProvider { return toTbCoreNotifications; } + @Override + public TbQueueProducer> getTbEdgeMsgProducer() { + return toEdge; + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + return toEdgeNotifications; + } + @Override public TbQueueProducer> getTbUsageStatsMsgProducer() { return toUsageStats; } @Override - public TbQueueProducer> getTbVersionControlMsgProducer() { + public TbQueueProducer> getTbVersionControlMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Rule Engine!"); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index e49074f92d..dda3f33ed4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -19,6 +19,9 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -67,14 +70,18 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory * * @return */ - TbQueueProducer> createTbCoreNotificationsMsgProducer(); + TbQueueProducer> createTbCoreNotificationsMsgProducer(); + + TbQueueProducer> createEdgeMsgProducer(); + + TbQueueProducer> createEdgeNotificationsMsgProducer(); /** * Used to consume messages about firmware update notifications to TB Core Service * * @return */ - TbQueueProducer> createToOtaPackageStateServiceMsgProducer(); + TbQueueProducer> createToOtaPackageStateServiceMsgProducer(); /** * Used to consume messages by TB Rule Engine Service diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java index df394cd997..b2c7856b20 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbTransportQueueProducerProvider.java @@ -18,14 +18,16 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -79,7 +81,17 @@ public class TbTransportQueueProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbVersionControlMsgProducer() { + public TbQueueProducer> getTbEdgeMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used Transport!"); + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Transport!"); + } + + @Override + public TbQueueProducer> getTbVersionControlMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Transport!"); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java index 7547c73f71..a8f3b124e2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbVersionControlProducerProvider.java @@ -18,14 +18,16 @@ package org.thingsboard.server.queue.provider; import jakarta.annotation.PostConstruct; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -75,7 +77,17 @@ public class TbVersionControlProducerProvider implements TbQueueProducerProvider } @Override - public TbQueueProducer> getTbVersionControlMsgProducer() { + public TbQueueProducer> getTbEdgeMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + } + + @Override + public TbQueueProducer> getTbEdgeNotificationsMsgProducer() { + throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); + } + + @Override + public TbQueueProducer> getTbVersionControlMsgProducer() { throw new RuntimeException("Not Implemented! Should not be used by Version Control Service!"); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java index 2faf4b1985..14aa67a4f0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java @@ -27,6 +27,7 @@ import java.util.Map; @Component @ConditionalOnExpression("'${queue.type:null}'=='pubsub'") public class TbPubSubSubscriptionSettings { + @Value("${queue.pubsub.queue-properties.core:}") private String coreProperties; @Value("${queue.pubsub.queue-properties.rule-engine:}") @@ -39,6 +40,8 @@ public class TbPubSubSubscriptionSettings { private String jsExecutorProperties; @Value("${queue.pubsub.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.pubsub.queue-properties.edge:}") + private String edgeProperties; @Getter private Map coreSettings; @@ -52,6 +55,8 @@ public class TbPubSubSubscriptionSettings { private Map jsExecutorSettings; @Getter private Map vcSettings; + @Getter + private Map edgeSettings; @PostConstruct private void init() { @@ -61,6 +66,7 @@ public class TbPubSubSubscriptionSettings { notificationsSettings = PropertyUtils.getProps(notificationsProperties); jsExecutorSettings = PropertyUtils.getProps(jsExecutorProperties); vcSettings = PropertyUtils.getProps(vcProperties); + edgeSettings = PropertyUtils.getProps(edgeProperties); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java index acd008fb1d..06613d5617 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqQueueArguments.java @@ -41,6 +41,8 @@ public class TbRabbitMqQueueArguments { private String jsExecutorProperties; @Value("${queue.rabbitmq.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.rabbitmq.queue-properties.edge:}") + private String edgeProperties; @Getter private Map coreArgs; @@ -54,6 +56,8 @@ public class TbRabbitMqQueueArguments { private Map jsExecutorArgs; @Getter private Map vcArgs; + @Getter + private Map edgeArgs; @PostConstruct private void init() { @@ -63,6 +67,7 @@ public class TbRabbitMqQueueArguments { notificationsArgs = getArgs(notificationsProperties); jsExecutorArgs = getArgs(jsExecutorProperties); vcArgs = getArgs(vcProperties); + edgeArgs = getArgs(edgeProperties); } public static Map getArgs(String properties) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java new file mode 100644 index 0000000000..87fefc8113 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueEdgeSettings.java @@ -0,0 +1,31 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.settings; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Lazy +@Data +@Component +public class TbQueueEdgeSettings { + + @Value("${queue.edge.topic}") + private String topic; + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java index 0c61c2d2eb..2c1b8793eb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java @@ -43,6 +43,8 @@ public class TbAwsSqsQueueAttributes { private String otaProperties; @Value("${queue.aws-sqs.queue-properties.version-control:}") private String vcProperties; + @Value("${queue.aws-sqs.queue-properties.edge:}") + private String edgeProperties; @Getter private Map coreAttributes; @@ -58,6 +60,8 @@ public class TbAwsSqsQueueAttributes { private Map otaAttributes; @Getter private Map vcAttributes; + @Getter + private Map edgeAttributes; private final Map defaultAttributes = new HashMap<>(); @@ -72,6 +76,7 @@ public class TbAwsSqsQueueAttributes { jsExecutorAttributes = getConfigs(jsExecutorProperties); otaAttributes = getConfigs(otaProperties); vcAttributes = getConfigs(vcProperties); + edgeAttributes = getConfigs(edgeProperties); } private Map getConfigs(String properties) { diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java index 6c854ac8d6..3833155c05 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java @@ -21,9 +21,10 @@ public enum StatsType { TRANSPORT("transport"), JS_INVOKE("jsInvoke"), RATE_EXECUTOR("rateExecutor"), - HOUSEKEEPER("housekeeper"); + HOUSEKEEPER("housekeeper"), + EDGE("edge"); - private String name; + private final String name; StatsType(String name) { this.name = name; diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index 99401bbdf0..138505bd8d 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -79,6 +79,7 @@ import org.thingsboard.server.queue.util.TbVersionControlComponent; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -174,7 +175,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe } } } - consumer.subscribe(event.getPartitions()); + consumer.subscribe(event.getPartitionsMap().values().stream().findAny().orElse(Collections.emptySet())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 6a9536d8ac..1e0202bf79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -17,11 +17,9 @@ package org.thingsboard.server.dao.edge; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import jakarta.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; @@ -54,6 +52,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -97,8 +96,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService() { - @Nullable - @Override - public List apply(@Nullable List edgeList) { - return edgeList == null ? - Collections.emptyList() : - edgeList.stream().filter(edge -> query.getEdgeTypes().contains(edge.getType())).collect(Collectors.toList()); - } - }, MoreExecutors.directExecutor()); - + edges = Futures.transform(edges, edgeList -> edgeList == null ? + Collections.emptyList() : + edgeList.stream().filter(edge -> query.getEdgeTypes().contains(edge.getType())).collect(Collectors.toList()), MoreExecutors.directExecutor()); return edges; } @@ -417,19 +408,11 @@ public class EdgeServiceImpl extends AbstractCachedEntityService pageData; - do { - pageData = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId, pageLink); - if (pageData.getData().size() > 0) { - for (RuleChain ruleChain : pageData.getData()) { - ruleChainService.assignRuleChainToEdge(tenantId, ruleChain.getId(), edgeId); - } - } - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); + PageDataIterable ruleChains = new PageDataIterable<>( + link -> ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId, link), 1024); + for (RuleChain ruleChain : ruleChains) { + ruleChainService.assignRuleChainToEdge(tenantId, ruleChain.getId(), edgeId); + } } @Override @@ -580,6 +563,17 @@ public class EdgeServiceImpl extends AbstractCachedEntityService isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String key) { ListenableFuture> futureKvEntry; @@ -594,17 +588,11 @@ public class EdgeServiceImpl extends AbstractCachedEntityService findEdgeRuleChains(TenantId tenantId, EdgeId edgeId) { List result = new ArrayList<>(); - PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - PageData pageData; - do { - pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { - result.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } - } while (pageData != null && pageData.hasNext()); + PageDataIterable ruleChains = new PageDataIterable<>( + link -> ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, link), 1024); + for (RuleChain ruleChain : ruleChains) { + result.add(ruleChain); + } return result; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java new file mode 100644 index 0000000000..948b860649 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.EdgeId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("EdgeSessionCache") +public class EdgeSessionCaffeineCache extends CaffeineTbTransactionalCache { + + public EdgeSessionCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.EDGE_SESSIONS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java new file mode 100644 index 0000000000..518c0c0b32 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeSessionRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright ยฉ 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.EdgeId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("EdgeSessionCache") +public class EdgeSessionRedisCache extends RedisTbTransactionalCache { + + public EdgeSessionRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.EDGE_SESSIONS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(String.class)); + } + +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 48f7c92b1d..e71c6b1294 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -75,6 +75,9 @@ cache.specs.otaPackagesData.maxSize=100000 cache.specs.edges.timeToLiveInMinutes=1440 cache.specs.edges.maxSize=100000 +cache.specs.edgeSessions.timeToLiveInMinutes=1440 +cache.specs.edgeSessions.maxSize=100000 + cache.specs.relatedEdges.timeToLiveInMinutes=1440 cache.specs.relatedEdges.maxSize=100000 From 4831a86295cde415b2ce2512a20f17b1d78fac3d Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 29 Aug 2024 13:16:56 +0300 Subject: [PATCH 40/80] Added UI part of RuleChain Editor Version Conflict --- .../rulechain/rulechain-page.component.ts | 50 ++++++++++++------- .../entity-conflict-dialog.component.html | 2 +- .../entity-conflict-dialog.component.ts | 12 +++-- .../import-export/import-export.service.ts | 30 +++++++---- ui-ngx/src/app/shared/models/entity.models.ts | 5 ++ .../app/shared/models/rule-chain.models.ts | 2 +- 6 files changed, 69 insertions(+), 32 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 46c84087ee..eb640a3af8 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -77,8 +77,8 @@ import { } from '@shared/models/rule-node.models'; import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; import { RuleChainService } from '@core/http/rule-chain.service'; -import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; +import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject, throwError } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { ISearchableComponent } from '../../models/searchable-component.models'; import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; @@ -93,6 +93,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; import { MatDrawer } from '@angular/material/sidenav'; +import { HttpStatusCode } from '@angular/common/http'; import Timeout = NodeJS.Timeout; @Component({ @@ -1282,7 +1283,9 @@ export class RuleChainPageComponent extends PageComponent onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) { const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent; const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent; - if (ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript && ruleNodeConfigDefinedComponent.testScript) { + if (ruleNodeConfigComponent.useDefinedDirective() + && ruleNodeConfigDefinedComponent.hasScript + && ruleNodeConfigDefinedComponent.testScript) { ruleNodeConfigDefinedComponent.testScript(debugEventBody); } } @@ -1456,7 +1459,8 @@ export class RuleChainPageComponent extends PageComponent const ruleChainMetaData: RuleChainMetaData = { ruleChainId: this.ruleChain.id, nodes: [], - connections: [] + connections: [], + version: ruleChain.version }; const nodes: FcRuleNode[] = []; this.ruleChainModel.nodes.forEach((node) => { @@ -1465,7 +1469,9 @@ export class RuleChainPageComponent extends PageComponent id: node.ruleNodeId, type: node.component.clazz, name: node.name, - configurationVersion: isDefinedAndNotNull(node.configurationVersion) ? node.configurationVersion : node.component.configurationVersion, + configurationVersion: isDefinedAndNotNull(node.configurationVersion) + ? node.configurationVersion + : node.component.configurationVersion, configuration: node.configuration, additionalInfo: node.additionalInfo ? node.additionalInfo : {}, debugMode: node.debugMode, @@ -1500,20 +1506,30 @@ export class RuleChainPageComponent extends PageComponent }); } }); - this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe((savedRuleChainMetaData) => { - this.ruleChainMetaData = savedRuleChainMetaData; - if (this.isImport) { - this.isDirtyValue = false; - this.isImport = false; - if (this.ruleChainType !== RuleChainType.EDGE) { - this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); + this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData) + .pipe( + catchError(err => { + if (err.status === HttpStatusCode.Conflict) { + return this.ruleChainService.getRuleChainMetadata(ruleChainMetaData.ruleChainId.id); + } + return throwError(() => err); + }) + ) + .subscribe((savedRuleChainMetaData) => { + this.ruleChain.version = savedRuleChainMetaData.version; + this.ruleChainMetaData = savedRuleChainMetaData; + if (this.isImport) { + this.isDirtyValue = false; + this.isImport = false; + if (this.ruleChainType !== RuleChainType.EDGE) { + this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); + } else { + this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); + } } else { - this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); + this.createRuleChainModel(); } - } else { - this.createRuleChainModel(); - } - saveResult.next(); + saveResult.next(); }); }); return saveResult; diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html index ba09311895..2e16f04498 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html @@ -30,7 +30,7 @@
{{ 'entity.version-conflict.link' | translate: - { entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) } + { entityType: (entityTypeTranslations.get(entityId.entityType).type | translate) } }} {{ 'entity.link' | translate }}. diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts index 37275bd79c..c0879d8ac2 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts @@ -20,11 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { CommonModule } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; -import { EntityInfoData } from '@shared/models/entity.models'; +import { EntityDTOInfoData, EntityInfoData } from '@shared/models/entity.models'; +import { EntityId } from '@shared/models/id/entity-id'; interface EntityConflictDialogData { message: string; - entity: EntityInfoData; + entity: EntityInfoData | EntityDTOInfoData; } @Component({ @@ -38,13 +39,18 @@ interface EntityConflictDialogData { ], }) export class EntityConflictDialogComponent { + + entityId: EntityId; + readonly entityTypeTranslations = entityTypeTranslations; constructor( @Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData, private dialogRef: MatDialogRef, private importExportService: ImportExportService, - ) {} + ) { + this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as EntityDTOInfoData).ruleChainId; + } onCancel(): void { this.dialogRef.close(); diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 4746365c50..be25786d5d 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -55,7 +55,12 @@ import { EntityType } from '@shared/models/entity-type.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetService } from '@core/http/widget.service'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; -import { EntityInfoData, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; +import { + EntityDTOInfoData, + EntityInfoData, + ImportEntitiesResultInfo, + ImportEntityData +} from '@shared/models/entity.models'; import { RequestConfig } from '@core/http/http-utils'; import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; @@ -360,25 +365,26 @@ export class ImportExportService { }); } - public exportEntity(entityData: EntityInfoData): void { + public exportEntity(entityData: EntityInfoData | EntityDTOInfoData): void { + const id = (entityData as EntityInfoData).id ?? (entityData as EntityDTOInfoData).ruleChainId; let preparedData; - switch (entityData.id.entityType) { + switch (id.entityType) { case EntityType.DEVICE_PROFILE: case EntityType.ASSET_PROFILE: preparedData = this.prepareProfileExport(entityData as DeviceProfile | AssetProfile); break; case EntityType.RULE_CHAIN: - this.ruleChainService.getRuleChainMetadata(entityData.id.id) + forkJoin([this.ruleChainService.getRuleChainMetadata(id.id), this.ruleChainService.getRuleChain(id.id)]) .pipe( take(1), - map((ruleChainMetaData) => { + map(([ruleChainMetaData, ruleChain]) => { const ruleChainExport: RuleChainImport = { - ruleChain: this.prepareRuleChain(entityData as RuleChain), + ruleChain: this.prepareRuleChain(ruleChain), metadata: this.prepareRuleChainMetaData(ruleChainMetaData) }; return ruleChainExport; })) - .subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name)); + .subscribe(this.onRuleChainExported()); return; case EntityType.WIDGETS_BUNDLE: this.exportSelectedWidgetsBundle(entityData as WidgetsBundle); @@ -584,8 +590,12 @@ export class ImportExportService { return ruleChainExport; }) )) - ).subscribe({ - next: (ruleChainExport) => { + ).subscribe(this.onRuleChainExported()); + } + + private onRuleChainExported() { + return { + next: (ruleChainExport: RuleChainImport) => { let name = ruleChainExport.ruleChain.name; name = name.toLowerCase().replace(/\W/g, '_'); this.exportToPc(ruleChainExport, name); @@ -593,7 +603,7 @@ export class ImportExportService { error: (e) => { this.handleExportError(e, 'rulechain.export-failed-error'); } - }); + }; } public importRuleChain(expectedRuleChainType: RuleChainType): Observable { diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 40ec570b13..5d9a3f6f04 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -34,6 +34,11 @@ export interface EntityInfoData { name: string; } +export interface EntityDTOInfoData { + ruleChainId: EntityId; + name: string; +} + export interface ImportEntityData { lineNumber: number; name: string; diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index 0bfdfbc459..bca4ae399e 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -34,7 +34,7 @@ export interface RuleChain extends BaseData, HasTenantId, HasVersio isDefault?: boolean; } -export interface RuleChainMetaData { +export interface RuleChainMetaData extends HasVersion { ruleChainId: RuleChainId; firstNodeIndex?: number; nodes: Array; From 61e7ba46fcbcdc288343c1193a0bffc90ebdfcda Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 29 Aug 2024 15:25:20 +0300 Subject: [PATCH 41/80] Adjusted typing --- .../entity-conflict-dialog.component.ts | 7 ++++--- .../src/app/shared/import-export/import-export.service.ts | 7 +++---- ui-ngx/src/app/shared/models/entity.models.ts | 5 ----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts index c0879d8ac2..b4f34ed232 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts @@ -20,12 +20,13 @@ import { SharedModule } from '@shared/shared.module'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { CommonModule } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; -import { EntityDTOInfoData, EntityInfoData } from '@shared/models/entity.models'; +import { EntityInfoData } from '@shared/models/entity.models'; import { EntityId } from '@shared/models/id/entity-id'; +import { RuleChainMetaData } from '@shared/models/rule-chain.models'; interface EntityConflictDialogData { message: string; - entity: EntityInfoData | EntityDTOInfoData; + entity: EntityInfoData | RuleChainMetaData; } @Component({ @@ -49,7 +50,7 @@ export class EntityConflictDialogComponent { private dialogRef: MatDialogRef, private importExportService: ImportExportService, ) { - this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as EntityDTOInfoData).ruleChainId; + this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as RuleChainMetaData).ruleChainId; } onCancel(): void { diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index be25786d5d..5f246873d7 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -56,7 +56,6 @@ import { UtilsService } from '@core/services/utils.service'; import { WidgetService } from '@core/http/widget.service'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { - EntityDTOInfoData, EntityInfoData, ImportEntitiesResultInfo, ImportEntityData @@ -365,8 +364,8 @@ export class ImportExportService { }); } - public exportEntity(entityData: EntityInfoData | EntityDTOInfoData): void { - const id = (entityData as EntityInfoData).id ?? (entityData as EntityDTOInfoData).ruleChainId; + public exportEntity(entityData: EntityInfoData | RuleChainMetaData): void { + const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId; let preparedData; switch (id.entityType) { case EntityType.DEVICE_PROFILE: @@ -395,7 +394,7 @@ export class ImportExportService { default: preparedData = this.prepareExport(entityData); } - this.exportToPc(preparedData, entityData.name); + this.exportToPc(preparedData, (entityData as EntityInfoData).name); } private exportSelectedWidgetsBundle(widgetsBundle: WidgetsBundle): void { diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 5d9a3f6f04..40ec570b13 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -34,11 +34,6 @@ export interface EntityInfoData { name: string; } -export interface EntityDTOInfoData { - ruleChainId: EntityId; - name: string; -} - export interface ImportEntityData { lineNumber: number; name: string; From 12415c15185f2e6a9060411af78308c1aa38b6eb Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 29 Aug 2024 16:11:28 +0300 Subject: [PATCH 42/80] UI: bug fix --- .../json/system/scada_symbols/stand-filter.svg | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/stand-filter.svg b/application/src/main/data/json/system/scada_symbols/stand-filter.svg index 87d578605d..ebd1e28edd 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-filter.svg @@ -7,7 +7,7 @@ ], "widgetSizeX": 3, "widgetSizeY": 5, - "stateRenderFunction": "", + "stateRenderFunction": "var running = ctx.values.running;\nif (running) {\n ctx.api.enable(svg.children());\n} else {\n ctx.api.disable(svg.children());\n}", "tags": [ { "tag": "background", @@ -25,7 +25,7 @@ }, { "tag": "filterMode", - "stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var i = 0;\n if (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n }\n if (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n }\n if (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n }\n if (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n }\n if (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n }\n if (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n }\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n if (isFinite(filtrationMode)) {\n setFilterModeColorsByMap(filtrationMode, running);\n }\n}\n\nif (running) {\n ctx.api.enable(element.children());\n} else {\n ctx.api.disable(element.children());\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}", + "stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nvar i = 0;\nif (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n}\nif (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n}\nif (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n}\nif (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n}\nif (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n}\nif (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n}\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n}\n\nif (isFinite(filtrationMode)) {\n if (element.findOne('.active')) {\n setFilterModeColors(element.findOne('.active'));\n }\n setFilterModeColorsByMap(filtrationMode, running);\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n ctx.api.enable(element.children());\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n if (active) {\n filterBox.addClass('active');\n } else {\n filterBox.removeClass('active');\n }\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}", "actions": null } ], @@ -159,7 +159,7 @@ "id": "filtrationMode", "name": "{i18n:scada.symbol.filter-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -175,7 +175,7 @@ "id": "wasteMode", "name": "{i18n:scada.symbol.waste-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -191,7 +191,7 @@ "id": "backwashMode", "name": "{i18n:scada.symbol.backwash-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -207,7 +207,7 @@ "id": "recirculateMode", "name": "{i18n:scada.symbol.recirculate-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -223,7 +223,7 @@ "id": "rinseMode", "name": "{i18n:scada.symbol.rinse-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, @@ -239,7 +239,7 @@ "id": "closedMode", "name": "{i18n:scada.symbol.closed-mode}", "type": "switch", - "default": false, + "default": true, "required": null, "subLabel": null, "divider": null, From 9593f97582e292b78ad6de11c766a88e9936ef6f Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 29 Aug 2024 17:05:53 +0300 Subject: [PATCH 43/80] TbHttpClient: use default 256 KB for maxInMemorySize in case 0 set in configuration --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 4830761110..104e477346 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -132,7 +132,8 @@ public class TbHttpClient { this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(config.getMaxInMemoryBufferSizeInKb() * 1024)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize( + (config.getMaxInMemoryBufferSizeInKb() > 0 ? config.getMaxInMemoryBufferSizeInKb() : 256) * 1024)) .build(); } catch (SSLException e) { throw new TbNodeException(e); From 4c3e1c62f2f77626d755327de555c12bba21ffc2 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 29 Aug 2024 18:24:12 +0300 Subject: [PATCH 44/80] tbel: fix bug ExecutionHashMap.entrySet element.key element.value Update version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 09cf0393b7..aa2a50bc8b 100755 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 3.9.2 3.25.3 1.63.0 - 1.2.2 + 1.2.3 1.18.32 1.2.5 1.2.5 From d6c40c1bb20ce4b44e9575fdb4676bbe5060645d Mon Sep 17 00:00:00 2001 From: Chantsova Ekaterina Date: Thu, 29 Aug 2024 18:28:36 +0300 Subject: [PATCH 45/80] UI: Recent dashboards widget - apply custom translation for dashboard titles --- .../lib/home-page/recent-dashboards-widget.component.html | 4 ++-- .../shared/components/dashboard-autocomplete.component.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html index 72b5d8a7f9..6c73e47574 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html @@ -48,7 +48,7 @@ {{ 'widgets.recent-dashboards.name' | translate }} - {{ lastVisitedDashboard.title }} + {{ lastVisitedDashboard.title | customTranslate }} @@ -78,7 +78,7 @@ class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}
diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html index 6512aa4e73..13cf1bd664 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html @@ -32,7 +32,7 @@ [matAutocomplete]="dashboardAutocomplete" [fxShow]="!useDashboardLink || !disabled || !selectDashboardFormGroup.get('dashboard').value"> - {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) }} + {{ displayDashboardFn(selectDashboardFormGroup.get('dashboard').value) | customTranslate }} + + + {{country.flag}} + + + +
+
+ contact.no-country-found +
+ + + {{ 'contact.no-country-matching' | translate: + {country: (searchText | truncate: true: 6: '...')} }} + + +
+
+
+ {{ autocompleteHint }} + + {{ requiredText }} + + diff --git a/ui-ngx/src/app/shared/components/country-autocomplete.component.ts b/ui-ngx/src/app/shared/components/country-autocomplete.component.ts new file mode 100644 index 0000000000..7fc3a10bca --- /dev/null +++ b/ui-ngx/src/app/shared/components/country-autocomplete.component.ts @@ -0,0 +1,209 @@ +/// +/// Copyright ยฉ 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { Country, CountryData } from '@shared/models/country.models'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { isNotEmptyStr } from '@core/utils'; +import { Observable, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; +import { SubscriptSizing } from '@angular/material/form-field'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { TranslateService } from '@ngx-translate/core'; + +interface CountrySearchData extends Country { + searchText?: string; +} + +@Component({ + selector: 'tb-country-autocomplete', + templateUrl: 'country-autocomplete.component.html', + providers: [ + CountryData, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CountryAutocompleteComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CountryAutocompleteComponent), + multi: true + } + ] +}) +export class CountryAutocompleteComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + labelText = this.translate.instant('contact.country'); + + @Input() + requiredText = this.translate.instant('contact.country-required'); + + @Input() + autocompleteHint: string; + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + required = false; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @ViewChild('countryInput', {static: true}) countryInput: ElementRef; + + @Output() + selectCountryCode = new EventEmitter(); + + countryFormGroup: FormGroup; + + searchText = ''; + + filteredCountries: Observable>; + + onTouched = () => { + }; + private propagateChange: (value: any) => void = () => { + }; + + private modelValue: Country; + + private allCountries: CountrySearchData[] = this.countryData.allCountries; + private initSearchData = false; + private dirty = false; + + constructor(private fb: FormBuilder, + private countryData: CountryData, + private translate: TranslateService) { + this.countryFormGroup = this.fb.group({ + country: [''] + }); + } + + ngOnInit(): void { + this.filteredCountries = this.countryFormGroup.get('country').valueChanges.pipe( + debounceTime(150), + tap(value => { + let modelValue: Country; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + distinctUntilChanged(), + switchMap(name => of(this.fetchCountries(name))), + share() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.countryFormGroup.disable({emitEvent: false}); + } else { + this.countryFormGroup.enable({emitEvent: false}); + } + } + + validate(): ValidationErrors | null { + return this.countryFormGroup.valid ? null : { + countryFormGroup: false + }; + } + + writeValue(country: string) { + this.dirty = true; + + const findCountry = isNotEmptyStr(country) ? this.allCountries.find(item => item.name === country) : null; + + this.modelValue = findCountry || null; + this.countryFormGroup.get('country').patchValue(this.modelValue || '', { emitEvent: false }); + this.selectCountryCode.emit(this.modelValue ? this.modelValue.iso2 : null); + } + + displayCountryFn(country?: Country): string | undefined { + return country ? `${country.flag} ${country.name}` : undefined; + } + + onFocus() { + if (this.dirty) { + this.countryFormGroup.get('country').updateValueAndValidity({onlySelf: true}); + this.dirty = false; + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + clear() { + this.countryFormGroup.get('country').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.countryInput.nativeElement.blur(); + this.countryInput.nativeElement.focus(); + }, 0); + } + + private fetchCountries(searchText: string): Country[] { + this.searchText = searchText; + if (!this.initSearchData) { + this.allCountries.forEach(country => { + country.searchText = `${country.name} ${country.iso2}`.toLowerCase(); + }); + this.initSearchData = true; + } + if (isNotEmptyStr(searchText)) { + const filterValue = searchText.toLowerCase(); + return this.allCountries.filter(country => country.searchText.includes(filterValue)); + } + return this.allCountries; + } + + private updateView(value: Country | null) { + if (this.modelValue?.name !== value?.name) { + this.modelValue = value; + this.propagateChange(this.modelValue); + if (value) { + this.selectCountryCode.emit(value.iso2); + } + } + } +} diff --git a/ui-ngx/src/app/shared/models/country.models.ts b/ui-ngx/src/app/shared/models/country.models.ts index 919d362067..d8563e2449 100644 --- a/ui-ngx/src/app/shared/models/country.models.ts +++ b/ui-ngx/src/app/shared/models/country.models.ts @@ -22,6 +22,7 @@ export interface Country { dialCode: string; areaCodes?: string[]; flag: string; + postCodePattern?: RegExp; } export enum CountryISO { @@ -80,10 +81,10 @@ export enum CountryISO { CongoRepublicCongoBrazzaville = 'CG', CookIslands = 'CK', CostaRica = 'CR', - CรดteDIvoire = 'CI', + CoteDivoire = 'CI', Croatia = 'HR', Cuba = 'CU', - Curaรงao = 'CW', + Curacao = 'CW', Cyprus = 'CY', CzechRepublic = 'CZ', Denmark = 'DK', @@ -155,7 +156,6 @@ export enum CountryISO { Lithuania = 'LT', Luxembourg = 'LU', Macau = 'MO', - Macedonia = 'MK', Madagascar = 'MG', Malawi = 'MW', Malaysia = 'MY', @@ -189,6 +189,7 @@ export enum CountryISO { Niue = 'NU', NorfolkIsland = 'NF', NorthKorea = 'KP', + NorthMacedonia = 'MK', NorthernMarianaIslands = 'MP', Norway = 'NO', Oman = 'OM', @@ -205,11 +206,11 @@ export enum CountryISO { Portugal = 'PT', PuertoRico = 'PR', Qatar = 'QA', - Rรฉunion = 'RE', + Reunion = 'RE', Romania = 'RO', Russia = 'RU', Rwanda = 'RW', - SaintBarthรฉlemy = 'BL', + SaintBarthelemy = 'BL', SaintHelena = 'SH', SaintKittsAndNevis = 'KN', SaintLucia = 'LC', @@ -218,7 +219,7 @@ export enum CountryISO { SaintVincentAndTheGrenadines = 'VC', Samoa = 'WS', SanMarino = 'SM', - SรฃoTomรฉAndPrรญncipe = 'ST', + SaoTomeAndPrincipe = 'ST', SaudiArabia = 'SA', Senegal = 'SN', Serbia = 'RS', @@ -274,9 +275,10 @@ export enum CountryISO { Yemen = 'YE', Zambia = 'ZM', Zimbabwe = 'ZW', - ร…landIslands = 'AX', + AlandIslands = 'AX', } +/* eslint-disable max-len */ @Injectable() export class CountryData { public allCountries: Array = [ @@ -292,15 +294,15 @@ export class CountryData { {name: 'Argentina', iso2: CountryISO.Argentina, dialCode: '54', flag: '๐Ÿ‡ฆ๐Ÿ‡ท'}, {name: 'Armenia', iso2: CountryISO.Armenia, dialCode: '374', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ'}, {name: 'Aruba', iso2: CountryISO.Aruba, dialCode: '297', flag: '๐Ÿ‡ฆ๐Ÿ‡ผ'}, - {name: 'Australia', iso2: CountryISO.Australia, dialCode: '61', flag: '๐Ÿ‡ฆ๐Ÿ‡บ'}, - {name: 'Austria', iso2: CountryISO.Austria, dialCode: '43', flag: '๐Ÿ‡ฆ๐Ÿ‡น'}, + {name: 'Australia', iso2: CountryISO.Australia, dialCode: '61', flag: '๐Ÿ‡ฆ๐Ÿ‡บ', postCodePattern: /[0-9]{4}/}, + {name: 'Austria', iso2: CountryISO.Austria, dialCode: '43', flag: '๐Ÿ‡ฆ๐Ÿ‡น', postCodePattern: /[0-9]{4}/}, {name: 'Azerbaijan', iso2: CountryISO.Azerbaijan, dialCode: '994', flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ'}, {name: 'Bahamas', iso2: CountryISO.Bahamas, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ธ'}, {name: 'Bahrain', iso2: CountryISO.Bahrain, dialCode: '973', flag: '๐Ÿ‡ง๐Ÿ‡ญ'}, {name: 'Bangladesh', iso2: CountryISO.Bangladesh, dialCode: '880', flag: '๐Ÿ‡ง๐Ÿ‡ฉ'}, {name: 'Barbados', iso2: CountryISO.Barbados, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ง'}, {name: 'Belarus', iso2: CountryISO.Belarus, dialCode: '375', flag: '๐Ÿ‡ง๐Ÿ‡พ'}, - {name: 'Belgium', iso2: CountryISO.Belgium, dialCode: '32', flag: '๐Ÿ‡ง๐Ÿ‡ช'}, + {name: 'Belgium', iso2: CountryISO.Belgium, dialCode: '32', flag: '๐Ÿ‡ง๐Ÿ‡ช', postCodePattern: /[0-9]{4}/}, {name: 'Belize', iso2: CountryISO.Belize, dialCode: '501', flag: '๐Ÿ‡ง๐Ÿ‡ฟ'}, {name: 'Benin', iso2: CountryISO.Benin, dialCode: '229', flag: '๐Ÿ‡ง๐Ÿ‡ฏ'}, {name: 'Bermuda', iso2: CountryISO.Bermuda, dialCode: '1', flag: '๐Ÿ‡ง๐Ÿ‡ฒ'}, @@ -310,16 +312,16 @@ export class CountryData { {name: 'Bouvet Island', iso2: CountryISO.BouvetIsland, dialCode: '47', flag: '๐Ÿ‡ง๐Ÿ‡ป'}, {name: 'Bosnia and Herzegovina', iso2: CountryISO.BosniaAndHerzegovina, dialCode: '387', flag: '๐Ÿ‡ง๐Ÿ‡ฆ'}, {name: 'Botswana', iso2: CountryISO.Botswana, dialCode: '267', flag: '๐Ÿ‡ง๐Ÿ‡ผ'}, - {name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '๐Ÿ‡ง๐Ÿ‡ท'}, + {name: 'Brazil', iso2: CountryISO.Brazil, dialCode: '55', flag: '๐Ÿ‡ง๐Ÿ‡ท', postCodePattern: /[0-9]{5}-?[0-9]{3}/}, {name: 'British Indian Ocean Territory', iso2: CountryISO.BritishIndianOceanTerritory, dialCode: '246', flag: '๐Ÿ‡ฎ๐Ÿ‡ด'}, {name: 'British Virgin Islands', iso2: CountryISO.BritishVirginIslands, dialCode: '1', flag: '๐Ÿ‡ป๐Ÿ‡ฌ'}, - {name: 'Brunei', iso2: CountryISO.Brunei, dialCode: '673', flag: '๐Ÿ‡ง๐Ÿ‡ณ'}, + {name: 'Brunei Darussalam', iso2: CountryISO.Brunei, dialCode: '673', flag: '๐Ÿ‡ง๐Ÿ‡ณ'}, {name: 'Bulgaria', iso2: CountryISO.Bulgaria, dialCode: '359', flag: '๐Ÿ‡ง๐Ÿ‡ฌ'}, {name: 'Burkina Faso', iso2: CountryISO.BurkinaFaso, dialCode: '226', flag: '๐Ÿ‡ง๐Ÿ‡ซ'}, {name: 'Burundi', iso2: CountryISO.Burundi, dialCode: '257', flag: '๐Ÿ‡ง๐Ÿ‡ฎ'}, {name: 'Cambodia', iso2: CountryISO.Cambodia, dialCode: '855', flag: '๐Ÿ‡ฐ๐Ÿ‡ญ'}, {name: 'Cameroon', iso2: CountryISO.Cameroon, dialCode: '237', flag: '๐Ÿ‡จ๐Ÿ‡ฒ'}, - {name: 'Canada', iso2: CountryISO.Canada, dialCode: '1', flag: '๐Ÿ‡จ๐Ÿ‡ฆ'}, + {name: 'Canada', iso2: CountryISO.Canada, dialCode: '1', flag: '๐Ÿ‡จ๐Ÿ‡ฆ', postCodePattern: /^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$/}, {name: 'Cape Verde', iso2: CountryISO.CapeVerde, dialCode: '238', flag: '๐Ÿ‡จ๐Ÿ‡ป'}, {name: 'Caribbean Netherlands', iso2: CountryISO.CaribbeanNetherlands, dialCode: '599', flag: '๐Ÿ‡ง๐Ÿ‡ถ'}, {name: 'Cayman Islands', iso2: CountryISO.CaymanIslands, dialCode: '1', flag: '๐Ÿ‡ฐ๐Ÿ‡พ'}, @@ -328,20 +330,20 @@ export class CountryData { {name: 'Chile', iso2: CountryISO.Chile, dialCode: '56', flag: '๐Ÿ‡จ๐Ÿ‡ฑ'}, {name: 'China', iso2: CountryISO.China, dialCode: '86', flag: '๐Ÿ‡จ๐Ÿ‡ณ'}, {name: 'Christmas Island', iso2: CountryISO.ChristmasIsland, dialCode: '61', flag: '๐Ÿ‡จ๐Ÿ‡ฝ'}, - {name: 'Cocos Islands', iso2: CountryISO.Cocos, dialCode: '61', flag: '๐Ÿ‡จ๐Ÿ‡จ'}, + {name: 'Cocos (Keeling) Islands', iso2: CountryISO.Cocos, dialCode: '61', flag: '๐Ÿ‡จ๐Ÿ‡จ'}, {name: 'Colombia', iso2: CountryISO.Colombia, dialCode: '57', flag: '๐Ÿ‡จ๐Ÿ‡จ'}, {name: 'Comoros', iso2: CountryISO.Comoros, dialCode: '269', flag: '๐Ÿ‡ฐ๐Ÿ‡ฒ'}, - {name: 'Congo-Kinshasa', iso2: CountryISO.CongoDRCJamhuriYaKidemokrasiaYaKongo, dialCode: '243', flag: '๐Ÿ‡จ๐Ÿ‡ฉ'}, - {name: 'Congo-Brazzaville', iso2: CountryISO.CongoRepublicCongoBrazzaville, dialCode: '242', flag: '๐Ÿ‡จ๐Ÿ‡ฌ'}, + {name: 'Congo', iso2: CountryISO.CongoDRCJamhuriYaKidemokrasiaYaKongo, dialCode: '243', flag: '๐Ÿ‡จ๐Ÿ‡ฉ'}, + {name: 'Congo, Democratic Republic of the', iso2: CountryISO.CongoRepublicCongoBrazzaville, dialCode: '242', flag: '๐Ÿ‡จ๐Ÿ‡ฌ'}, {name: 'Cook Islands', iso2: CountryISO.CookIslands, dialCode: '682', flag: '๐Ÿ‡จ๐Ÿ‡ฐ'}, {name: 'Costa Rica', iso2: CountryISO.CostaRica, dialCode: '506', flag: '๐Ÿ‡จ๐Ÿ‡ท'}, - {name: 'Cรดte dโ€™Ivoire', iso2: CountryISO.CรดteDIvoire, dialCode: '225', flag: '๐Ÿ‡จ๐Ÿ‡ฎ'}, + {name: 'Cรดte d\'Ivoire', iso2: CountryISO.CoteDivoire, dialCode: '225', flag: '๐Ÿ‡จ๐Ÿ‡ฎ'}, {name: 'Croatia', iso2: CountryISO.Croatia, dialCode: '385', flag: '๐Ÿ‡ญ๐Ÿ‡ท'}, {name: 'Cuba', iso2: CountryISO.Cuba, dialCode: '53', flag: '๐Ÿ‡จ๐Ÿ‡บ'}, - {name: 'Curaรงao', iso2: CountryISO.Curaรงao, dialCode: '599', flag: '๐Ÿ‡จ๐Ÿ‡ผ'}, + {name: 'Curaรงao', iso2: CountryISO.Curacao, dialCode: '599', flag: '๐Ÿ‡จ๐Ÿ‡ผ'}, {name: 'Cyprus', iso2: CountryISO.Cyprus, dialCode: '357', flag: '๐Ÿ‡จ๐Ÿ‡พ'}, {name: 'Czech Republic', iso2: CountryISO.CzechRepublic, dialCode: '420', flag: '๐Ÿ‡จ๐Ÿ‡ฟ'}, - {name: 'Denmark', iso2: CountryISO.Denmark, dialCode: '45', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ'}, + {name: 'Denmark', iso2: CountryISO.Denmark, dialCode: '45', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ', postCodePattern: /[0-9]{3,4}/}, {name: 'Djibouti', iso2: CountryISO.Djibouti, dialCode: '253', flag: '๐Ÿ‡ฉ๐Ÿ‡ฏ'}, {name: 'Dominica', iso2: CountryISO.Dominica, dialCode: '1767', flag: '๐Ÿ‡ฉ๐Ÿ‡ฒ'}, {name: 'Dominican Republic', iso2: CountryISO.DominicanRepublic, dialCode: '1', flag: '๐Ÿ‡ฉ๐Ÿ‡ด'}, @@ -352,8 +354,8 @@ export class CountryData { {name: 'Eritrea', iso2: CountryISO.Eritrea, dialCode: '291', flag: '๐Ÿ‡ช๐Ÿ‡ท'}, {name: 'Estonia', iso2: CountryISO.Estonia, dialCode: '372', flag: '๐Ÿ‡ช๐Ÿ‡ช'}, {name: 'Ethiopia', iso2: CountryISO.Ethiopia, dialCode: '251', flag: '๐Ÿ‡ช๐Ÿ‡น'}, - {name: 'Falkland Islands', iso2: CountryISO.FalklandIslands, dialCode: '500', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ'}, - {name: 'Faroe Islands', iso2: CountryISO.FaroeIslands, dialCode: '298', flag: '๐Ÿ‡ซ๐Ÿ‡ด'}, + {name: 'Falkland Islands (Malvinas)', iso2: CountryISO.FalklandIslands, dialCode: '500', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ'}, + {name: 'Faroe Islands', iso2: CountryISO.FaroeIslands, dialCode: '298', flag: '๐Ÿ‡ซ๐Ÿ‡ด', postCodePattern: /[0-9]{3,4}/}, {name: 'Fiji', iso2: CountryISO.Fiji, dialCode: '679', flag: '๐Ÿ‡ซ๐Ÿ‡ฏ'}, {name: 'Finland', iso2: CountryISO.Finland, dialCode: '358', flag: '๐Ÿ‡ซ๐Ÿ‡ฎ'}, {name: 'France', iso2: CountryISO.France, dialCode: '33', flag: '๐Ÿ‡ซ๐Ÿ‡ท'}, @@ -363,7 +365,7 @@ export class CountryData { {name: 'Gabon', iso2: CountryISO.Gabon, dialCode: '241', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ'}, {name: 'Gambia', iso2: CountryISO.Gambia, dialCode: '220', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ'}, {name: 'Georgia', iso2: CountryISO.Georgia, dialCode: '995', flag: '๐Ÿ‡ฌ๐Ÿ‡ช'}, - {name: 'Germany', iso2: CountryISO.Germany, dialCode: '49', flag: '๐Ÿ‡ฉ๐Ÿ‡ช'}, + {name: 'Germany', iso2: CountryISO.Germany, dialCode: '49', flag: '๐Ÿ‡ฉ๐Ÿ‡ช', postCodePattern: /[0-9]{5}/}, {name: 'Ghana', iso2: CountryISO.Ghana, dialCode: '233', flag: '๐Ÿ‡ฌ๐Ÿ‡ญ'}, {name: 'Gibraltar', iso2: CountryISO.Gibraltar, dialCode: '350', flag: '๐Ÿ‡ฌ๐Ÿ‡ฎ'}, {name: 'Greece', iso2: CountryISO.Greece, dialCode: '30', flag: '๐Ÿ‡ฌ๐Ÿ‡ท'}, @@ -380,18 +382,18 @@ export class CountryData { {name: 'Heard Island and McDonald Islands', iso2: CountryISO.HeardIslandandMcDonaldIslands, dialCode: '672', flag: '๐Ÿ‡ญ๐Ÿ‡ฒ'}, {name: 'Honduras', iso2: CountryISO.Honduras, dialCode: '504', flag: '๐Ÿ‡ญ๐Ÿ‡ณ'}, {name: 'Hong Kong', iso2: CountryISO.HongKong, dialCode: '852', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ'}, - {name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '๐Ÿ‡ญ๐Ÿ‡บ'}, + {name: 'Hungary', iso2: CountryISO.Hungary, dialCode: '36', flag: '๐Ÿ‡ญ๐Ÿ‡บ', postCodePattern: /[0-9]{4}/}, {name: 'Iceland', iso2: CountryISO.Iceland, dialCode: '354', flag: '๐Ÿ‡ฎ๐Ÿ‡ธ'}, {name: 'India', iso2: CountryISO.India, dialCode: '91', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ'}, {name: 'Indonesia', iso2: CountryISO.Indonesia, dialCode: '62', flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ'}, - {name: 'Iran', iso2: CountryISO.Iran, dialCode: '98', flag: '๐Ÿ‡ฎ๐Ÿ‡ท'}, + {name: 'Islamic Republic of Iran', iso2: CountryISO.Iran, dialCode: '98', flag: '๐Ÿ‡ฎ๐Ÿ‡ท'}, {name: 'Iraq', iso2: CountryISO.Iraq, dialCode: '964', flag: '๐Ÿ‡ฎ๐Ÿ‡ถ'}, {name: 'Ireland', iso2: CountryISO.Ireland, dialCode: '353', flag: '๐Ÿ‡ฎ๐Ÿ‡ช'}, {name: 'Isle of Man', iso2: CountryISO.IsleOfMan, dialCode: '44', flag: '๐Ÿ‡ฎ๐Ÿ‡ฒ'}, {name: 'Israel', iso2: CountryISO.Israel, dialCode: '972', flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ'}, - {name: 'Italy', iso2: CountryISO.Italy, dialCode: '39', flag: '๐Ÿ‡ฎ๐Ÿ‡น'}, + {name: 'Italy', iso2: CountryISO.Italy, dialCode: '39', flag: '๐Ÿ‡ฎ๐Ÿ‡น', postCodePattern: /[0-9]{5}/}, {name: 'Jamaica', iso2: CountryISO.Jamaica, dialCode: '1', flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ'}, - {name: 'Japan', iso2: CountryISO.Japan, dialCode: '81', flag: '๐Ÿ‡ฏ๐Ÿ‡ต'}, + {name: 'Japan', iso2: CountryISO.Japan, dialCode: '81', flag: '๐Ÿ‡ฏ๐Ÿ‡ต', postCodePattern: /\d{3}-\d{4}/}, {name: 'Jersey', iso2: CountryISO.Jersey, dialCode: '44', flag: '๐Ÿ‡ฏ๐Ÿ‡ช'}, {name: 'Jordan', iso2: CountryISO.Jordan, dialCode: '962', flag: '๐Ÿ‡ฏ๐Ÿ‡ด'}, {name: 'Kazakhstan', iso2: CountryISO.Kazakhstan, dialCode: '7', flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ'}, @@ -400,7 +402,7 @@ export class CountryData { {name: 'Kosovo', iso2: CountryISO.Kosovo, dialCode: '383', flag: '๐Ÿ‡ฝ๐Ÿ‡ฐ'}, {name: 'Kuwait', iso2: CountryISO.Kuwait, dialCode: '965', flag: '๐Ÿ‡ฐ๐Ÿ‡ผ'}, {name: 'Kyrgyzstan', iso2: CountryISO.Kyrgyzstan, dialCode: '996', flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ'}, - {name: 'Laos', iso2: CountryISO.Laos, dialCode: '856', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ'}, + {name: 'Lao People\'s Democratic Republic', iso2: CountryISO.Laos, dialCode: '856', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ'}, {name: 'Latvia', iso2: CountryISO.Latvia, dialCode: '371', flag: '๐Ÿ‡ฑ๐Ÿ‡ป'}, {name: 'Lebanon', iso2: CountryISO.Lebanon, dialCode: '961', flag: '๐Ÿ‡ฑ๐Ÿ‡ง'}, {name: 'Lesotho', iso2: CountryISO.Lesotho, dialCode: '266', flag: '๐Ÿ‡ฑ๐Ÿ‡ธ'}, @@ -408,9 +410,8 @@ export class CountryData { {name: 'Libya', iso2: CountryISO.Libya, dialCode: '218', flag: '๐Ÿ‡ฑ๐Ÿ‡พ'}, {name: 'Liechtenstein', iso2: CountryISO.Liechtenstein, dialCode: '423', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ'}, {name: 'Lithuania', iso2: CountryISO.Lithuania, dialCode: '370', flag: '๐Ÿ‡ฑ๐Ÿ‡น'}, - {name: 'Luxembourg', iso2: CountryISO.Luxembourg, dialCode: '352', flag: '๐Ÿ‡ฑ๐Ÿ‡บ'}, - {name: 'Macau', iso2: CountryISO.Macau, dialCode: '853', flag: '๐Ÿ‡ฒ๐Ÿ‡ด'}, - {name: 'Macedonia', iso2: CountryISO.Macedonia, dialCode: '389', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ'}, + {name: 'Luxembourg', iso2: CountryISO.Luxembourg, dialCode: '352', flag: '๐Ÿ‡ฑ๐Ÿ‡บ', postCodePattern: /(L\s*([-โ€”โ€“]))\s*?\d{4}/}, + {name: 'Macao', iso2: CountryISO.Macau, dialCode: '853', flag: '๐Ÿ‡ฒ๐Ÿ‡ด'}, {name: 'Madagascar', iso2: CountryISO.Madagascar, dialCode: '261', flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ'}, {name: 'Malawi', iso2: CountryISO.Malawi, dialCode: '265', flag: '๐Ÿ‡ฒ๐Ÿ‡ผ'}, {name: 'Malaysia', iso2: CountryISO.Malaysia, dialCode: '60', flag: '๐Ÿ‡ฒ๐Ÿ‡พ'}, @@ -423,8 +424,8 @@ export class CountryData { {name: 'Mauritius', iso2: CountryISO.Mauritius, dialCode: '230', flag: '๐Ÿ‡ฒ๐Ÿ‡บ'}, {name: 'Mayotte', iso2: CountryISO.Mayotte, dialCode: '262', flag: '๐Ÿ‡พ๐Ÿ‡น'}, {name: 'Mexico', iso2: CountryISO.Mexico, dialCode: '52', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ'}, - {name: 'Micronesia', iso2: CountryISO.Micronesia, dialCode: '691', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ'}, - {name: 'Moldova', iso2: CountryISO.Moldova, dialCode: '373', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ'}, + {name: 'Micronesia, Federated States of', iso2: CountryISO.Micronesia, dialCode: '691', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ'}, + {name: 'Moldova, Republic of', iso2: CountryISO.Moldova, dialCode: '373', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ'}, {name: 'Monaco', iso2: CountryISO.Monaco, dialCode: '377', flag: '๐Ÿ‡ฒ๐Ÿ‡จ'}, {name: 'Mongolia', iso2: CountryISO.Mongolia, dialCode: '976', flag: '๐Ÿ‡ฒ๐Ÿ‡ณ'}, {name: 'Montenegro', iso2: CountryISO.Montenegro, dialCode: '382', flag: '๐Ÿ‡ฒ๐Ÿ‡ช'}, @@ -435,7 +436,7 @@ export class CountryData { {name: 'Namibia', iso2: CountryISO.Namibia, dialCode: '264', flag: '๐Ÿ‡ณ๐Ÿ‡ฆ'}, {name: 'Nauru', iso2: CountryISO.Nauru, dialCode: '674', flag: '๐Ÿ‡ณ๐Ÿ‡ท'}, {name: 'Nepal', iso2: CountryISO.Nepal, dialCode: '977', flag: '๐Ÿ‡ณ๐Ÿ‡ต'}, - {name: 'Netherlands', iso2: CountryISO.Netherlands, dialCode: '31', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ'}, + {name: 'Netherlands', iso2: CountryISO.Netherlands, dialCode: '31', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ', postCodePattern: /[1-9][0-9]{3}\s?[a-zA-Z]{2}/}, {name: 'New Caledonia', iso2: CountryISO.NewCaledonia, dialCode: '687', flag: '๐Ÿ‡ณ๐Ÿ‡จ'}, {name: 'New Zealand', iso2: CountryISO.NewZealand, dialCode: '64', flag: '๐Ÿ‡ณ๐Ÿ‡ฟ'}, {name: 'Nicaragua', iso2: CountryISO.Nicaragua, dialCode: '505', flag: '๐Ÿ‡ณ๐Ÿ‡ฎ'}, @@ -444,43 +445,44 @@ export class CountryData { {name: 'Niue', iso2: CountryISO.Niue, dialCode: '683', flag: '๐Ÿ‡ณ๐Ÿ‡บ'}, {name: 'Norfolk Island', iso2: CountryISO.NorfolkIsland, dialCode: '672', flag: '๐Ÿ‡ณ๐Ÿ‡ซ'}, {name: 'North Korea', iso2: CountryISO.NorthKorea, dialCode: '850', flag: '๐Ÿ‡ฐ๐Ÿ‡ต'}, + {name: 'North Macedonia', iso2: CountryISO.NorthMacedonia, dialCode: '389', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ'}, {name: 'Northern Mariana Islands', iso2: CountryISO.NorthernMarianaIslands, dialCode: '1', flag: '๐Ÿ‡ฒ๐Ÿ‡ต'}, {name: 'Norway', iso2: CountryISO.Norway, dialCode: '47', flag: '๐Ÿ‡ณ๐Ÿ‡ด'}, {name: 'Oman', iso2: CountryISO.Oman, dialCode: '968', flag: '๐Ÿ‡ด๐Ÿ‡ฒ'}, {name: 'Pakistan', iso2: CountryISO.Pakistan, dialCode: '92', flag: '๐Ÿ‡ต๐Ÿ‡ฐ'}, {name: 'Palau', iso2: CountryISO.Palau, dialCode: '680', flag: '๐Ÿ‡ต๐Ÿ‡ผ'}, - {name: 'Palestine', iso2: CountryISO.Palestine, dialCode: '970', flag: '๐Ÿ‡ต๐Ÿ‡ธ'}, + {name: 'Palestine, State of', iso2: CountryISO.Palestine, dialCode: '970', flag: '๐Ÿ‡ต๐Ÿ‡ธ'}, {name: 'Panama', iso2: CountryISO.Panama, dialCode: '507', flag: '๐Ÿ‡ต๐Ÿ‡ฆ'}, {name: 'Papua New Guinea', iso2: CountryISO.PapuaNewGuinea, dialCode: '675', flag: '๐Ÿ‡ต๐Ÿ‡ฌ'}, {name: 'Paraguay', iso2: CountryISO.Paraguay, dialCode: '595', flag: '๐Ÿ‡ต๐Ÿ‡พ'}, {name: 'Peru', iso2: CountryISO.Peru, dialCode: '51', flag: '๐Ÿ‡ต๐Ÿ‡ช'}, {name: 'Philippines', iso2: CountryISO.Philippines, dialCode: '63', flag: '๐Ÿ‡ต๐Ÿ‡ญ'}, {name: 'Pitcairn Islands', iso2: CountryISO.PitcairnIslands, dialCode: '649', flag: '๐Ÿ‡ต๐Ÿ‡ณ'}, - {name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ'}, + {name: 'Poland', iso2: CountryISO.Poland, dialCode: '48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ', postCodePattern: /[0-9]{2}-[0-9]{3}/}, {name: 'Portugal', iso2: CountryISO.Portugal, dialCode: '351', flag: '๐Ÿ‡ต๐Ÿ‡น'}, {name: 'Puerto Rico', iso2: CountryISO.PuertoRico, dialCode: '1', flag: '๐Ÿ‡ต๐Ÿ‡ท'}, {name: 'Qatar', iso2: CountryISO.Qatar, dialCode: '974', flag: '๐Ÿ‡ถ๐Ÿ‡ฆ'}, - {name: 'Rรฉunion', iso2: CountryISO.Rรฉunion, dialCode: '262', flag: '๐Ÿ‡ท๐Ÿ‡ช'}, + {name: 'Rรฉunion', iso2: CountryISO.Reunion, dialCode: '262', flag: '๐Ÿ‡ท๐Ÿ‡ช'}, {name: 'Romania', iso2: CountryISO.Romania, dialCode: '40', flag: '๐Ÿ‡ท๐Ÿ‡ด'}, - {name: 'Russia', iso2: CountryISO.Russia, dialCode: '7', flag: '๐Ÿ‡ท๐Ÿ‡บ'}, + {name: 'Russian Federation', iso2: CountryISO.Russia, dialCode: '7', flag: '๐Ÿ‡ท๐Ÿ‡บ'}, {name: 'Rwanda', iso2: CountryISO.Rwanda, dialCode: '250', flag: '๐Ÿ‡ท๐Ÿ‡ผ'}, - {name: 'Saint Barthรฉlemy', iso2: CountryISO.SaintBarthรฉlemy, dialCode: '590', flag: '๐Ÿ‡ง๐Ÿ‡ฑ'}, - {name: 'Saint Helena', iso2: CountryISO.SaintHelena, dialCode: '290', flag: '๐Ÿ‡ธ๐Ÿ‡ญ'}, + {name: 'Saint Barthรฉlemy', iso2: CountryISO.SaintBarthelemy, dialCode: '590', flag: '๐Ÿ‡ง๐Ÿ‡ฑ'}, + {name: 'Saint Helena, Ascension and Tristan da Cunha', iso2: CountryISO.SaintHelena, dialCode: '290', flag: '๐Ÿ‡ธ๐Ÿ‡ญ'}, {name: 'Saint Kitts and Nevis', iso2: CountryISO.SaintKittsAndNevis, dialCode: '1', flag: '๐Ÿ‡ฐ๐Ÿ‡ณ'}, {name: 'Saint Lucia', iso2: CountryISO.SaintLucia, dialCode: '1', flag: '๐Ÿ‡ฑ๐Ÿ‡จ'}, - {name: 'Saint Martin', iso2: CountryISO.SaintMartin, dialCode: '590', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ'}, + {name: 'Saint Martin (French part)', iso2: CountryISO.SaintMartin, dialCode: '590', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ'}, {name: 'Saint Pierre and Miquelon', iso2: CountryISO.SaintPierreAndMiquelon, dialCode: '508', flag: '๐Ÿ‡ต๐Ÿ‡ฒ'}, {name: 'Saint Vincent and the Grenadines', iso2: CountryISO.SaintVincentAndTheGrenadines, dialCode: '1', flag: '๐Ÿ‡ป๐Ÿ‡จ'}, {name: 'Samoa', iso2: CountryISO.Samoa, dialCode: '685', flag: '๐Ÿ‡ผ๐Ÿ‡ธ'}, {name: 'San Marino', iso2: CountryISO.SanMarino, dialCode: '378', flag: '๐Ÿ‡ธ๐Ÿ‡ฒ'}, - {name: 'Sรฃo Tomรฉ and Prรญncipe', iso2: CountryISO.SรฃoTomรฉAndPrรญncipe, dialCode: '239', flag: '๐Ÿ‡ธ๐Ÿ‡น'}, + {name: 'Sao Tome and Principe', iso2: CountryISO.SaoTomeAndPrincipe, dialCode: '239', flag: '๐Ÿ‡ธ๐Ÿ‡น'}, {name: 'Saudi Arabia', iso2: CountryISO.SaudiArabia, dialCode: '966', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ'}, {name: 'Senegal', iso2: CountryISO.Senegal, dialCode: '221', flag: '๐Ÿ‡ธ๐Ÿ‡ณ'}, {name: 'Serbia', iso2: CountryISO.Serbia, dialCode: '381', flag: '๐Ÿ‡ท๐Ÿ‡ธ'}, {name: 'Seychelles', iso2: CountryISO.Seychelles, dialCode: '248', flag: '๐Ÿ‡ธ๐Ÿ‡จ'}, {name: 'Sierra Leone', iso2: CountryISO.SierraLeone, dialCode: '232', flag: '๐Ÿ‡ธ๐Ÿ‡ฑ'}, {name: 'Singapore', iso2: CountryISO.Singapore, dialCode: '65', flag: '๐Ÿ‡ธ๐Ÿ‡ฌ'}, - {name: 'Sint Maarten', iso2: CountryISO.SintMaarten, dialCode: '1', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ'}, + {name: 'Sint Maarten (Dutch Part)', iso2: CountryISO.SintMaarten, dialCode: '1', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ'}, {name: 'Slovakia', iso2: CountryISO.Slovakia, dialCode: '421', flag: '๐Ÿ‡ธ๐Ÿ‡ฐ'}, {name: 'Slovenia', iso2: CountryISO.Slovenia, dialCode: '386', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ'}, {name: 'Solomon Islands', iso2: CountryISO.SolomonIslands, dialCode: '677', flag: '๐Ÿ‡ธ๐Ÿ‡ง'}, @@ -490,18 +492,18 @@ export class CountryData { iso2: CountryISO.SouthGeorgiaAndTheSouthSandwichIslands, dialCode: '500', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ'}, {name: 'South Korea', iso2: CountryISO.SouthKorea, dialCode: '82', flag: '๐Ÿ‡ฐ๐Ÿ‡ท'}, {name: 'South Sudan', iso2: CountryISO.SouthSudan, dialCode: '211', flag: '๐Ÿ‡ธ๐Ÿ‡ธ'}, - {name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '๐Ÿ‡ช๐Ÿ‡ธ'}, + {name: 'Spain', iso2: CountryISO.Spain, dialCode: '34', flag: '๐Ÿ‡ช๐Ÿ‡ธ', postCodePattern: /((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}/}, {name: 'Sri Lanka', iso2: CountryISO.SriLanka, dialCode: '94', flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ'}, {name: 'Sudan', iso2: CountryISO.Sudan, dialCode: '249', flag: '๐Ÿ‡ธ๐Ÿ‡ฉ'}, - {name: 'Suriname: ', iso2: CountryISO.Suriname, dialCode: '597', flag: '๐Ÿ‡ธ๐Ÿ‡ท'}, + {name: 'Suriname', iso2: CountryISO.Suriname, dialCode: '597', flag: '๐Ÿ‡ธ๐Ÿ‡ท'}, {name: 'Svalbard and Jan Mayen', iso2: CountryISO.SvalbardAndJanMayen, dialCode: '47', flag: '๐Ÿ‡ธ๐Ÿ‡ฏ'}, {name: 'Swaziland', iso2: CountryISO.Swaziland, dialCode: '268', flag: '๐Ÿ‡ธ๐Ÿ‡ฟ'}, - {name: 'Sweden', iso2: CountryISO.Sweden, dialCode: '46', flag: '๐Ÿ‡ธ๐Ÿ‡ช'}, + {name: 'Sweden', iso2: CountryISO.Sweden, dialCode: '46', flag: '๐Ÿ‡ธ๐Ÿ‡ช', postCodePattern: /\d{3}\s?\d{2}/}, {name: 'Switzerland', iso2: CountryISO.Switzerland, dialCode: '41', flag: '๐Ÿ‡จ๐Ÿ‡ญ'}, - {name: 'Syria', iso2: CountryISO.Syria, dialCode: '963', flag: '๐Ÿ‡ธ๐Ÿ‡พ'}, + {name: 'Syrian Arab Republic', iso2: CountryISO.Syria, dialCode: '963', flag: '๐Ÿ‡ธ๐Ÿ‡พ'}, {name: 'Taiwan', iso2: CountryISO.Taiwan, dialCode: '886', flag: '๐Ÿ‡น๐Ÿ‡ผ'}, {name: 'Tajikistan', iso2: CountryISO.Tajikistan, dialCode: '992', flag: '๐Ÿ‡น๐Ÿ‡ฏ'}, - {name: 'Tanzania', iso2: CountryISO.Tanzania, dialCode: '255', flag: '๐Ÿ‡น๐Ÿ‡ฟ'}, + {name: 'Tanzania, United Republic of', iso2: CountryISO.Tanzania, dialCode: '255', flag: '๐Ÿ‡น๐Ÿ‡ฟ'}, {name: 'Thailand', iso2: CountryISO.Thailand, dialCode: '66', flag: '๐Ÿ‡น๐Ÿ‡ญ'}, {name: 'Timor-Leste', iso2: CountryISO.TimorLeste, dialCode: '670', flag: '๐Ÿ‡น๐Ÿ‡ฑ'}, {name: 'Togo', iso2: CountryISO.Togo, dialCode: '228', flag: '๐Ÿ‡น๐Ÿ‡ฌ'}, @@ -517,8 +519,8 @@ export class CountryData { {name: 'Uganda', iso2: CountryISO.Uganda, dialCode: '256', flag: '๐Ÿ‡บ๐Ÿ‡ฌ'}, {name: 'Ukraine', iso2: CountryISO.Ukraine, dialCode: '380', flag: '๐Ÿ‡บ๐Ÿ‡ฆ'}, {name: 'United Arab Emirates', iso2: CountryISO.UnitedArabEmirates, dialCode: '971', flag: '๐Ÿ‡ฆ๐Ÿ‡ช'}, - {name: 'United Kingdom', iso2: CountryISO.UnitedKingdom, dialCode: '44', flag: '๐Ÿ‡ฌ๐Ÿ‡ง'}, - {name: 'United States', iso2: CountryISO.UnitedStates, dialCode: '1', flag: '๐Ÿ‡บ๐Ÿ‡ธ'}, + {name: 'United Kingdom', iso2: CountryISO.UnitedKingdom, dialCode: '44', flag: '๐Ÿ‡ฌ๐Ÿ‡ง', postCodePattern: /[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}/}, + {name: 'United States', iso2: CountryISO.UnitedStates, dialCode: '1', flag: '๐Ÿ‡บ๐Ÿ‡ธ', postCodePattern: /(\d{5}(-\d{4})?)/}, {name: 'Uruguay', iso2: CountryISO.Uruguay, dialCode: '598', flag: '๐Ÿ‡บ๐Ÿ‡พ'}, {name: 'Uzbekistan', iso2: CountryISO.Uzbekistan, dialCode: '998', flag: '๐Ÿ‡บ๐Ÿ‡ฟ'}, {name: 'Vanuatu', iso2: CountryISO.Vanuatu, dialCode: '678', flag: '๐Ÿ‡ป๐Ÿ‡บ'}, @@ -530,6 +532,7 @@ export class CountryData { {name: 'Yemen', iso2: CountryISO.Yemen, dialCode: '967', flag: '๐Ÿ‡พ๐Ÿ‡ช'}, {name: 'Zambia', iso2: CountryISO.Zambia, dialCode: '260', flag: '๐Ÿ‡ฟ๐Ÿ‡ฒ'}, {name: 'Zimbabwe', iso2: CountryISO.Zimbabwe, dialCode: '263', flag: '๐Ÿ‡ฟ๐Ÿ‡ผ'}, - {name: 'ร…land Islands', iso2: CountryISO.ร…landIslands, dialCode: '358', flag: '๐Ÿ‡ฆ๐Ÿ‡ฝ'} + {name: 'ร…land Islands', iso2: CountryISO.AlandIslands, dialCode: '358', flag: '๐Ÿ‡ฆ๐Ÿ‡ฝ'} ]; } +/* eslint-enable max-len */ diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 3d4edacd1d..21c912edf5 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -222,6 +222,7 @@ import { WidgetButtonComponent } from '@shared/components/button/widget-button.c import { HexInputComponent } from '@shared/components/color-picker/hex-input.component'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component'; +import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -383,6 +384,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TogglePasswordComponent, ProtobufContentComponent, BranchAutocompleteComponent, + CountryAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, NotificationComponent, @@ -640,6 +642,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TogglePasswordComponent, ProtobufContentComponent, BranchAutocompleteComponent, + CountryAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, NotificationComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d9259eec78..26c955c6ea 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -992,6 +992,7 @@ }, "contact": { "country": "Country", + "country-required": "Country is required.", "city": "City", "state": "State / Province", "postal-code": "Zip / Postal Code", @@ -1001,6 +1002,8 @@ "phone": "Phone", "email": "Email", "no-address": "No address", + "no-country-found": "No countries found.", + "no-country-matching": "No country matching '{{country}}' were found.", "state-max-length": "State length should be less than 256", "phone-max-length": "Phone number should be less than 256", "city-max-length": "Specified city should be less than 256" From 0af0c3667378be8ba180786ae121e5b11d8ff084 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 2 Sep 2024 10:41:17 +0200 Subject: [PATCH 53/80] refactoring --- .../thingsboard/server/service/ws/WebSocketSessionRef.java | 5 +++++ .../ws/notification/DefaultNotificationCommandsHandler.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java index 44919b138c..7ce7bd5735 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.ws; import lombok.Builder; import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.security.model.SecurityUser; import java.net.InetSocketAddress; @@ -39,6 +40,10 @@ public class WebSocketSessionRef { private final WebSocketSessionType sessionType; private final AtomicInteger sessionSubIdSeq = new AtomicInteger(); + public TenantId getTenantId() { + return securityCtx != null ? securityCtx.getTenantId() : TenantId.SYS_TENANT_ID; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index 84f3698845..77f000990c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -242,7 +242,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH @Override public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { - localSubscriptionService.cancelSubscription(sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSessionId(), cmd.getCmdId()); + localSubscriptionService.cancelSubscription(sessionRef.getTenantId(), sessionRef.getSessionId(), cmd.getCmdId()); } private void sendUpdate(String sessionId, CmdUpdate update) { From 73ae4b3c666d3bc257276a5354dacd5d0df2ee72 Mon Sep 17 00:00:00 2001 From: Chantsova Ekaterina Date: Mon, 2 Sep 2024 12:24:58 +0300 Subject: [PATCH 54/80] UI: Image service - change parameters order to keep backward compatibility with older version --- ui-ngx/src/app/core/http/image.service.ts | 4 ++-- .../app/shared/components/image/image-gallery.component.ts | 2 +- ui-ngx/src/app/shared/components/image/images-datasource.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts index 68c128028f..835e8ffb61 100644 --- a/ui-ngx/src/app/core/http/image.service.ts +++ b/ui-ngx/src/app/core/http/image.service.ts @@ -87,8 +87,8 @@ export class ImageService { imageInfo, defaultHttpOptionsFromConfig(config)); } - public getImages(pageLink: PageLink, imageSubType: ResourceSubType = ResourceSubType.IMAGE, - includeSystemImages = false, config?: RequestConfig): Observable> { + public getImages(pageLink: PageLink, includeSystemImages = false, + imageSubType: ResourceSubType = ResourceSubType.IMAGE, config?: RequestConfig): Observable> { return this.http.get>( `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&imageSubType=${imageSubType}&includeSystemImages=${includeSystemImages}`, defaultHttpOptionsFromConfig(config)); diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts index 5b50282ea7..23f489d78c 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts @@ -205,7 +205,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe property: 'createdTime', direction: Direction.DESC }); - return this.imageService.getImages(pageLink, this.imageSubType, filter.includeSystemImages); + return this.imageService.getImages(pageLink, filter.includeSystemImages, this.imageSubType); }; } diff --git a/ui-ngx/src/app/shared/components/image/images-datasource.ts b/ui-ngx/src/app/shared/components/image/images-datasource.ts index 3a46508d45..78f02df4b1 100644 --- a/ui-ngx/src/app/shared/components/image/images-datasource.ts +++ b/ui-ngx/src/app/shared/components/image/images-datasource.ts @@ -84,7 +84,7 @@ export class ImagesDatasource implements DataSource { } fetchEntities(pageLink: PageLink, imageSubType: ResourceSubType, includeSystemImages = false): Observable> { - return this.imageService.getImages(pageLink, imageSubType, includeSystemImages); + return this.imageService.getImages(pageLink, includeSystemImages, imageSubType); } isAllSelected(): Observable { From ff01f77f9e60c47dac7af090a7897d8c746943b7 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 2 Sep 2024 15:27:42 +0300 Subject: [PATCH 55/80] UI: Added additionEntityTypes in entity-select.component and delete CURRENT_RULE_NODE --- .../entity/entity-autocomplete.component.ts | 2 - .../entity/entity-select.component.html | 3 +- .../entity/entity-select.component.ts | 24 ++++++----- .../entity/entity-type-select.component.ts | 40 +++++++++---------- .../app/shared/models/entity-type.models.ts | 10 +---- .../assets/locale/locale.constant-en_US.json | 1 - 6 files changed, 36 insertions(+), 44 deletions(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 071559f234..b55f01a7ae 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -414,8 +414,6 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } else { return EntityType.CUSTOMER; } - } else if (entityType === AliasEntityType.CURRENT_RULE_NODE) { - return EntityType.RULE_NODE; } return entityType; } diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index b4a3a33029..7f5c127e00 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -22,10 +22,11 @@ [required]="required" [useAliasEntityTypes]="useAliasEntityTypes" [allowedEntityTypes]="allowedEntityTypes" + [additionEntityTypes]="additionEntityTypes" formControlName="entityType"> diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index eff7f5306f..5671e9de3b 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -22,8 +22,8 @@ import { TranslateService } from '@ngx-translate/core'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; import { EntityService } from '@core/http/entity.service'; import { EntityId } from '@shared/models/id/entity-id'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-entity-select', @@ -47,24 +47,22 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte @Input() useAliasEntityTypes: boolean; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() disabled: boolean; + @Input() + additionEntityTypes: {[entityType in string]: string} = {}; + displayEntityTypeSelect: boolean; AliasEntityType = AliasEntityType; - entityTypeNullUid: Set = new Set([ - AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER, AliasEntityType.CURRENT_RULE_NODE + entityTypeNullUUID: Set = new Set([ + AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER ]); private readonly defaultEntityType: EntityType | AliasEntityType = null; @@ -110,6 +108,10 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte this.updateView(this.modelValue.entityType, id); } ); + const additionNullUIIDEntityTypes = Object.keys(this.additionEntityTypes) as string[]; + if (additionNullUIIDEntityTypes.length > 0) { + additionNullUIIDEntityTypes.forEach((entityType) => this.entityTypeNullUUID.add(entityType)); + } } ngAfterViewInit(): void { @@ -147,7 +149,7 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte id: this.modelValue.entityType !== entityType ? null : entityId }; - if (this.entityTypeNullUid.has(this.modelValue.entityType)) { + if (this.entityTypeNullUUID.has(this.modelValue.entityType)) { this.modelValue.id = NULL_UUID; } else if (this.modelValue.entityType === AliasEntityType.CURRENT_CUSTOMER && !this.modelValue.id) { this.modelValue.id = NULL_UUID; diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index 3b0dca6e81..2405a269b6 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -15,13 +15,13 @@ /// import { AfterViewInit, Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared/models/entity-type.models'; import { EntityService } from '@core/http/entity.service'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-entity-type-select', @@ -48,28 +48,21 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @Input() filterAllowedEntityTypes = true; - private showLabelValue: boolean; - get showLabel(): boolean { - return this.showLabelValue; - } @Input() - set showLabel(value: boolean) { - this.showLabelValue = coerceBooleanProperty(value); - } + @coerceBoolean() + showLabel: boolean; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() disabled: boolean; - entityTypes: Array; + @Input() + additionEntityTypes: {[key in string]: string} = {}; + + entityTypes: Array; private propagateChange = (v: any) => { }; @@ -93,8 +86,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, this.entityTypes = this.filterAllowedEntityTypes ? this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes; - if (this.allowedEntityTypes?.length && this.allowedEntityTypes.includes(AliasEntityType.CURRENT_RULE_NODE)) { - this.entityTypes.push(AliasEntityType.CURRENT_RULE_NODE); + const additionEntityTypes = Object.keys(this.additionEntityTypes); + if (additionEntityTypes.length > 0) { + this.entityTypes.push(...additionEntityTypes); } this.entityTypeFormGroup.get('entityType').valueChanges.subscribe( (value) => { @@ -116,6 +110,10 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, if (propName === 'allowedEntityTypes') { this.entityTypes = this.filterAllowedEntityTypes ? this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes) : this.allowedEntityTypes; + const additionEntityTypes = Object.keys(this.additionEntityTypes); + if (additionEntityTypes.length > 0) { + this.entityTypes.push(...additionEntityTypes); + } const currentEntityType: EntityType | AliasEntityType = this.entityTypeFormGroup.get('entityType').value; if (currentEntityType && !this.entityTypes.includes(currentEntityType)) { this.entityTypeFormGroup.get('entityType').patchValue(null, {emitEvent: true}); @@ -155,7 +153,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, } displayEntityTypeFn(entityType?: EntityType | AliasEntityType | null): string | undefined { - if (entityType) { + if (this.additionEntityTypes[entityType]) { + return this.additionEntityTypes[entityType]; + } else if (entityType) { return this.translate.instant(entityTypeTranslations.get(entityType as EntityType).type); } else { return ''; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index f2467b9736..51fefefdf2 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -54,8 +54,7 @@ export enum AliasEntityType { CURRENT_CUSTOMER = 'CURRENT_CUSTOMER', CURRENT_TENANT = 'CURRENT_TENANT', CURRENT_USER = 'CURRENT_USER', - CURRENT_USER_OWNER = 'CURRENT_USER_OWNER', - CURRENT_RULE_NODE = 'CURRENT_RULE_NODE' + CURRENT_USER_OWNER = 'CURRENT_USER_OWNER' } export interface EntityTypeTranslation { @@ -327,13 +326,6 @@ export const entityTypeTranslations = new Map Date: Mon, 2 Sep 2024 15:32:00 +0300 Subject: [PATCH 56/80] UI: Clear code --- .../app/shared/components/entity/entity-select.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index 7f5c127e00..676294f344 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
Date: Mon, 2 Sep 2024 16:46:04 +0300 Subject: [PATCH 57/80] translation small fix --- .../basic/gateway-basic-configuration.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html index b515d1baeb..9c67bd2eaf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html @@ -725,7 +725,7 @@ - {{ 'gateway.min-pack-send-delay-min-pattern' | translate }} + {{ 'gateway.min-pack-send-delay-pattern' | translate }} info_outlined From 5f112b211205a1624c5a8f6bd938524a4e4d9899 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 2 Sep 2024 16:53:45 +0300 Subject: [PATCH 58/80] UI: Add leak sensor and water stop symbol --- .../json/system/scada_symbols/leak-sensor.svg | 158 ++++++++++ .../json/system/scada_symbols/waterstop.svg | 276 ++++++++++++++++++ .../scada_water_system_symbols.json | 2 + .../assets/locale/locale.constant-en_US.json | 5 +- 4 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 application/src/main/data/json/system/scada_symbols/leak-sensor.svg create mode 100644 application/src/main/data/json/system/scada_symbols/waterstop.svg diff --git a/application/src/main/data/json/system/scada_symbols/leak-sensor.svg b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg new file mode 100644 index 0000000000..506385d7fb --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg @@ -0,0 +1,158 @@ + +{ + "title": "Leak sensor", + "description": "Leak sensor", + "searchTags": [ + "leak" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "tags": [ + { + "tag": "background", + "stateRenderFunction": "var fill = ctx.properties.backgroundColor;\nelement.attr({fill: fill});", + "actions": null + }, + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'click');" + } + } + }, + { + "tag": "icon", + "stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.attr({stroke: ctx.properties.leakColor});\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n} else {\n element.attr({stroke: ctx.properties.defaultColor});\n ctx.api.resetAnimation(element);\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "leak", + "name": "{i18n:scada.symbol.leak}", + "hint": "{i18n:scada.symbol.water-leak-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "GET_TIME_SERIES", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "leak" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "click", + "name": "{i18n:scada.symbol.on-click}", + "hint": "{i18n:scada.symbol.on-click-hint}", + "group": null, + "type": "widgetAction", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": { + "type": "doNothing", + "targetDashboardStateId": null, + "openRightLayout": false, + "setEntityId": false, + "stateEntityParamName": null + } + } + ], + "properties": [ + { + "id": "backgroundColor", + "name": "{i18n:scada.symbol.background-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "defaultColor", + "name": "{i18n:scada.symbol.default-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "leakColor", + "name": "{i18n:scada.symbol.leak-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/waterstop.svg b/application/src/main/data/json/system/scada_symbols/waterstop.svg new file mode 100644 index 0000000000..e1ed0e0fd5 --- /dev/null +++ b/application/src/main/data/json/system/scada_symbols/waterstop.svg @@ -0,0 +1,276 @@ + +{ + "title": "Water stop", + "description": "Remotely controlled water shutoff valve with configurable connectors and various states.", + "searchTags": [ + "water stop" + ], + "widgetSizeX": 1, + "widgetSizeY": 1, + "stateRenderFunction": "", + "tags": [ + { + "tag": "clickArea", + "stateRenderFunction": null, + "actions": { + "click": { + "actionFunction": "var opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});" + } + } + }, + { + "tag": "pipe-color", + "stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});", + "actions": null + }, + { + "tag": "walve", + "stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n", + "actions": null + } + ], + "behavior": [ + { + "id": "opened", + "name": "{i18n:scada.symbol.opened}", + "hint": "{i18n:scada.symbol.opened-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.opened}", + "defaultGetValueSettings": { + "action": "GET_ATTRIBUTE", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "open", + "name": "{i18n:scada.symbol.open}", + "hint": "{i18n:scada.symbol.open-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": false, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "close", + "name": "{i18n:scada.symbol.close}", + "hint": "{i18n:scada.symbol.close-hint}", + "group": null, + "type": "action", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": null, + "defaultSetValueSettings": { + "action": "SET_ATTRIBUTE", + "executeRpc": { + "method": "setState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "setAttribute": { + "scope": "SHARED_SCOPE", + "key": "open" + }, + "putTimeSeries": { + "key": "state" + }, + "valueToData": { + "type": "CONSTANT", + "constantValue": true, + "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;" + } + }, + "defaultWidgetActionSettings": null + }, + { + "id": "criticalAnimation", + "name": "{i18n:scada.symbol.critical-state-animation}", + "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": null, + "falseLabel": null, + "stateLabel": "{i18n:scada.symbol.animation}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], + "properties": [ + { + "id": "openedColor", + "name": "{i18n:scada.symbol.opened-color}", + "type": "color", + "default": "#1C943E", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "closedColor", + "name": "{i18n:scada.symbol.closed-color}", + "type": "color", + "default": "#D12730", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + }, + { + "id": "pipeColor", + "name": "{i18n:scada.symbol.pipe-color}", + "type": "color", + "default": "#FFFFFF", + "required": null, + "subLabel": null, + "divider": null, + "fieldSuffix": null, + "disableOnProperty": null, + "rowClass": "", + "fieldClass": "", + "min": null, + "max": null, + "step": null + } + ] +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json index 6f0edcd02f..619e336daf 100644 --- a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json @@ -34,6 +34,7 @@ "left_flow_meter", "horizontal_inline_flow_meter", "vertical_inline_flow_meter", + "leak_sensor", "centrifugal_pump", "small_right_motor_pump", "small_left_motor_pump", @@ -50,6 +51,7 @@ "vertical_wheel_valve", "horizontal_ball_valve", "vertical_ball_valve", + "water_stop", "vertical_tank", "stand_vertical_tank", "cylindrical_tank", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d9259eec78..cc28a4edd3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3875,7 +3875,10 @@ "stand-filter-color": "Stand filter color", "mode-box-background": "Mode box background", "border-color": "Border color", - "label-color": "Label color" + "label-color": "Label color", + "water-leak-hint": "Indicates whether a leak.", + "default-color": "Default color", + "leak-color": "Leak color" } }, "item": { From b86a9d3802ec6a4bb25fcb5f370b99079d33383e Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 2 Sep 2024 21:44:23 +0300 Subject: [PATCH 59/80] tbel: refactoring tbUtils with docs --- .../thingsboard/script/api/tbel/TbUtils.java | 28 +++--- .../script/api/tbel/TbUtilsTest.java | 85 ++++++++++++------- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index d188111cc3..6651f26cc5 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -273,13 +273,13 @@ public class TbUtils { Long.class, boolean.class, boolean.class))); parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex", Long.class, boolean.class, boolean.class, int.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class, int.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class, int.class, boolean.class))); - parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString", + parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString", Long.class, int.class, boolean.class, boolean.class))); parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex", Float.class))); @@ -327,7 +327,7 @@ public class TbUtils { String.class))); parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal", String.class))); - parserConfig.addImport("byteArrayToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList", + parserConfig.addImport("bytesToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList", ExecutionContext.class, byte[].class))); parserConfig.addImport("padStart", new MethodStub(TbUtils.class.getMethod("padStart", String.class, int.class, char.class))); @@ -725,19 +725,19 @@ public class TbUtils { return prepareNumberHexString(l, bigEndian, pref, len, HEX_LEN_LONG_MAX); } - public static String intLongToString(Long number) { - return intLongToString(number, DEC_RADIX); + public static String intLongToRadixString(Long number) { + return intLongToRadixString(number, DEC_RADIX); } - public static String intLongToString(Long number, int radix) { - return intLongToString(number, radix, true); + public static String intLongToRadixString(Long number, int radix) { + return intLongToRadixString(number, radix, true); } - public static String intLongToString(Long number, int radix, boolean bigEndian) { - return intLongToString(number, radix, bigEndian, false); + public static String intLongToRadixString(Long number, int radix, boolean bigEndian) { + return intLongToRadixString(number, radix, bigEndian, false); } - public static String intLongToString(Long number, int radix, boolean bigEndian, boolean pref) { + public static String intLongToRadixString(Long number, int radix, boolean bigEndian, boolean pref) { if (radix >= 25 && radix <= MAX_RADIX) { return Long.toString(number, radix); } @@ -1344,8 +1344,8 @@ public class TbUtils { * Writes the bit value to the appropriate location in the bins array, starting at the end of the array, * to ensure proper alignment (highest bit to low end). */ - public static byte[] parseLongToBinaryArray(long longValue, int binsLength) { - int len = Math.min(binsLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX); + public static byte[] parseLongToBinaryArray(long longValue, int binLength) { + int len = Math.min(binLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX); byte[] bins = new byte[len]; for (int i = 0; i < len; i++) { bins[len - 1 - i] = (byte) ((longValue >> i) & 1); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 0c22f62f1f..2e36b3df42 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -330,8 +330,8 @@ public class TbUtilsTest { @Test public void parseBytesIntToFloat() { byte[] intValByte = {0x00, 0x00, 0x00, 0x0A}; - Float valueExpected = 10.0f; - Float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true); + float valueExpected = 10.0f; + float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true); Assertions.assertEquals(valueExpected, valueActual); valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, false); Assertions.assertEquals(valueExpected, valueActual); @@ -345,20 +345,23 @@ public class TbUtilsTest { valueExpected = 10.0f; valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, true); Assertions.assertEquals(valueExpected, valueActual); - valueExpected = 1.6777216E8f; - valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false); + valueExpected = 167772.16f; + double factor = 1e3; + valueActual = (float) (TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false) / factor); + Assertions.assertEquals(valueExpected, valueActual); String dataAT101 = "0x01756403671B01048836BF7701F000090722050000"; List byteAT101 = TbUtils.hexToBytes(ctx, dataAT101); float latitudeExpected = 24.62495f; int offset = 9; - valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false); - Assertions.assertEquals(latitudeExpected, valueActual / 1000000); + factor = 1e6; + valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false) / factor); + Assertions.assertEquals(latitudeExpected, valueActual); float longitudeExpected = 118.030576f; - valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false); - Assertions.assertEquals(longitudeExpected, valueActual / 1000000); + valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false) / factor); + Assertions.assertEquals(longitudeExpected, valueActual); valueExpected = 9.185175E8f; valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset); @@ -520,6 +523,18 @@ public class TbUtilsTest { valueExpected = 7.2057594037927936E17d; valueActual = TbUtils.parseBytesLongToDouble(longValByte, 0, 8, false); Assertions.assertEquals(valueExpected, valueActual); + + String dataPalacKiyv = "0x32D009423F23B300B0106E08D96B6C00"; + List byteAT101 = TbUtils.hexToBytes(ctx, dataPalacKiyv); + double latitudeExpected = 50.422775429058610d; + int offset = 0; + double factor = 1e15; + valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset, 8, false); + Assertions.assertEquals(latitudeExpected, valueActual / factor); + + double longitudeExpected = 30.517877378257072d; + valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset + 8, 8, false); + Assertions.assertEquals(longitudeExpected, valueActual / factor); } @Test @@ -720,27 +735,27 @@ public class TbUtilsTest { @Test public void numberToString_Test() { - Assertions.assertEquals("00111010", TbUtils.intLongToString(58L, MIN_RADIX)); - Assertions.assertEquals("0000000010011110", TbUtils.intLongToString(158L, MIN_RADIX)); - Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToString(131585L, MIN_RADIX)); - Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToString(Long.MAX_VALUE, MIN_RADIX)); - Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToString(-Long.MAX_VALUE, MIN_RADIX)); - Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToString(-102L, MIN_RADIX)); - Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToString(-13158L, MIN_RADIX)); - Assertions.assertEquals("777777777777777777777", TbUtils.intLongToString(Long.MAX_VALUE, 8)); - Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToString(Long.MIN_VALUE, 8)); - Assertions.assertEquals("9223372036854775807", TbUtils.intLongToString(Long.MAX_VALUE)); - Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToString(Long.MIN_VALUE)); - Assertions.assertEquals("3366", TbUtils.intLongToString(13158L, 16)); - Assertions.assertEquals("FFCC9A", TbUtils.intLongToString(-13158L, 16)); - Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToString(-13158L, 16, true, true)); - - Assertions.assertEquals("0x0400", TbUtils.intLongToString(1024L, 16, true, true)); - Assertions.assertNotEquals("400", TbUtils.intLongToString(1024L, 16)); - Assertions.assertEquals("0xFFFC00", TbUtils.intLongToString(-1024L, 16, true, true)); - Assertions.assertNotEquals("0xFC00", TbUtils.intLongToString(-1024L, 16, true, true)); - - Assertions.assertEquals("hazelnut", TbUtils.intLongToString(1356099454469L, MAX_RADIX)); + Assertions.assertEquals("00111010", TbUtils.intLongToRadixString(58L, MIN_RADIX)); + Assertions.assertEquals("0000000010011110", TbUtils.intLongToRadixString(158L, MIN_RADIX)); + Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToRadixString(131585L, MIN_RADIX)); + Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToRadixString(Long.MAX_VALUE, MIN_RADIX)); + Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToRadixString(-Long.MAX_VALUE, MIN_RADIX)); + Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToRadixString(-102L, MIN_RADIX)); + Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToRadixString(-13158L, MIN_RADIX)); + Assertions.assertEquals("777777777777777777777", TbUtils.intLongToRadixString(Long.MAX_VALUE, 8)); + Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToRadixString(Long.MIN_VALUE, 8)); + Assertions.assertEquals("9223372036854775807", TbUtils.intLongToRadixString(Long.MAX_VALUE)); + Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToRadixString(Long.MIN_VALUE)); + Assertions.assertEquals("3366", TbUtils.intLongToRadixString(13158L, 16)); + Assertions.assertEquals("FFCC9A", TbUtils.intLongToRadixString(-13158L, 16)); + Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToRadixString(-13158L, 16, true, true)); + + Assertions.assertEquals("0x0400", TbUtils.intLongToRadixString(1024L, 16, true, true)); + Assertions.assertNotEquals("400", TbUtils.intLongToRadixString(1024L, 16)); + Assertions.assertEquals("0xFFFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true)); + Assertions.assertNotEquals("0xFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true)); + + Assertions.assertEquals("hazelnut", TbUtils.intLongToRadixString(1356099454469L, MAX_RADIX)); } @Test @@ -1067,6 +1082,18 @@ public class TbUtilsTest { String actual = TbUtils.hexToBase64(hex); Assertions.assertEquals(expected, actual); } + @Test + public void bytesToHex_Test() { + byte[] bb = {(byte) 0xBB, (byte) 0xAA}; + String expected = "BBAA"; + String actual = TbUtils.bytesToHex(bb); + Assertions.assertEquals(expected, actual); + ExecutionArrayList expectedList = new ExecutionArrayList<>(ctx); + expectedList.addAll(List.of(-69, 83)); + expected = "BB53"; + actual = TbUtils.bytesToHex(expectedList); + Assertions.assertEquals(expected, actual); + } private static List toList(byte[] data) { List result = new ArrayList<>(data.length); From 5c483284235d34489109f266bc91473a0a8d33f1 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 3 Sep 2024 12:13:31 +0200 Subject: [PATCH 60/80] fixed DefaultTbLocalSubscriptionService build --- .../DefaultTbLocalSubscriptionService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index f82f48bf74..bbfdd2d44f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -61,6 +61,7 @@ import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -158,28 +159,29 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer * Even if we cache locally the list of active subscriptions by entity id, it is still time-consuming operation to get them from cache * Since number of subscriptions is usually much less than number of devices that are pushing data. */ - Set staleSubs = new HashSet<>(); + Map> staleSubs = new HashMap<>(); subscriptionsByEntityId.forEach((id, sub) -> { try { pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED)); } catch (TenantNotFoundException e) { - staleSubs.add(id); + staleSubs.computeIfAbsent(sub.getTenantId(), key -> new HashSet<>()).add(id); log.warn("Cleaning up stale subscription {} for tenant {} due to TenantNotFoundException", id, sub.getTenantId()); } catch (Exception e) { log.error("Failed to push subscription {} to manager service", sub, e); } }); - if (!staleSubs.isEmpty()) { + staleSubs.forEach((tenantId, subs) -> { + var subsLock = getSubsLock(tenantId); subsLock.lock(); try { - staleSubs.forEach(entityId -> { + subs.forEach(entityId -> { subscriptionsByEntityId.remove(entityId); entityUpdates.remove(entityId); }); } finally { subsLock.unlock(); } - } + }); } } From 60f89ba47354c13bc380e04507d38df3a31f549d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 13:23:13 +0300 Subject: [PATCH 61/80] added validation for client id length --- .../rule/engine/mqtt/TbMqttNode.java | 16 +++++++-- .../rule/engine/mqtt/TbMqttNodeTest.java | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index c909a44a27..5d9c9548ad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -77,6 +77,8 @@ public class TbMqttNode extends TbAbstractExternalNode { this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); try { this.mqttClient = initClient(ctx); + } catch (TbNodeException e) { + throw e; } catch (Exception e) { throw new TbNodeException(e); } @@ -119,8 +121,7 @@ public class TbMqttNode extends TbAbstractExternalNode { MqttClientConfig config = new MqttClientConfig(getSslContext()); config.setOwnerId(getOwnerId(ctx)); if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { - config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ? - this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId()); + config.setClientId(getClientId(ctx)); } config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); @@ -146,6 +147,17 @@ public class TbMqttNode extends TbAbstractExternalNode { return client; } + private String getClientId(TbContext ctx) throws TbNodeException { + String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ? + this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : + this.mqttNodeConfiguration.getClientId(); + if (clientId.length() > 23) { + throw new TbNodeException("Client ID was too long '" + clientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); + } + return clientId; + } + MqttClient getMqttClient(TbContext ctx, MqttClientConfig config) { return MqttClient.create(config, null, ctx.getExternalCallExecutor()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index da568eefbb..90ecba923b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -191,6 +191,42 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))); } + @Test + public void givenClientIdIsTooLong_whenInit_thenThrowsException() { + String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj"; + mqttNodeConfig.setClientId(invalidClientId); + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("Client ID was too long '" + invalidClientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + + @Test + public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() { + String validClientId = "fertjnhnjj4ge"; + mqttNodeConfig.setClientId("fertjnhnjj4ge"); + mqttNodeConfig.setAppendClientIdSuffix(true); + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + String serviceId = "test-service"; + given(ctxMock.getServiceId()).willReturn(serviceId); + + String resultedClientId = validClientId + "_" + serviceId; + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("Client ID was too long '" + resultedClientId + "'. " + + "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); + } + @Test public void givenFailedByTimeoutConnectResult_whenInit_thenThrowsException() throws ExecutionException, InterruptedException, TimeoutException { mqttNodeConfig.setHost("localhost"); From 3a6a44a9a4567ed97bada32c182193c7405e792d Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 3 Sep 2024 13:42:28 +0300 Subject: [PATCH 62/80] Refactoring after review --- .../service/queue/DefaultTbCoreConsumerService.java | 2 +- .../service/queue/DefaultTbEdgeConsumerService.java | 12 +++++------- .../server/service/queue/TbEdgeConsumerService.java | 6 +----- application/src/main/resources/thingsboard.yml | 2 ++ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index d30f03262d..2a64dd3388 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -243,7 +243,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, EdgeQueueConfig edgeQueueConfig) throws InterruptedException { @@ -235,7 +233,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService { - +public interface TbEdgeConsumerService { } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 9de3d88568..aefb78106d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1798,6 +1798,8 @@ queue: pack-processing-timeout: "${TB_QUEUE_EDGE_PACK_PROCESSING_TIMEOUT_MS:10000}" # Retries for processing a failure message pack by Edge services pack-processing-retries: "${TB_QUEUE_EDGE_MESSAGE_PROCESSING_RETRIES:3}" + # Enable/disable a separate consumer per partition for Edge queue + consumer-per-partition: "${TB_QUEUE_EDGE_CONSUMER_PER_PARTITION:false}" stats: # Enable/disable statistics for Edge services enabled: "${TB_QUEUE_EDGE_STATS_ENABLED:true}" From ea5519893bfa16b4d1b56fab8a1137cc5012763a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 14:09:03 +0300 Subject: [PATCH 63/80] fixed error message --- .../main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 5d9c9548ad..912496f39e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -152,7 +152,7 @@ public class TbMqttNode extends TbAbstractExternalNode { this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId(); if (clientId.length() > 23) { - throw new TbNodeException("Client ID was too long '" + clientId + "'. " + + throw new TbNodeException("Client ID is too long '" + clientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); } return clientId; From 03af2564eb6a2e022a8bc1cacd478ef3b1577aab Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 3 Sep 2024 14:22:29 +0300 Subject: [PATCH 64/80] tbel: refactoring tbUtils with docs #2 --- .../src/main/java/org/thingsboard/script/api/tbel/TbUtils.java | 2 +- .../test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 6651f26cc5..43d69e602e 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -1154,7 +1154,7 @@ public class TbUtils { } public static void raiseError(String message, Object value) { - String msg = value == null ? message : message + " for value " + value; + String msg = value == null ? message : message + " A value of " + value + " is invalid."; throw new RuntimeException(msg); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 2e36b3df42..950587e2e4 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -891,7 +891,7 @@ public class TbUtilsTest { TbUtils.raiseError(message, value); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. for value 4")); + Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid.")); } try { TbUtils.raiseError(message); From 977c36d83f123a5a1426ea9eb9b99f1de180875e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Sep 2024 16:33:04 +0300 Subject: [PATCH 65/80] Fix RelatedEdgesCacheValue --- .../server/cache/edge/RelatedEdgesCacheValue.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java index a46b2c5875..1289d09353 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/edge/RelatedEdgesCacheValue.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.cache.edge; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.page.PageData; @@ -23,15 +25,13 @@ import java.io.Serial; import java.io.Serializable; @Data +@NoArgsConstructor +@AllArgsConstructor public class RelatedEdgesCacheValue implements Serializable { @Serial private static final long serialVersionUID = -2765080094748518572L; - private final PageData pageData; - - public RelatedEdgesCacheValue(PageData pageData) { - this.pageData = pageData; - } + private PageData pageData; } From 591cc91733fd491ffb54fa5605f0e8ea7805f44f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Sep 2024 16:33:23 +0300 Subject: [PATCH 66/80] Fix created edge version --- .../server/service/entitiy/edge/DefaultTbEdgeService.java | 2 +- .../java/org/thingsboard/server/common/data/edge/Edge.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java index 5b8fd949bd..8771b48c35 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -54,7 +54,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE if (ActionType.ADDED.equals(actionType)) { ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edgeId); - edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); + savedEdge = edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); edgeService.assignDefaultRuleChainsToEdge(tenantId, edgeId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index ec8ade7f69..34ba77269d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -75,9 +75,9 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel this.tenantId = edge.getTenantId(); this.customerId = edge.getCustomerId(); this.rootRuleChainId = edge.getRootRuleChainId(); + this.name = edge.getName(); this.type = edge.getType(); this.label = edge.getLabel(); - this.name = edge.getName(); this.routingKey = edge.getRoutingKey(); this.secret = edge.getSecret(); this.version = edge.getVersion(); @@ -87,9 +87,9 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel this.tenantId = edge.getTenantId(); this.customerId = edge.getCustomerId(); this.rootRuleChainId = edge.getRootRuleChainId(); + this.name = edge.getName(); this.type = edge.getType(); this.label = edge.getLabel(); - this.name = edge.getName(); this.routingKey = edge.getRoutingKey(); this.secret = edge.getSecret(); this.version = edge.getVersion(); From 937621ea43effb75bb4e3866d23ee2623b7c28cb Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Sep 2024 16:33:31 +0300 Subject: [PATCH 67/80] Fix testDeviceVc_sameTenant --- .../thingsboard/server/service/sync/vc/VersionControlTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index ffc88f7f5a..2e01d700a9 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -241,7 +241,7 @@ public class VersionControlTest extends AbstractControllerTest { checkImportedEntity(tenantId1, device, tenantId1, importedDevice); assertThat(importedDevice.getDeviceProfileId()).isEqualTo(device.getDeviceProfileId()); - assertThat(findDeviceCredentials(device.getId())).isEqualTo(deviceCredentials); + assertThat(findDeviceCredentials(device.getId())).isEqualToIgnoringGivenFields(deviceCredentials, "version"); assertThat(importedDevice.getFirmwareId()).isEqualTo(firmware.getId()); assertThat(importedDevice.getSoftwareId()).isEqualTo(software.getId()); } From b518a1168eb6bdcef6d34974383339ec2c1399af Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 3 Sep 2024 17:08:24 +0300 Subject: [PATCH 68/80] rollback to old api path for backward compatibility --- .../org/thingsboard/server/controller/OAuth2Controller.java | 2 +- ui-ngx/src/app/core/auth/auth.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 261c4705a9..8a8055c4af 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -75,7 +75,7 @@ public class OAuth2Controller extends BaseController { @ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " + "to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " + "the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)") - @PostMapping(value = "/noauth/oauth2/client") + @PostMapping(value = "/noauth/oauth2Clients") public List getOAuth2Clients(HttpServletRequest request, @Parameter(description = "Mobile application package name, to find OAuth2 clients " + "where there is configured mobile application with such package name") diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 6f1c2bc116..5f8a968710 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -224,7 +224,7 @@ export class AuthService { } public loadOAuth2Clients(): Observable> { - const url = '/api/noauth/oauth2/client?platform=' + PlatformType.WEB; + const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB; return this.http.post>(url, null, defaultHttpOptions()).pipe( catchError(err => of([])), From 6d109bdcf236404877e86c38defc45be6867eccb Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 3 Sep 2024 18:05:51 +0300 Subject: [PATCH 69/80] UI: Reset css class tb-suffix-absolute in dashboard-settings-dialog --- .../dashboard-settings-dialog.component.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index 1c3ffbc922..0a1733e851 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -145,8 +145,7 @@
dashboard.min-layout-width
- + @@ -191,7 +190,7 @@
dashboard.row-height
- + @@ -246,7 +245,7 @@
dashboard.mobile-row-height
- + From 9c961878ebb942b9e1c90d6b5afbcc0162a4d122 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 3 Sep 2024 18:15:21 +0300 Subject: [PATCH 70/80] UI: Fixed process entity type in entity table widgets --- .../lib/entity/entities-table-widget.component.ts | 10 ++++++---- .../home/components/widget/lib/table-widget.models.ts | 3 ++- .../en_US/widget/action/show_widget_action_cell_fn.md | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index c6c07fb1ad..ab25a96af1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -49,7 +49,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { BehaviorSubject, fromEvent, merge, Observable, Subject } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; -import { entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort, SortDirection } from '@angular/material/sort'; @@ -408,7 +408,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } as EntityColumn ); this.contentsInfo.entityType = { - useCellContentFunction: false + useCellContentFunction: true, + cellContentFunction: (entityType: EntityType) => + entityType ? this.translate.instant(entityTypeTranslations.get(entityType).type) : '' }; this.stylesInfo.entityType = { useCellStyleFunction: false @@ -869,9 +871,9 @@ class EntityDatasource implements DataSource { } if (datasource.entityType) { entity.id.entityType = datasource.entityType; - entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type); + entity.entityType = datasource.entityType; } else { - entity.entityType = ''; + entity.entityType = null; } this.dataKeys.forEach((dataKey, index) => { const keyData = data[index].data; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 33428e949d..dcb40cd85d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -25,6 +25,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; +import { EntityType } from '@shared/models/entity-type.models'; type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile'; @@ -65,7 +66,7 @@ export interface EntityData { id: EntityId; entityName: string; entityLabel?: string; - entityType?: string; + entityType?: EntityType; actionCellButtons?: TableCellButtonActionDescriptor[]; hasActions?: boolean; [key: string]: any; diff --git a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md index 8708993103..459a71e8cc 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md @@ -36,7 +36,7 @@ return widgetContext.currentUser.authority === 'CUSTOMER_USER'; * Display action only if the entity in the row is device and has type `thermostat`: ```javascript -return data && data.entityType === 'Device' && data.Type === 'thermostat'; +return data && data.entityType === 'DEVICE' && data.Type === 'thermostat'; {:copy-code} ``` From 7adb60e0e6eee0a091a581d3814f72916246ad5d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 3 Sep 2024 18:29:06 +0300 Subject: [PATCH 71/80] fixed error msg in the tests --- .../java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 90ecba923b..8fa8c387fd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -201,7 +201,7 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID was too long '" + invalidClientId + "'. " + + .hasMessage("Client ID is too long '" + invalidClientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); @@ -221,7 +221,7 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { String resultedClientId = validClientId + "_" + serviceId; assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID was too long '" + resultedClientId + "'. " + + .hasMessage("Client ID is too long '" + resultedClientId + "'. " + "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); From bf6bd064b7e4eb3fb82bf0db820a4616dd0aad2b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 4 Sep 2024 12:01:13 +0300 Subject: [PATCH 72/80] UI: Add country data to module providers. --- ui-ngx/src/app/shared/shared.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 21c912edf5..0ed819b248 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -223,6 +223,7 @@ import { HexInputComponent } from '@shared/components/color-picker/hex-input.com import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component'; import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component'; +import { CountryData } from '@shared/models/country.models'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -279,7 +280,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) disableTooltipInteractivity: true } }, - TbBreakPointsProvider + TbBreakPointsProvider, + CountryData ], declarations: [ FooterComponent, From 5e513787d7710f462ea09f6b3ba76c977cb441e9 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 4 Sep 2024 14:51:41 +0300 Subject: [PATCH 73/80] UI: Refactoring translate --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index cc28a4edd3..31ab8a1cf2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3830,6 +3830,8 @@ "open-hint": "Action invoked when user clicks to open component.", "close": "Close", "close-hint": "Action invoked when user clicks to close component.", + "close-state-animation": "Closed state animation", + "close-state-animation-hint": "Whether to enable blinking animation when component is in closed state.", "opened-color": "Opened color", "closed-color": "Closed color", "opened-rotation-angle": "Opened rotation angle", From 3bb9924bde177136579e1cd71c76b6d7d3eb50af Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 4 Sep 2024 14:58:29 +0300 Subject: [PATCH 74/80] UI: Update water stop symbol --- .../src/main/data/json/system/scada_symbols/waterstop.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/waterstop.svg b/application/src/main/data/json/system/scada_symbols/waterstop.svg index e1ed0e0fd5..e3fb963d71 100644 --- a/application/src/main/data/json/system/scada_symbols/waterstop.svg +++ b/application/src/main/data/json/system/scada_symbols/waterstop.svg @@ -25,7 +25,7 @@ }, { "tag": "walve", - "stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.criticalAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n", + "stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.closeAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n", "actions": null } ], @@ -134,9 +134,9 @@ "defaultWidgetActionSettings": null }, { - "id": "criticalAnimation", - "name": "{i18n:scada.symbol.critical-state-animation}", - "hint": "{i18n:scada.symbol.critical-state-animation-hint}", + "id": "closeAnimation", + "name": "{i18n:scada.symbol.close-state-animation}", + "hint": "{i18n:scada.symbol.close-state-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", From 1c2d1f8780e5a7d1a14a37e4b02afe1fdc8ce13c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 5 Sep 2024 10:19:05 +0300 Subject: [PATCH 75/80] UI: Improve widget layout settings: Add preserve aspect ratio and resizable options. --- .../core/services/dashboard-utils.service.ts | 7 +- .../add-widget-dialog.component.ts | 2 + .../dashboard-page.component.ts | 2 +- .../widget/widget-config.component.html | 36 ++-- .../widget/widget-config.component.ts | 22 ++- .../home/models/dashboard-component.models.ts | 176 +++++++++++++++--- .../src/app/shared/models/dashboard.models.ts | 2 + ui-ngx/src/app/shared/models/widget.models.ts | 2 + .../assets/locale/locale.constant-en_US.json | 6 +- 9 files changed, 206 insertions(+), 49 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 53d1df8219..2196969d01 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -345,10 +345,11 @@ export class DashboardUtilsService { return widgetConfig; } - public prepareWidgetForScadaLayout(widget: Widget): Widget { + public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { const config = widget.config; config.showTitle = false; config.dropShadow = false; + config.preserveAspectRatio = isScada; config.padding = '0'; config.margin = '0'; config.backgroundColor = 'rgba(0,0,0,0)'; @@ -654,7 +655,9 @@ export class DashboardUtilsService { mobileOrder: widget.config.mobileOrder, mobileHeight: widget.config.mobileHeight, mobileHide: widget.config.mobileHide, - desktopHide: widget.config.desktopHide + desktopHide: widget.config.desktopHide, + preserveAspectRatio: widget.config.preserveAspectRatio, + resizable: widget.config.resizable }; if (isUndefined(originalColumns)) { originalColumns = 24; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 1d2fb7bfcf..7a2b550201 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -187,6 +187,8 @@ export class AddWidgetDialogComponent extends DialogComponent
-
-
- - {{ 'widget-config.mobile-hide' | translate }} - -
-
- - {{ 'widget-config.desktop-hide' | translate }} - +
+
+
widget-config.resize-options
+
+ + {{ 'widget-config.resizable' | translate }} + +
+
+ + {{ 'widget-config.preserve-aspect-ratio' | translate }} + +
-
+
+
{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}
+
+ + {{ 'widget-config.mobile-hide' | translate }} + +
+
+ + {{ 'widget-config.desktop-hide' | translate }} + +
widget-config.order
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index f554c4bf3f..643a5da990 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -256,6 +256,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe }); this.layoutSettings = this.fb.group({ + resizable: [true], + preserveAspectRatio: [false], mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]], mobileHide: [false], @@ -349,16 +351,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe value: 'actions' } ); - if (this.showLayoutConfig) { - this.headerOptions.push( - { - name: this.isDefaultBreakpoint - ? this.translate.instant('widget-config.mobile') - : this.translate.instant('widget-config.list-layout'), - value: 'mobile' - } - ); - } + this.headerOptions.push( + { + name: this.translate.instant('widget-config.layout'), + value: 'layout' + } + ); if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) { this.selectedOption = this.headerOptions[0].value; } @@ -569,6 +567,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe if (layout) { this.layoutSettings.patchValue( { + resizable: isDefined(layout.resizable) ? layout.resizable : true, + preserveAspectRatio: layout.preserveAspectRatio, mobileOrder: layout.mobileOrder, mobileHeight: layout.mobileHeight, mobileHide: layout.mobileHide, @@ -579,6 +579,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } else { this.layoutSettings.patchValue( { + resizable: true, + preserveAspectRatio: false, mobileOrder: null, mobileHeight: null, mobileHide: false, diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index e7fdfa78e3..ed9ca5123f 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -343,6 +343,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private selectedValue = false; private selectedCallback: (selected: boolean) => void = () => {}; + resizableHandles = {} as any; + + resizeEnabled = true; + isFullscreen = false; isReference = false; @@ -387,6 +391,15 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private gridsterItemComponentSubject = new Subject(); private gridsterItemComponentValue: GridsterItemComponentInterface; + private readonly aspectRatio: number; + + private heightValue: number; + private widthValue: number; + + private rowsValue: number; + private colsValue: number; + + get mobileHide(): boolean { return this.widgetLayout ? this.widgetLayout.mobileHide === true : false; } @@ -397,10 +410,96 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { set gridsterItemComponent(item: GridsterItemComponentInterface) { this.gridsterItemComponentValue = item; + + if (this.widgetLayout?.preserveAspectRatio) { + this.applyPreserveAspectRatio(item); + } + this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue); this.gridsterItemComponentSubject.complete(); } + private applyPreserveAspectRatio(item: GridsterItemComponentInterface) { + this.resizableHandles.ne = false; + this.resizableHandles.sw = false; + this.resizableHandles.nw = false; + + const $item = item.$item; + + this.rowsValue = $item.rows; + this.colsValue = $item.cols; + + Object.defineProperty($item, 'rows', { + get: () => this.rowsValue, + set: v => { + if (this.rowsValue !== v) { + if (this.preserveAspectRatio) { + this.colsValue = v * this.aspectRatio; + } + this.rowsValue = v; + } + } + }); + + Object.defineProperty($item, 'cols', { + get: () => this.colsValue, + set: v => { + if (this.colsValue !== v) { + if (this.preserveAspectRatio) { + this.rowsValue = v / this.aspectRatio; + } + this.colsValue = v; + } + } + }); + + const resizable = item.resize; + + this.heightValue = resizable.height; + this.widthValue = resizable.width; + + const setItemHeight = resizable.setItemHeight.bind(resizable); + const setItemWidth = resizable.setItemWidth.bind(resizable); + resizable.setItemHeight = (height) => { + setItemHeight(height); + this.heightValue = height; + if (this.preserveAspectRatio) { + setItemWidth(height * this.aspectRatio); + } + }; + resizable.setItemWidth = (width) => { + setItemWidth(width); + this.widthValue = width; + if (this.preserveAspectRatio) { + setItemHeight(width / this.aspectRatio); + } + }; + + Object.defineProperty(resizable, 'height', { + get: () => this.heightValue, + set: v => { + if (this.heightValue !== v) { + if (this.preserveAspectRatio) { + this.widthValue = v * this.aspectRatio; + } + this.heightValue = v; + } + } + }); + + Object.defineProperty(resizable, 'width', { + get: () => this.widthValue, + set: v => { + if (this.widthValue !== v) { + if (this.preserveAspectRatio) { + this.heightValue = v / this.aspectRatio; + } + this.widthValue = v; + } + } + }); + } + get highlighted() { return this.highlightedValue; } @@ -434,6 +533,13 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { public widgetLayout?: WidgetLayout, private parentDashboard?: IDashboardComponent, private popoverComponent?: TbPopoverComponent) { + + if (isDefined(widgetLayout?.resizable)) { + this.resizeEnabled = widgetLayout.resizable; + } + if (widgetLayout?.preserveAspectRatio) { + this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY; + } if (!widget.id) { widget.id = guid(); } @@ -603,30 +709,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { } } - @enumerable(true) - get cols(): number { - let res; - if (this.widgetLayout) { - res = this.widgetLayout.sizeX; + get preserveAspectRatio(): boolean { + if (!this.dashboard.isMobileSize && this.widgetLayout) { + return this.widgetLayout.preserveAspectRatio; } else { - res = this.widget.sizeX; + return false; } - return Math.floor(res); + } + + @enumerable(true) + get cols(): number { + return Math.floor(this.sizeX); } set cols(cols: number) { if (!this.dashboard.isMobileSize) { - if (this.widgetLayout) { - this.widgetLayout.sizeX = cols; - } else { - this.widget.sizeX = cols; - } + this.sizeX = cols; } } @enumerable(true) get rows(): number { - let res; + let res: number; if (this.dashboard.isMobileSize) { let mobileHeight; if (this.widgetLayout) { @@ -638,26 +742,50 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { if (mobileHeight) { res = mobileHeight; } else { - const sizeY = this.widgetLayout ? this.widgetLayout.sizeY : this.widget.sizeY; + const sizeY = this.sizeY; res = sizeY * 24 / this.dashboard.gridsterOpts.minCols; } } else { - if (this.widgetLayout) { - res = this.widgetLayout.sizeY; - } else { - res = this.widget.sizeY; - } + res = this.sizeY; } return Math.floor(res); } set rows(rows: number) { if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) { - if (this.widgetLayout) { - this.widgetLayout.sizeY = rows; - } else { - this.widget.sizeY = rows; - } + this.sizeY = rows; + } + } + + get sizeX(): number { + if (this.widgetLayout) { + return this.widgetLayout.sizeX; + } else { + return this.widget.sizeX; + } + } + + set sizeX(sizeX: number) { + if (this.widgetLayout) { + this.widgetLayout.sizeX = sizeX; + } else { + this.widget.sizeX = sizeX; + } + } + + get sizeY(): number { + if (this.widgetLayout) { + return this.widgetLayout.sizeY; + } else { + return this.widget.sizeY; + } + } + + set sizeY(sizeY: number) { + if (this.widgetLayout) { + this.widgetLayout.sizeY = sizeY; + } else { + this.widget.sizeY = sizeY; } } diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 2621175030..c4e767b314 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -43,6 +43,8 @@ export interface WidgetLayout { mobileOrder?: number; col?: number; row?: number; + resizable?: boolean; + preserveAspectRatio?: boolean; } export interface WidgetLayouts { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 3b60cd727e..429bb79e35 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -761,6 +761,8 @@ export interface WidgetConfig { displayTimewindow?: boolean; timewindow?: Timewindow; timewindowStyle?: TimewindowStyle; + resizable?: boolean; + preserveAspectRatio?: boolean; desktopHide?: boolean; mobileHide?: boolean; mobileHeight?: number; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4dd7125f72..aaa51ea3dd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6045,7 +6045,11 @@ "color": "Color", "tooltip": "Tooltip", "units-required": "Unit is required.", - "list-layout": "List layout" + "list-layout": "List layout", + "layout": "Layout", + "resize-options": "Resize options", + "resizable": "Resizable", + "preserve-aspect-ratio": "Preserve aspect ratio" }, "widget-type": { "import": "Import widget type", From 7a84dc655dbff28805e54061d14275f375c884f8 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 5 Sep 2024 12:23:43 +0300 Subject: [PATCH 76/80] added catch for DataValidationException --- .../engine/aws/lambda/TbAwsLambdaNode.java | 5 ++++- .../aws/lambda/TbAwsLambdaNodeTest.java | 22 ++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 06dc0310ae..1dbc81bfa1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; @@ -65,8 +66,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class); String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; - validateFields(config, errorPrefix); try { + validateFields(config, errorPrefix); AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey()); client = AWSLambdaAsyncClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) @@ -75,6 +76,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { .withConnectionTimeout((int) TimeUnit.SECONDS.toMillis(config.getConnectionTimeout())) .withRequestTimeout((int) TimeUnit.SECONDS.toMillis(config.getRequestTimeout()))) .build(); + } catch (DataValidationException e) { + throw new TbNodeException(e, true); } catch (Exception e) { throw new TbNodeException(e); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java index d730b9b837..14ae9bef20 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java @@ -36,6 +36,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; @@ -43,7 +44,6 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -99,7 +99,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidFunctionName_whenInit_thenThrowsException(String funcName) { config.setFunctionName(funcName); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -107,7 +107,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) { config.setAccessKey(accessKey); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -115,7 +115,7 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) { config.setSecretKey(secretAccessKey); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -123,19 +123,19 @@ public class TbAwsLambdaNodeTest { @ValueSource(strings = " ") public void givenInvalidRegion_whenInit_thenThrowsException(String region) { config.setRegion(region); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @Test public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() { config.setConnectionTimeout(-100); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @Test public void givenInvalidRequestTimeout_whenInit_thenThrowsException() { config.setRequestTimeout(-100); - verifyDataValidationExceptionOnInit(); + verifyValidationExceptionOnInit(); } @ParameterizedTest @@ -318,14 +318,16 @@ public class TbAwsLambdaNodeTest { assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg); } - private void verifyDataValidationExceptionOnInit() { + private void verifyValidationExceptionOnInit() { RuleNode ruleNode = new RuleNode(); ruleNode.setName("test"); when(ctx.getSelf()).thenReturn(ruleNode); String errorPrefix = "'test' node configuration is invalid: "; assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) - .isInstanceOf(DataValidationException.class) - .hasMessageContaining(errorPrefix); + .isInstanceOf(TbNodeException.class) + .hasMessageContaining(errorPrefix) + .extracting(e -> ((TbNodeException) e).isUnrecoverable()) + .isEqualTo(true); } private void init() { From 6f2801db0aab0b901524196cc2040f3ff6b3d955 Mon Sep 17 00:00:00 2001 From: Iryna Matveieva <101514424+irynamatveieva@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:38:52 +0300 Subject: [PATCH 77/80] Fixed incorrect display of device state (#11536) * changed logic to save to db activity value from cache * changed check if partition belongs --- .../state/DefaultDeviceStateService.java | 63 +++++----- .../state/DefaultDeviceStateServiceTest.java | 108 ++++++++++++++++++ 2 files changed, 141 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 1c53920a93..6025e874b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -157,7 +157,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService() { @Override - public void onSuccess(@Nullable DeviceStateData state) { + public void onSuccess(DeviceStateData state) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, device.getId()); - if (addDeviceUsingState(tpi, state)) { - save(deviceId, ACTIVITY_STATE, false); + Set deviceIds = partitionedEntities.get(tpi); + boolean isMyPartition = deviceIds != null; + if (isMyPartition) { + deviceIds.add(state.getDeviceId()); + initializeActivityState(deviceId, state); callback.onSuccess(); } else { - log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}" - , tenantId, deviceId, tpi.getFullTopicName()); + log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}", tenantId, deviceId, tpi.getFullTopicName()); callback.onFailure(new RuntimeException("Device belongs to external partition " + tpi.getFullTopicName() + "!")); } } @@ -400,6 +403,21 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIdSet = partitionedEntities.get(tpi); + if (deviceIdSet != null) { + deviceIdSet.remove(deviceId); + } + } + + private void initializeActivityState(DeviceId deviceId, DeviceStateData fetchedState) { + DeviceStateData cachedState = deviceStates.putIfAbsent(fetchedState.getDeviceId(), fetchedState); + boolean activityState = Objects.requireNonNullElse(cachedState, fetchedState).getState().isActive(); + save(deviceId, ACTIVITY_STATE, activityState); + } + @Override protected Map>> onAddedPartitions(Set addedPartitions) { var result = new HashMap>>(); @@ -436,10 +454,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIds = partitionedEntities.get(tpi); + boolean isMyPartition = deviceIds != null; + if (isMyPartition) { + deviceIds.add(state.getDeviceId()); + deviceStates.putIfAbsent(state.getDeviceId(), state); + checkAndUpdateState(state.getDeviceId(), state); + } else { + log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName()); } - checkAndUpdateState(state.getDeviceId(), state); } log.info("[{}] Initialized {} out of {} device states", entry.getKey().getPartition().orElse(0), counter.addAndGet(states.size()), entry.getValue().size()); } @@ -475,18 +499,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIds = partitionedEntities.get(tpi); - if (deviceIds != null) { - deviceIds.add(state.getDeviceId()); - deviceStates.putIfAbsent(state.getDeviceId(), state); - return true; - } else { - log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName()); - return false; - } - } - void checkStates() { try { final long ts = getCurrentTimeMillis(); @@ -619,15 +631,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceIdSet = partitionedEntities.get(tpi); - if (deviceIdSet != null) { - deviceIdSet.remove(deviceId); - } - } - @Override protected void cleanupEntityOnPartitionRemoval(DeviceId deviceId) { cleanupEntity(deviceId); diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 9833396a0a..296191d61b 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.state; +import com.google.common.util.concurrent.Futures; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,8 +29,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; @@ -41,11 +44,13 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.sql.query.EntityQueryRepository; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; @@ -66,6 +71,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -1070,4 +1076,106 @@ public class DefaultDeviceStateServiceTest { then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId); } + @Test + public void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + + TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + + // WHEN + service.onQueueMsg(proto, TbCallback.EMPTY); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(false), any()); + }); + } + + @Test + public void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + long currentTime = System.currentTimeMillis(); + DeviceState deviceState = DeviceState.builder() + .active(false) + .inactivityTimeout(service.getDefaultInactivityTimeoutInSec()) + .build(); + DeviceStateData stateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .deviceCreationTime(currentTime - 10000) + .state(deviceState) + .metaData(TbMsgMetaData.EMPTY) + .build(); + service.deviceStates.put(deviceId, stateData); + + // WHEN + service.onDeviceActivity(tenantId, deviceId, currentTime); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(LAST_ACTIVITY_TIME), eq(currentTime), any()); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any()); + }); + } + + @Test + public void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() throws InterruptedException { + // GIVEN + final long defaultTimeout = 1000; + initStateService(defaultTimeout); + given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + + long currentTime = System.currentTimeMillis(); + DeviceState deviceState = DeviceState.builder() + .active(true) + .lastConnectTime(currentTime - 8000) + .lastActivityTime(currentTime - 4000) + .lastDisconnectTime(0) + .lastInactivityAlarmTime(0) + .inactivityTimeout(3000) + .build(); + DeviceStateData stateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .deviceCreationTime(currentTime - 10000) + .state(deviceState) + .build(); + service.deviceStates.put(deviceId, stateData); + + // WHEN + TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + service.onQueueMsg(proto, TbCallback.EMPTY); + + // THEN + await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); + then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any()); + }); + } + } From cfcf627fe10de244ca69711859c3a14d4792ee8a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 5 Sep 2024 12:48:55 +0300 Subject: [PATCH 78/80] UI: Improve widget resizable options. --- .../core/services/dashboard-utils.service.ts | 1 + .../add-widget-dialog.component.ts | 9 +++++++- .../widget/widget-config.component.ts | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 2196969d01..5c2363789d 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -349,6 +349,7 @@ export class DashboardUtilsService { const config = widget.config; config.showTitle = false; config.dropShadow = false; + config.resizable = !isScada; config.preserveAspectRatio = isScada; config.padding = '0'; config.margin = '0'; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 7a2b550201..709159aec3 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -130,7 +130,14 @@ export class AddWidgetDialogComponent extends DialogComponent { - this.updateWidgetSettingsEnabledState(); - }); - this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => { + merge(this.widgetSettings.get('showTitle').valueChanges, + this.widgetSettings.get('showTitleIcon').valueChanges).subscribe(() => { this.updateWidgetSettingsEnabledState(); }); @@ -263,6 +261,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe mobileHide: [false], desktopHide: [false] }); + + this.layoutSettings.get('resizable').valueChanges.subscribe(() => { + this.updateLayoutEnabledState(); + }); + this.actionsSettings = this.fb.group({ actions: [null, []] }); @@ -589,6 +592,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe {emitEvent: false} ); } + this.updateLayoutEnabledState(); } this.createChangeSubscriptions(); } @@ -622,6 +626,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } } + private updateLayoutEnabledState() { + const resizable: boolean = this.layoutSettings.get('resizable').value; + if (resizable) { + this.layoutSettings.get('preserveAspectRatio').enable({emitEvent: false}); + } else { + this.layoutSettings.get('preserveAspectRatio').disable({emitEvent: false}); + } + } + private updateSchemaForm(settings?: any) { const widgetSettingsFormData: JsonFormComponentData = {}; if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) { From b226ebdccfed54a99fa725b39002a0b3694c5644 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 5 Sep 2024 15:15:32 +0300 Subject: [PATCH 79/80] TbHttpClient - added check for maxInMemoryBufferSizeInKb; should be less than system configured maxInMemoryBufferSizeInKb --- .../rule/engine/rest/TbHttpClient.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 839a7633fc..c21914577f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -80,6 +80,8 @@ public class TbHttpClient { public static final String PROXY_USER = "tb.proxy.user"; public static final String PROXY_PASSWORD = "tb.proxy.password"; + public static final String MAX_IN_MEMORY_BUFFER_SIZE_IN_KB = "tb.http.maxInMemoryBufferSizeInKb"; + private final TbRestApiCallNodeConfiguration config; private EventLoopGroup eventLoopGroup; @@ -129,6 +131,8 @@ public class TbHttpClient { httpClient = httpClient.secure(t -> t.sslContext(sslContext)); } + validateMaxInMemoryBufferSize(config); + this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset") @@ -140,6 +144,21 @@ public class TbHttpClient { } } + private void validateMaxInMemoryBufferSize(TbRestApiCallNodeConfiguration config) throws TbNodeException { + int systemMaxInMemoryBufferSizeInKb = 25000; + try { + Properties properties = System.getProperties(); + if (properties.containsKey(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB)) { + systemMaxInMemoryBufferSizeInKb = Integer.parseInt(properties.getProperty(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB)); + } + } catch (Exception ignored) {} + if (config.getMaxInMemoryBufferSizeInKb() > systemMaxInMemoryBufferSizeInKb) { + throw new TbNodeException("The configured maximum in-memory buffer size (in KB) exceeds the system limit for this parameter.\n" + + "The system limit is " + systemMaxInMemoryBufferSizeInKb + " KB.\n" + + "Please use the system variable '" + MAX_IN_MEMORY_BUFFER_SIZE_IN_KB + "' to override the system limit."); + } + } + EventLoopGroup getSharedOrCreateEventLoopGroup(EventLoopGroup eventLoopGroupShared) { if (eventLoopGroupShared != null) { return eventLoopGroupShared; From 5699c49c535923c85af31068936a1ae7aea66aa1 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 5 Sep 2024 18:49:53 +0300 Subject: [PATCH 80/80] tbel: refactoring raiseError --- .../java/org/thingsboard/script/api/tbel/TbUtils.java | 9 +-------- .../org/thingsboard/script/api/tbel/TbUtilsTest.java | 7 +++++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 43d69e602e..7f7b1f699f 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -315,8 +315,6 @@ public class TbUtils { String.class))); parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI", String.class))); - parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError", - String.class, Object.class))); parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError", String.class))); parserConfig.addImport("isBinary", new MethodStub(TbUtils.class.getMethod("isBinary", @@ -1150,12 +1148,7 @@ public class TbUtils { } public static void raiseError(String message) { - raiseError(message, null); - } - - public static void raiseError(String message, Object value) { - String msg = value == null ? message : message + " A value of " + value + " is invalid."; - throw new RuntimeException(msg); + throw new RuntimeException(message); } private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 950587e2e4..b7276edd8d 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -19,6 +19,7 @@ import com.google.common.collect.Lists; import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.units.qual.A; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -885,14 +886,16 @@ public class TbUtilsTest { @Test public void raiseError_Test() { - String message = "frequency_weighting_type must be 0, 1 or 2."; Object value = 4; + String message = "frequency_weighting_type must be 0, 1 or 2. A value of " + value.toString() + " is invalid."; + try { - TbUtils.raiseError(message, value); + TbUtils.raiseError(message); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid.")); } + message = "frequency_weighting_type must be 0, 1 or 2."; try { TbUtils.raiseError(message); Assertions.fail("Should throw NumberFormatException");