400 Bad Request “message”: “Forbidden”

Estou a avaliar a API do Jasmin para uma possível integração, mas não estou a conseguir consumir a API.

Efetuei os passos seguintes:

  1. Registei uma Conta no JasminSoftware.com
  2. Acedi à área de Developer https://nitrogen.primaverabss.com/developer/dashboard
  3. Criei uma Subscrição de testes
  4. Criei uma App de testes
  5. Subscrevi a App de testes na Subscrição de testes
  6. Adicionei as coleções do Postman disponibilizadas no Github
  7. Obtive o access_token com sucesso
  8. Tentei consumir vários Endpoints usando o access_token mas sem sucesso
  9. Obtenho sempre a resposta { "message": "Forbidden" }

Log dos pedidos no Postman (informação sensível omitida)

Obtenção do Access Token:

POST /core/connect/token Content-Type: application/x-www-form-urlencoded User-Agent: PostmanRuntime/7.15.0 Accept: */* Cache-Control: no-cache Host: identity.primaverabss.com accept-encoding: gzip, deflate content-length: 120 Connection: keep-alive client_secret=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&client_id=xxxxxx&scope=application&grant_type=client_credentials  HTTP/1.1 200 status: 200 Cache-Control: no-store, no-cache, max-age=0 Pragma: no-cache Transfer-Encoding: chunked Content-Type: application/json; charset=UTF-8 Content-Encoding: gzip Vary: Accept-Encoding Server: Kestrel Request-Context: appId=cid-v1:17e9b034-971e-4695-a061-0d7084a5a033 X-Powered-By: ASP.NET Date: Tue, 16 Jul 2019 10:05:22 GMT  {"access_token":"XYZ","expires_in":14400,"token_type":"Bearer"} 

Exemplo Lista Faturas


GET /api/222870/222870-0002/billing/invoices/ Content-Type: application/json Authorization: Bearer XYZ User-Agent: PostmanRuntime/7.15.0 Accept: */* Cache-Control: no-cache Postman-Token: fc8bb4a6-5467-46f2-a2fe-84ac81a3a904 Host: my.jasminsoftware.com cookie: xxx accept-encoding: gzip, deflate Connection: keep-alive  HTTP/1.1 400 status: 400 Content-Type: application/json; charset=utf-8 Server: Microsoft-IIS/10.0 Request-Context: appId=cid-v1:07e06186-bc8d-4628-9f1a-465b9a0bb54e X-Powered-By: ASP.NET X-Powered-By: ARR/3.0 Date: Tue, 16 Jul 2019 10:10:23 GMT Content-Length: 30  { "message": "Forbidden" } 

Agradeço a ajuda.

What can I do to prevent or reduce message loss in a microservices system?

Quite often I have methods that do the following:

  1. Process some data
  2. (frequent, but optional) Save some state to database
  3. Publish a message to a queue / topic

What options do I have to protect myself against transient errors (but not only transient) with #3? Implementing a retry / repeat mechanism is one approach, but it probably won’t work if the issue that prevents the message from being sent lasts longer than a few seconds or a few minutes.

How to check if the command is execuable when it delivers through asynchronous message queue?

The aggregate root’s state is the product of the commands it receives. So different order of receiving the commands produce different aggregate root states.

How can I check domain rules based on the state when the order of commands are not guaranteed? How to check if a command can be executed based on the last state of the aggregate root if the command is sent by an asynchronous message queue like Rabbitmq?

Hat’s the use of event sourcing and asynchronous message queue for sending commands

In short: Out of order commands end up with out of order events, so does the ordering of the events matter in such situations?

Since we are uncertain about the order of commands when an asynchronous message queue like Rabbitmq is used for sending the commands,so we are uncertain about the order of the resulting events.

So what is the use of persisting the events as a sequence when we are unsure about their order?

What are design tips for using rabbitmq for sending commands and event store?

Magento 2 error in [unknown object].fireEvent(): event name: tinymceSetContent error message: Cannot read property ‘serialize’ of undefined

We are using a magento 2.2.4 version and recently installed an extension pavelleonidov/magento2-tinymce4 to our site to upgrade the editor to tinymce4. Now we are facing an error message at the category page from admin panel.

error: error in [unknown object].fireEvent(): event name: tinymceSetContent error message: Cannot read property ‘serialize’ of undefined

I have however found some related issue with the different error message in github but I was not able to fix this error with their reference.

Error Picture enter image description here

