Skip to content

Instantly share code, notes, and snippets.

@icebob
Last active October 2, 2023 10:47
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save icebob/c75d4d532c0d7783eb924a96110b9020 to your computer and use it in GitHub Desktop.
Save icebob/c75d4d532c0d7783eb924a96110b9020 to your computer and use it in GitHub Desktop.
Saga middleware PoC for Moleculer
"use strict";
const _ = require("lodash");
const chalk = require("chalk");
const Promise = require("bluebird");
const ServiceBroker = require("../src/service-broker");
const { MoleculerError } = require("../src/errors");
// --- SAGA MIDDLEWARE ---
const SagaMiddleware = function() {
return {
localAction(handler, action) {
if (action.saga) {
const opts = action.saga;
return function sagaHandler(ctx) {
return handler(ctx)
.then(res => {
if (opts.compensation) {
if (!ctx.meta.$saga) {
ctx.meta.$saga = {
compensations: []
};
}
const comp = {
action: opts.compensation.action
};
if (opts.compensation.params) {
comp.params = opts.compensation.params.reduce((a, b) => {
_.set(a, b, _.get(res, b));
return a;
}, {});
}
ctx.meta.$saga.compensations.unshift(comp);
}
return res;
})
.catch(err => {
if (ctx.meta.$saga && ctx.meta.$saga.compensations) {
// Start compensating
ctx.service.logger.warn(chalk.red.bold("Some error occured. Start compensating..."));
ctx.service.logger.info(ctx.meta.$saga.compensations);
if (ctx.meta.$saga && Array.isArray(ctx.meta.$saga.compensations)) {
return Promise.map(ctx.meta.$saga.compensations, item => {
return ctx.call(item.action, item.params);
}).then(() => {
throw err;
});
}
}
throw err;
});
};
}
return handler;
}
};
};
// --- BROKER ---
const broker = new ServiceBroker({
logFormatter: "short",
middlewares: [
SagaMiddleware()
]
});
// --- CARS SERVICE ---
broker.createService({
name: "cars",
actions: {
reserve: {
saga: {
compensation: {
action: "cars.cancel",
params: ["id"]
}
},
handler(ctx) {
this.logger.info(chalk.cyan.bold("Car is reserved."));
return {
id: 5,
name: "Honda Civic"
};
}
},
cancel: {
handler(ctx) {
this.logger.info(chalk.yellow.bold(`Cancel car reservation of ID: ${ctx.params.id}`));
}
}
}
});
// --- HOTELS SERVICE ---
broker.createService({
name: "hotels",
actions: {
book: {
saga: {
compensation: {
action: "hotels.cancel",
params: ["id"]
}
},
handler(ctx) {
this.logger.info(chalk.cyan.bold("Hotel is booked."));
return {
id: 8,
name: "Holiday Inn",
from: "2019-08-10",
to: "2019-08-18"
};
}
},
cancel: {
handler(ctx) {
this.logger.info(chalk.yellow.bold(`Cancel hotel reservation of ID: ${ctx.params.id}`));
}
}
}
});
// --- FLIGHTS SERVICE ---
broker.createService({
name: "flights",
actions: {
book: {
saga: {
compensation: {
action: "flights.cancel",
params: ["id"]
}
},
handler(ctx) {
return this.Promise.reject(new MoleculerError("Unable to book flight!"));
this.logger.info(chalk.cyan.bold("Flight is booked."));
return {
id: 2,
number: "SQ318",
from: "SIN",
to: "LHR"
};
}
},
cancel: {
handler(ctx) {
this.logger.info(chalk.yellow.bold(`Cancel flight ticket of ID: ${ctx.params.id}`));
}
}
}
});
// --- TRIP SAGA SERVICE ---
broker.createService({
name: "trip-saga",
actions: {
createTrip: {
saga: true,
async handler(ctx) {
try {
const car = await ctx.call("cars.reserve");
const hotel = await ctx.call("hotels.book");
const flight = await ctx.call("flights.book");
this.logger.info(chalk.green.bold("Trip is created successfully: "), { car, flight, hotel });
} catch(err) {
this.logger.error(chalk.red.bold("Trip couldn't be created. Reason: "), err.message);
}
}
}
}
});
// --- START ---
async function start() {
await broker.start();
//broker.repl();
await broker.call("trip-saga.createTrip");
}
start();
@icebob
Copy link
Author

icebob commented Mar 11, 2019

image

@davidroman0O
Copy link

davidroman0O commented Mar 25, 2019

Hey @icebob , I've found something wrong! :)

// --- CARS SERVICE ---
broker.createService({
    name: "cars",
    actions: {
        reserve: {
            saga: {
                compensation: {
                    action: "cars.cancel",
                    params: ["id"]
                }
            },
            handler(ctx) {
                this.logger.info(chalk.cyan.bold("Car is reserved."));
                //    If we already inserted something here and we got an error like that, we got "Cannot read property 'compensations' of undefined"
                //    So the compensation isn't fired :/
                return this.Promise.reject(new MoleculerError("Unable to book flight!")); 
                return {
                    id: 5,
                    name: "Honda Civic"
                };
            }
        },

        cancel: {
            handler(ctx) {
                this.logger.info(chalk.yellow.bold(`Cancel car reservation of ID: ${ctx.params.id}`));
            }
        }
    }
});


// --- FLIGHTS SERVICE ---
broker.createService({
    name: "flights",
    settings: {
        error: false,
    },
    actions: {

        do: {
            handler(ctx) {
                this.settings.error = !this.settings.error;
            }
        },

        book: {
            saga: {
                compensation: {
                    action: "flights.cancel",
                    params: ["id"]
                }
            },
            handler(ctx) {

                if (this.settings.error) {
                    return this.Promise.reject(new MoleculerError("Unable to book flight!"));
                }

                this.logger.info(chalk.cyan.bold("Flight is booked."));
                return {
                    id: 2,
                    number: "SQ318",
                    from: "SIN",
                    to: "LHR"
                };
            }
        },

        cancel: {
            handler(ctx) {
                this.logger.info(chalk.yellow.bold(`Cancel flight ticket of ID: ${ctx.params.id}`));
            }
        }
    }
});


// --- START ---
async function start() {
    await broker.start();

    //broker.repl();
    try {
        await broker.call("trip-saga.createTrip");
    } catch (e) {
        console.log("trip-saga.createTrip -- error ", e);
        throw e;
    }

    try {
        await broker.call("trip-saga.createTrip");
    } catch (e) {
        console.log("trip-saga.createTrip -- error ", e);
        throw e;
    }

    try {
        await broker.call("trip-saga.createTrip");
    } catch (e) {
        console.log("trip-saga.createTrip -- error ", e);
        throw e;
    }
    try {
        await broker.call("flights.do");
    } catch (e) {
        console.log("flights.do -- error ", e);
        throw e;
    }
    try {
        await broker.call("trip-saga.createTrip");
    } catch (e) {
        console.log("trip-saga.createTrip -- error ", e);
        throw e;
    }
}

I just read the code of the saga middleware but I'm not 100% confortable with middlewares to find out exactly if we need to change the execution order to detect the saga and get the compensations or something else. I don't want to complicating everything.

@icebob
Copy link
Author

icebob commented Mar 26, 2019

Hi,
I've fixed. Missing an if statement in the catch branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment