Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/ENGINE_INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ class MyEnginePlugin(EngineValidationPlugin):
return {
"qos_features_supported": ["*"],
"composition_nodes_supported": ["TASK", "SEQ"],
"objective_types_supported": ["weighted_sum"],
"objective_types_supported": ["MONO"],
"constraints_supported": ["attribute_bound"],
"type": "HEURISTIC", # or "EXACT" depending on your engine's nature
"schema_version": "v1",
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,30 @@
import es.us.isa.qosawarewsbinding.solution.QoSAwareWSCompositionSolution;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Controller implements HttpHandler {
private static final long MAX_BODY_BYTES = 512L * 1024L * 1024L;
private static final String PAYLOAD_TOO_LARGE_MESSAGE =
"Request body is too large. Maximum allowed size is " + MAX_BODY_BYTES + " bytes.";

private final Gson gson = new Gson();

private static class PayloadTooLargeException extends RuntimeException {
PayloadTooLargeException(String message) {
super(message);
}
}

@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
Expand All @@ -35,7 +47,18 @@ public void handle(HttpExchange exchange) throws IOException {
}

try {
SolveRequest req = gson.fromJson(new InputStreamReader(exchange.getRequestBody()), SolveRequest.class);
String contentLength = exchange.getRequestHeaders().getFirst("Content-Length");
if (contentLength != null) {
try {
if (Long.parseLong(contentLength) > MAX_BODY_BYTES) {
throw new PayloadTooLargeException(PAYLOAD_TOO_LARGE_MESSAGE);
}
} catch (NumberFormatException ignored) {
}
}

String requestBody = readBodyWithLimit(exchange.getRequestBody(), MAX_BODY_BYTES);
SolveRequest req = gson.fromJson(requestBody, SolveRequest.class);
SolveResponse resp = process(req);

String jsonResp = gson.toJson(resp);
Expand All @@ -50,6 +73,12 @@ public void handle(HttpExchange exchange) throws IOException {
OutputStream os = exchange.getResponseBody();
os.write(error.getBytes());
os.close();
} catch (PayloadTooLargeException e) {
String error = "{\"error\": \"" + e.getMessage() + "\"}";
exchange.sendResponseHeaders(413, error.length());
OutputStream os = exchange.getResponseBody();
os.write(error.getBytes());
os.close();
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
Expand All @@ -64,6 +93,23 @@ public void handle(HttpExchange exchange) throws IOException {
}
}

private String readBodyWithLimit(InputStream inputStream, long maxBytes) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
long total = 0;

int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
total += bytesRead;
if (total > maxBytes) {
throw new PayloadTooLargeException(PAYLOAD_TOO_LARGE_MESSAGE);
}
output.write(buffer, 0, bytesRead);
}

return new String(output.toByteArray(), StandardCharsets.UTF_8);
}

private SolveResponse process(SolveRequest req) {
ProblemBuilder builder = new ProblemBuilder();
ProblemBuildResult mapped = builder.build(req);
Expand Down
9 changes: 8 additions & 1 deletion engines/minizinc-csp/model/composition.mzn
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ int: KIND_SEQ = 2;
int: KIND_AND = 3;
int: KIND_XOR = 4;
int: KIND_LOOP = 5;
int: KIND_ELEMENT = 6;

array[1..n_nodes] of int: node_kind;
array[1..n_nodes] of int: node_task_id; % 0 if not TASK
Expand All @@ -48,6 +49,8 @@ array[1..n_qos, 1..5] of int: agg_policy;

% 5. Objective Weights and Bounds
array[1..n_qos] of float: qos_weights;
array[1..n_qos] of float: neutral_qos;
array[1..n_qos] of float: qos_lb;
array[1..n_qos] of float: qos_ub;

% --- VARIABLES ---
Expand Down Expand Up @@ -100,7 +103,7 @@ array[1..n_nodes, 1..n_qos] of var float: node_qos;

% Bound Constraint (Optional but helps solver)
constraint forall(i in 1..n_nodes, q in 1..n_qos)(
node_qos[i,q] >= 0.0 /\ node_qos[i,q] <= qos_ub[q]
node_qos[i,q] >= qos_lb[q] /\ node_qos[i,q] <= qos_ub[q]
);

constraint forall(i in 1..n_nodes)(
Expand All @@ -109,6 +112,10 @@ constraint forall(i in 1..n_nodes)(
forall(q in 1..n_qos)(
node_qos[i,q] == cand_qos[c, q]
)
elseif node_kind[i] == KIND_ELEMENT then
forall(q in 1..n_qos)(
node_qos[i,q] == neutral_qos[q]
)
elseif node_kind[i] == KIND_LOOP then
% Loop Aggregation
% Child is node_children[i,1]
Expand Down
97 changes: 75 additions & 22 deletions engines/minizinc-csp/src/dzn_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,12 @@ export class DznBuilder {

const scaleValue = (val: number, featId: string): number => {
const range = featureRanges[featId] || { min: 0.0, max: 1.0 };
const denom = range.max - range.min;
if (Math.abs(denom) < 1e-12 || !Number.isFinite(val)) return 0.0;
let scaled = (val - range.min) / denom;
if (scaled < 0.0) scaled = 0.0;
if (scaled > 1.0) scaled = 1.0;
if (!Number.isFinite(scaled)) return 0.0;
return scaled;
if (!Number.isFinite(val)) return 0.0;
let bounded = val;
if (Number.isFinite(range.min) && bounded < range.min) bounded = range.min;
if (Number.isFinite(range.max) && bounded > range.max) bounded = range.max;
if (!Number.isFinite(bounded)) return 0.0;
return bounded;
};

const FN_MAP: Record<string, number> = {
Expand All @@ -78,18 +77,47 @@ export class DznBuilder {
};
const DEFAULT_FN = 1;

const usesProductSpace = (featId: string): boolean => {
const compose = (instance.aggregation_policies?.[featId]?.compose || {}) as Record<string, any>;
const fns = [compose.seq?.fn, compose.and?.fn, compose.xor?.fn, compose.loop?.fn]
.map((v: any) => String(v || '').toLowerCase());
return fns.includes('product') || fns.includes('scaled_product');
};

const featureUsesProductSpace: Record<string, boolean> = {};
for (const feat of features) {
featureUsesProductSpace[feat] = usesProductSpace(feat);
}

const toModelValue = (scaledVal: number, featId: string): number => {
if (!featureUsesProductSpace[featId]) {
return scaledVal;
}
const safe = Math.max(1e-12, scaledVal);
return Math.log(safe);
};

const agg_policy: number[][] = [];

for (const feat of features) {
const pol = (instance.aggregation_policies || {})[feat] || {};
const compose = pol.compose || {};
const productSpace = featureUsesProductSpace[feat];

const mapFnForFeature = (fnRaw: any): number => {
const fn = String(fnRaw || '').toLowerCase();
if (productSpace && (fn === 'product' || fn === 'scaled_product')) {
return 1;
}
return FN_MAP[fn] || DEFAULT_FN;
};

const row: number[] = [];
row.push(DEFAULT_FN);
row.push(FN_MAP[compose.seq?.fn?.toLowerCase()] || DEFAULT_FN);
row.push(FN_MAP[compose.and?.fn?.toLowerCase()] || FN_MAP.max);
row.push(FN_MAP[compose.xor?.fn?.toLowerCase()] || 5);
row.push(FN_MAP[compose.loop?.fn?.toLowerCase()] || DEFAULT_FN);
row.push(mapFnForFeature(compose.seq?.fn));
row.push(mapFnForFeature(compose.and?.fn) || FN_MAP.max);
row.push(mapFnForFeature(compose.xor?.fn) || 5);
row.push(mapFnForFeature(compose.loop?.fn));
agg_policy.push(row);
}

Expand Down Expand Up @@ -123,7 +151,7 @@ export class DznBuilder {
let val = qosData[feat];
if (val === undefined || val === null) val = 0.0;
const scaled = scaleValue(Number(val), feat);
row.push(scaled);
row.push(toModelValue(scaled, feat));
}
cand_qos.push(row);
});
Expand All @@ -149,9 +177,25 @@ export class DznBuilder {

const constraints = instance.constraints || [];

const neutral_qos: number[] = [];
for (const featId of features) {
const featDef = featureDefinitions.find((feat: any) => feat.id === featId) || {};
const featDir = (featDef.direction || 'MINIMIZE').toUpperCase();
const neutralRaw =
(instance.aggregation_policies?.[featId]?.neutral as number | undefined) ??
(featDir === 'MAXIMIZE' ? featDef.valid_range?.min : featDef.valid_range?.max);
const neutralValue =
neutralRaw !== undefined && neutralRaw !== null
? toModelValue(scaleValue(Number(neutralRaw), featId), featId)
: 0.0;
neutral_qos.push(neutralValue);
}

const qos_lb: number[] = [];
const qos_ub: number[] = [];
for (let f = 0; f < n_qos; f++) {
const featId = features[f];
const productSpace = featureUsesProductSpace[featId];
const isAvailability =
featId.toLowerCase().includes('availability') || featId.toLowerCase().includes('success');

Expand All @@ -162,7 +206,7 @@ export class DznBuilder {
(featDir === 'MAXIMIZE' ? featDef.valid_range?.min : featDef.valid_range?.max);
const neutralScaled =
neutralRaw !== undefined && neutralRaw !== null
? scaleValue(Number(neutralRaw), featId)
? toModelValue(scaleValue(Number(neutralRaw), featId), featId)
: 0.0;

let constraintMax = 0.0;
Expand All @@ -171,15 +215,15 @@ export class DznBuilder {
if (c.attribute_id !== featId) continue;

if (typeof c.value === 'number') {
constraintMax = Math.max(constraintMax, scaleValue(Number(c.value), featId));
constraintMax = Math.max(constraintMax, Math.abs(toModelValue(scaleValue(Number(c.value), featId), featId)));
} else if (c.value && typeof c.value === 'object') {
const minVal = c.value.min;
const maxVal = c.value.max;
if (minVal !== undefined && minVal !== null) {
constraintMax = Math.max(constraintMax, scaleValue(Number(minVal), featId));
constraintMax = Math.max(constraintMax, Math.abs(toModelValue(scaleValue(Number(minVal), featId), featId)));
}
if (maxVal !== undefined && maxVal !== null) {
constraintMax = Math.max(constraintMax, scaleValue(Number(maxVal), featId));
constraintMax = Math.max(constraintMax, Math.abs(toModelValue(scaleValue(Number(maxVal), featId), featId)));
}
}
}
Expand All @@ -189,12 +233,17 @@ export class DznBuilder {
maxVal = Math.max(1.0, ...cand_qos.map((row) => Math.abs(row[f])));
}

maxVal = Math.max(maxVal, neutralScaled, constraintMax);
maxVal = Math.max(maxVal, Math.abs(neutralScaled), constraintMax);

const seqPol = agg_policy[f]?.[1] || 1;
const loopPol = agg_policy[f]?.[4] || 1;

if (isAvailability || seqPol === 2 || loopPol === 2) {
if (productSpace) {
const absBound = Math.max(1.0, maxVal * Math.max(1, n_tasks) * 10);
qos_lb.push(-absBound);
qos_ub.push(absBound);
} else if (isAvailability || seqPol === 2 || loopPol === 2) {
qos_lb.push(0.0);
qos_ub.push(1.0);
} else if (seqPol === 1 || loopPol === 1) {
let taskSum = 0;
Expand All @@ -205,8 +254,10 @@ export class DznBuilder {
}
}
const loopFactor = 10;
qos_lb.push(0.0);
qos_ub.push(Math.max(1.0, taskSum * loopFactor));
} else {
qos_lb.push(0.0);
qos_ub.push(maxVal * 1.5);
}
}
Expand Down Expand Up @@ -284,7 +335,7 @@ export class DznBuilder {
const qos_weights: number[] = [];
for (const feat of features) {
let w = Number(weightsObj[feat] || 0.0);
if (featureDirection[feat] === 'MAXIMIZE') {
if (featureDirection[feat] === 'MAXIMIZE' && !featureUsesProductSpace[feat]) {
w = -w;
}
qos_weights.push(w);
Expand Down Expand Up @@ -324,15 +375,15 @@ export class DznBuilder {
if (scope === 'global') {
gc_attr.push(attrIdx);
gc_op.push(validOp);
gc_val.push(scaleValue(c.value, featId));
gc_val.push(toModelValue(scaleValue(c.value, featId), featId));
} else if (scope === 'local') {
const taskId = c.task_id || (c.tasks && c.tasks[0]);
const tIdx = taskId ? taskIdx[taskId] : undefined;
if (tIdx) {
lc_task.push(tIdx);
lc_attr.push(attrIdx);
lc_op.push(validOp);
lc_val.push(scaleValue(c.value, featId));
lc_val.push(toModelValue(scaleValue(c.value, featId), featId));
}
}
} else if (kind === 'dependency') {
Expand Down Expand Up @@ -387,6 +438,8 @@ export class DznBuilder {
agg_policy = ${fmt2d(agg_policy)};

qos_weights = ${fmt(qos_weights)};
neutral_qos = ${fmt(neutral_qos)};
qos_lb = ${fmt(qos_lb)};
qos_ub = ${fmt(qos_ub)};

n_global_constraints = ${gc_attr.length};
Expand All @@ -413,7 +466,7 @@ export class DznBuilder {
if (k === 'AND') return 3;
if (k === 'XOR') return 4;
if (k === 'LOOP') return 5;
if (k === 'ELEMENT') return 2;
if (k === 'ELEMENT') return 6;
return 0;
}
}
4 changes: 3 additions & 1 deletion engines/minizinc-csp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import Fastify from 'fastify';
import { Solver } from './solver';
import { JobManager } from './jobs';

const fastify = Fastify({ logger: true });
const MAX_BODY_BYTES = 512 * 1024 * 1024;

const fastify = Fastify({ logger: true, bodyLimit: MAX_BODY_BYTES });
const solver = new Solver();
const jobManager = new JobManager();

Expand Down
Loading