The error message are coming 6 times. We do have 4 more content editors from the theme. So whenever we click the menu tab this error message pops up. You can see the menu tab from the above picture.

File Browser button missing

I am getting this issue in category edit page only, for product edit page it is working fine.

enter image description here

Files used for debugging

  • /vendor/pavelleonidov/module-tinymce4/view/base/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js
  • /lib/web/mage/adminhtml/events.js

After some time of debugging i saw that the issue was in the fireEvent() whenever it was calling with the event name tinymceSetContent. For all the tinymceSetContent events the argument lengths are greater than 1 and the async is false so it is going in the else condition and setting the value for the result variable result = this.arrEvents[evtName][i].method(arguments[1]); but if i try to print the variable or if check the typeof() i am getting undefined.

Events Js File

/**  * Copyright © Magento, Inc. All rights reserved.  * See COPYING.txt for license details.  */  /* global varienEvents */ /* eslint-disable strict */ define([     'Magento_Ui/js/modal/alert',     'prototype' ], function (alert) { // from http://www.someelement.com/2007/03/eventpublisher-custom-events-la-pubsub.html window.varienEvents = Class.create();  varienEvents.prototype = {     /**      * Initialize.      */     initialize: function () {         this.arrEvents = {};         this.eventPrefix = '';     },      /**     * Attaches a {handler} function to the publisher's {eventName} event for execution upon the event firing     * @param {String} eventName     * @param {Function} handler     * @param {Boolean} [asynchFlag] - Defaults to false if omitted.     * Indicates whether to execute {handler} asynchronously (true) or not (false).     */     attachEventHandler: function (eventName, handler) {         var asynchVar, handlerObj;          if (typeof handler == 'undefined' || handler == null) {             return;         }         eventName += this.eventPrefix;         // using an event cache array to track all handlers for proper cleanup         if (this.arrEvents[eventName] == null) {             this.arrEvents[eventName] = [];         }         //create a custom object containing the handler method and the asynch flag         asynchVar = arguments.length > 2 ? arguments[2] : false;         handlerObj = {             method: handler,             asynch: asynchVar         };         this.arrEvents[eventName].push(handlerObj);     },      /**     * Removes a single handler from a specific event     * @param {String} eventName - The event name to clear the handler from     * @param {Function} handler - A reference to the handler function to un-register from the event     */     removeEventHandler: function (eventName, handler) {         eventName += this.eventPrefix;          if (this.arrEvents[eventName] != null) {             this.arrEvents[eventName] = this.arrEvents[eventName].reject(function (obj) {                 return obj.method == handler; //eslint-disable-line eqeqeq             });         }     },      /**     * Removes all handlers from a single event     * @param {String} eventName - The event name to clear handlers from     */     clearEventHandlers: function (eventName) {         eventName += this.eventPrefix;         this.arrEvents[eventName] = null;     },      /**     * Removes all handlers from ALL events     */     clearAllEventHandlers: function () {         this.arrEvents = {};     },      /**     * Fires the event {eventName}, resulting in all registered handlers to be executed.     * It also collects and returns results of all non-asynchronous handlers     * @param {String} eventName - The name of the event to fire     * @param {Object} [args] - Any object, will be passed into the handler function as the only argument     * @return {Array}     */     fireEvent: function (eventName) {         var evtName = eventName + this.eventPrefix,             results = [],             result, len, i, eventArgs, method, eventHandler;          if (this.arrEvents[evtName] != null) {             len = this.arrEvents[evtName].length; //optimization              for (i = 0; i < len; i++) {                 /* eslint-disable max-depth */                 try {                     if (arguments.length > 1) {                         if (this.arrEvents[evtName][i].asynch) {                             eventArgs = arguments[1];                             method = this.arrEvents[evtName][i].method.bind(this);                             setTimeout(function () { //eslint-disable-line no-loop-func                                 method(eventArgs);                             }, 10);                         } else {                             result = this.arrEvents[evtName][i].method(arguments[1]);                         }                     } else {                         if (this.arrEvents[evtName][i].asynch) { //eslint-disable-line no-lonely-if                             eventHandler = this.arrEvents[evtName][i].method;                             setTimeout(eventHandler, 1);                         } else if (                             this.arrEvents &&                             this.arrEvents[evtName] &&                             this.arrEvents[evtName][i] &&                             this.arrEvents[evtName][i].method                         ) {                             result = this.arrEvents[evtName][i].method();                         }                     }                     results.push(result);                 }                 catch (e) {                     if (this.id) {                         alert({                             content: 'error: error in ' + this.id + '.fireEvent():\n\nevent name: ' +                             eventName + '\n\nerror message: ' + e.message                         });                     } else {                         alert({                             content: 'error: error in [unknown object].fireEvent():\n\nevent name: ' +                             eventName + '\n\nerror message: ' + e.message                         });                     }                 }                  /* eslint-enable max-depth */             }         }          return results;     } };  window.varienGlobalEvents = new varienEvents(); //jscs:ignore requireCapitalizedConstructors }); 

Setup Js File

define([   'jquery',   'underscore',   "tinymce",   'mage/adminhtml/wysiwyg/tiny_mce/html5-schema',   'mage/translate',   'prototype',   'mage/adminhtml/events',   'mage/adminhtml/browser' ], function (jQuery, _, tinyMCE, html5Schema) {  tinyMceWysiwygSetup = Class.create();  tinyMceWysiwygSetup.prototype = {     mediaBrowserOpener: null,     mediaBrowserTargetElementId: null,      initialize: function(htmlId, config) {         if (config.baseStaticUrl && config.baseStaticDefaultUrl) {             window.tinymce.baseURL = window.tinymce.baseURL.replace(config.baseStaticUrl, config.baseStaticDefaultUrl);         } else {             window.tinymce.baseURL = require.toUrl('PavelLeonidov_TinyMce4/lib/tinymce4');         }           this.id = htmlId;         this.config = config;         this.schema = config.schema || html5Schema;          _.bindAll(this, 'beforeSetContent', 'saveContent', 'onChangeContent', 'openFileBrowser', 'updateTextArea');          varienGlobalEvents.attachEventHandler('tinymceChange', this.onChangeContent);         varienGlobalEvents.attachEventHandler('tinymceBeforeSetContent', this.beforeSetContent);         varienGlobalEvents.attachEventHandler('tinymceSetContent', this.updateTextArea);         varienGlobalEvents.attachEventHandler('tinymceSaveContent', this.saveContent);          if (typeof tinyMceEditors == 'undefined') {             tinyMceEditors = $  H({});         }          tinyMceEditors.set(this.id, this);     },      setup: function(mode) {         if (this.config['widget_plugin_src']) {             window.tinymce.PluginManager.load('magentowidget', this.config['widget_plugin_src']);         }          if (this.config.plugins) {             this.config.plugins.each(function(plugin) {                 window.tinymce.PluginManager.load(plugin.name, plugin.src);             });         }          if (jQuery.isReady) {             window.tinymce.dom.Event.domLoaded = true;         }           window.tinymce.init(this.getSettings(mode));     },      getSettings: function(mode) {         //var plugins = 'inlinepopups safari pagebreak style layer table advhr advimage emotions iespell media searchreplace contextmenu paste directionality fullscreen noneditable visualchars nonbreaking xhtmlxtras textcolor image';           var plugins = [             'advlist autolink lists link image charmap print preview hr anchor pagebreak',             'searchreplace wordcount visualblocks visualchars code fullscreen',             'insertdatetime media nonbreaking save table contextmenu directionality',             'emoticons template paste textcolor colorpicker textpattern imagetools autoresize'         ];          self = this;           if (this.config['widget_plugin_src']) {             plugins.push('magentowidget');         }           var magentoPluginsOptions = $  H({});         var magentoPlugins = '';          if (this.config.plugins) {             this.config.plugins.each(function(plugin) {                 magentoPlugins = plugin.name + ' ' + magentoPlugins;                 magentoPluginsOptions.set(plugin.name, plugin.options);             });             if (magentoPlugins) {                 plugins.push(magentoPlugins);             }         }           var settings = {             mode: (mode != undefined ? mode : 'none'),             elements: this.id,         //  theme_advanced_buttons1: magentoPlugins + 'magentowidget,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect',         //  theme_advanced_buttons2: 'cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,forecolor,backcolor',         //  theme_advanced_buttons3: 'tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,iespell,media,advhr,|,ltr,rtl,|,fullscreen',         //  theme_advanced_buttons4: 'insertlayer,moveforward,movebackward,absolute,|,styleprops,|,cite,abbr,acronym,del,ins,attribs,|,visualchars,nonbreaking,pagebreak',             theme_advanced_toolbar_location: 'top',             theme_advanced_toolbar_align: 'left',             theme_advanced_statusbar_location: 'bottom',             //extended_valid_elements : "div[*],meta[*],span[*],link[*],a[name|href|target|title|onclick],img[id|class|src|border=0|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name],hr[class|width|size|noshade],font[face|size|color|style],span[class|align|style]",              //selector: '#' + this.htmlId,             schema: "html5",             theme: 'modern',             plugins: plugins,             toolbar1: 'styleselect | bold italic | forecolor backcolor | undo redo | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media | fullscreen | magentowidget' + (magentoPlugins.length ? ' ' + magentoPlugins : ""),             image_advtab: true,             //valid_elements: "*[*]",             codemirror: { indentOnInit: true },             //valid_children: "+body[a|meta|link],+div[*],+a[div|h1|h2|h3|h4|h5|h6|p|#text]",             valid_elements: this.schema.validElements.join(','),             extended_valid_elements: this.schema.validElements.join(','),             valid_children: this.schema.validChildren.join(','),             skin_url: this.config.skin_url,              content_css: [                 this.config.content_css             ],              theme_advanced_resizing: true,             theme_advanced_resize_horizontal: false,             convert_urls: false,             relative_urls: false,             custom_popup_css: this.config['popup_css'],             magentowidget_url: this.config['widget_window_url'],             magentoPluginsOptions: magentoPluginsOptions,             doctype: '<!DOCTYPE html>',             setup: function(ed){                 ed.on('init', function(e) {                     self.onEditorInit.bind(self)                 });                  ed.on('submit', function(e) {                     varienGlobalEvents.fireEvent('tinymceSubmit', e);                 });                  ed.on('paste', function(o) {                     varienGlobalEvents.fireEvent('tinymcePaste', o);                  });                  ed.on('BeforeSetContent', function(o) {                     varienGlobalEvents.fireEvent('tinymceBeforeSetContent', o);                  });                  ed.on('SetContent', function(o) {                     varienGlobalEvents.fireEvent('tinymceSetContent', o);                  });                  ed.on('SaveContent', function(o) {                     varienGlobalEvents.fireEvent('tinymceSaveContent', o);                  });                   var onChange = function(ed, l) {                     varienGlobalEvents.fireEvent('tinymceChange', l);                 };                  ed.on('change', onChange);                  ed.on('KeyUp', onChange);                  ed.on('ExecCommand', function(cmd, ui, val) {                     varienGlobalEvents.fireEvent('tinymceExecCommand', cmd);                  });              }          };            // Set the document base URL         if (this.config.document_base_url) {             settings.document_base_url = this.config.document_base_url;         }          if (this.config.files_browser_window_url) {             settings.file_browser_callback = function(fieldName, url, objectType, w) {                 varienGlobalEvents.fireEvent("open_browser_callback", {                     win: w,                     type: objectType,                     field: fieldName                 });             };         }          if (this.config.width) {             settings.width = this.config.width;         }          if (this.config.height) {             settings.height = this.config.height;         }          if (this.config.settings) {             Object.extend(settings, this.config.settings)         }          return settings;     },      applySchema: function (editor) {         var schema      = editor.schema,             schemaData  = this.schema,             makeMap     = window.tinymce.makeMap;          jQuery.extend(true, {             nonEmpty: schema.getNonEmptyElements(),             boolAttrs: schema.getBoolAttrs(),             whiteSpace: schema.getWhiteSpaceElements(),             shortEnded: schema.getShortEndedElements(),             selfClosing: schema.getSelfClosingElements(),             blockElements: schema.getBlockElements()         }, {             nonEmpty: makeMap(schemaData.nonEmpty),             boolAttrs: makeMap(schemaData.boolAttrs),             whiteSpace: makeMap(schemaData.whiteSpace),             shortEnded: makeMap(schemaData.shortEnded),             selfClosing: makeMap(schemaData.selfClosing),             blockElements: makeMap(schemaData.blockElements)         });     },      openFileBrowser: function(o) {         var typeTitle,             storeId = this.config.store_id !== null ? this.config.store_id : 0,             frameDialog = jQuery('.mce-container[role="dialog"]'),              wUrl = this.config.files_browser_window_url +                 'target_element_id/' + this.id + '/' +                 'store/' + storeId + '/';           this.mediaBrowserOpener = o.win;         this.mediaBrowserTargetElementId = o.field;          if (typeof(o.type) != 'undefined' && o.type != "") {             typeTitle = 'image' == o.type ? this.translate('Insert Image...') : this.translate('Insert Media...');             wUrl = wUrl + "type/" + o.type + "/";         } else {             typeTitle = this.translate('Insert File...');         }          frameDialog.hide();         jQuery('#mce-modal-block').hide();          MediabrowserUtility.openDialog(wUrl, false, false, typeTitle, {             closed: function() {                 frameDialog.show();                 jQuery('#mce-modal-block').show();             }         });     },      translate: function(string) {         return jQuery.mage.__ ? jQuery.mage.__(string) : string;     },      getMediaBrowserOpener: function() {         return this.mediaBrowserOpener;     },      getMediaBrowserTargetElementId: function() {         return this.mediaBrowserTargetElementId;     },      getToggleButton: function() {         return $  ('toggle' + this.id);     },      getPluginButtons: function() {         return $  $  ('#buttons' + this.id + ' > button.plugin');     },      turnOn: function(mode) {         this.closePopups();          this.setup(mode);          window.tinymce.execCommand('mceAddEditor', false, this.id);          this.getPluginButtons().each(function(e) {             e.hide();         });          return this;     },      turnOff: function() {         this.closePopups();          window.tinymce.execCommand('mceRemoveEditor', false, this.id);          this.getPluginButtons().each(function(e) {             e.show();         });          return this;     },      closePopups: function() {         if (typeof closeEditorPopup == 'function') {             // close all popups to avoid problems with updating parent content area             closeEditorPopup('widget_window' + this.id);             closeEditorPopup('browser_window' + this.id);         }     },      toggle: function() {         if (!window.tinymce.get(this.id)) {             this.turnOn();             return true;         } else {             this.turnOff();             return false;         }     },      onEditorInit: function (editor) {         this.applySchema(editor);     },      onFormValidation: function() {         if (window.tinymce.get(this.id)) {             $  (this.id).value = window.tinymce.get(this.id).getContent();         }     },      onChangeContent: function() {         // Add "changed" to tab class if it exists         this.updateTextArea();          if (this.config.tab_id) {             var tab = $  $  ('a[id$  =' + this.config.tab_id + ']')[0];             if ($  (tab) != undefined && $  (tab).hasClassName('tab-item-link')) {                 $  (tab).addClassName('changed');             }         }     },      // retrieve directives URL with substituted directive value     makeDirectiveUrl: function(directive) {         return this.config.directives_url.replace('directive', 'directive/___directive/' + directive);     },      encodeDirectives: function(content) {         // collect all HTML tags with attributes that contain directives         return content.gsub(/<([a-z0-9\-\_]+.+?)([a-z0-9\-\_]+=".*?\{\{.+?\}\}.*?".+?)>/i, function(match) {             var attributesString = match[2];             // process tag attributes string             attributesString = attributesString.gsub(/([a-z0-9\-\_]+)="(.*?)(\{\{.+?\}\})(.*?)"/i, function(m) {                 return m[1] + '="' + m[2] + this.makeDirectiveUrl(Base64.mageEncode(m[3])) + m[4] + '"';             }.bind(this));              return '<' + match[1] + attributesString + '>';          }.bind(this));     },      encodeWidgets: function (content) {         return content.gsub(/\{\{widget(.*?)\}\}/i, function (match) {             var attributes = this.parseAttributesString(match[1]),                 imageSrc, imageHtml;              if (attributes.type) {                 attributes.type = attributes.type.replace(/\\/g, '\');                 imageSrc = this.config['widget_placeholders'][attributes.type];                 imageHtml = '<img';                 imageHtml += ' id="' + Base64.idEncode(match[0]) + '"';                 imageHtml += ' src="' + imageSrc + '"';                 imageHtml += ' title="' +                     match[0].replace(/\{\{/g, '{').replace(/\}\}/g, '}').replace(/\"/g, '&quot;') + '"';                 imageHtml += '>';                 return imageHtml;             }         }.bind(this));     },      decodeDirectives: function(content) {         // escape special chars in directives url to use it in regular expression         var url = this.makeDirectiveUrl('%directive%').replace(/([$  ^.?*!+:=()\[\]{}|\])/g, '\$  1');         var reg = new RegExp(url.replace('%directive%', '([a-zA-Z0-9,_-]+)'));          return content.gsub(reg, function(match) {             return Base64.mageDecode(match[1]);         }.bind(this));     },      /**      * @param {Object} content      * @return {*}      */     decodeWidgets: function (content) {         return content.gsub(/<img([^>]+id=\"[^>]+)>/i, function (match) {             var attributes = this.parseAttributesString(match[1]),                 widgetCode;              if (attributes.id) {                 widgetCode = Base64.idDecode(attributes.id);                  if (widgetCode.indexOf('{{widget') !== -1) {                     return widgetCode;                 }                  return match[0];             }              return match[0];         }.bind(this));     },      parseAttributesString: function(attributes) {         var result = {};          attributes.gsub(             /(\w+)(?:\s*=\s*(?:(?:"((?:\.|[^"])*)")|(?:'((?:\.|[^'])*)')|([^>\s]+)))?/,             function (match) {                 result[match[1]] = match[2];             }         );          return result;     },      updateTextArea: function () {         var editor = window.tinymce.get(this.id),             content;          if (!editor) {             return;         }          content = editor.getContent();         content = this.decodeContent(content);          jQuery('#' + this.id).val(content).trigger('change');     },      decodeContent: function (content) {         var result = content;        // console.log("Decode: " + result);         if (this.config.add_widgets) {             result = this.decodeWidgets(result);             result = this.decodeDirectives(result);         } else if (this.config["add_directives"]) {             result = this.decodeDirectives(result);         }          return result;     },      encodeContent: function (content) {         var result = content;          if (this.config["add_widgets"]) {             result = this.encodeWidgets(result);             result = this.encodeDirectives(result);         } else if (this.config["add_directives"]) {             result = this.encodeDirectives(result);         }          return result;     },      beforeSetContent: function(o){         o.content = this.encodeContent(o.content);     },      saveContent: function(o) {         o.content = this.decodeContent(o.content);     } }; }); 

Please help !

Connect a web-app to to the back-end message queue

We are currently looking into dividing our back-end in multiple services using a message queue (RabbitMQ). Having a message queue suddenly gives us the possibility to update the web-app freely, but do we need to/want to use the message queue itself for that?

The web-client doesn’t really send messages, but just regular web request that result in messages on the back-end. But now that we have detached some processes from web requests, we somehow have to notify the end user of the process of these tasks. (A task can take less then a second to more than a day).

Is it good practice to consume the messages on the front-end and setting up websockets etc to maintain the connection. Or would you solve a problem like this with regular polling of the back end in a specific interval based on expected duration? Do we actually need to notify the user is also something we are wondering (software design question really).

Some technologies we are using are Vuex(front-end), Django(backend API), RabbitMQ(message queue) and Heroku to deploy everything.

MQ integration: How to notify consumers of upcoming message format changes?

We have multiple microservices communicating over MQ. As MQ messages are the interface/contract between the services, whenever we make changes to the MQ message published by a service we need to make the same adjustments on the services which consume the message.

As of now, the number of services is small enough so that we know which services communicate with each other, and can keep the MQ message contract in sync between them. But as the number of services grow this becomes harder.

Option 1: Break things first, then fix it

I’ve been thinking maybe of implementing some kind of health check. Let’s say service A during operations may emit message type X, which is consumed by service B. Service A could then on startup emit a health check type of message, something in the lines of a message X dry-run. When service B receives this, it simply verifies that the message is according to contract. If not, for example if service A have remove a critical field in the message, then service B will reject the message which in turn will end up on a dead-letter exchange, which again will trigger a warning notification to the devops staff.

This approach won’t prevent us from deploying non-compatible message types, but will notify us pretty much instantly when we do. For our use case, this might work due to our very small number of developers and projects, so if we break things like this we’d be able to fix it quite quickly.

Option 2: Early probes

A variation over this might be that we start versioning the MQ message format (which we probably should and will do anyway). Then, when service A plans to upgrade from version 1 of message type X to version 2, server A could early on start emitting “dry-run” type of version 2 of message type X. This would cause service B to drop the message. Say this happens a few days or weeks before service A perform the actual switch from version 1 to version 2, then the devops team will have time to add support for version 2 in the mean time.

Option 3: Manually detecting conflicts before deployment

Another approach would be to have some way of detecting – before the actual deployment – that service A is about to start emitting non-compatible messages in the first place. This would mean that would need to maintain some matrix or something over which versions of message X is support by which consumer, and defer deploying service A (with the new version of message X) until all the consumer are ready for it. How to implement this effectively I don’t know.

Other alternatives

How does other handle message type compatibility between services that communicate using MQ – how do you know that when your service A makes a change to message type X, it won’t break any of the consumers?

PS. I posted this over at Reddit a few days ago, but due to the lack of feedback I decided to post here as well.