Skip to content

Commit 230acb0

Browse files
authored
fix(function-server) run fs on deno runtime (#1249)
* fix(function-server) run fs on deno runtime * fix(function-server) create workers for workspace function on demand
1 parent c230533 commit 230acb0

File tree

31 files changed

+1961
-1103
lines changed

31 files changed

+1961
-1103
lines changed

.github/workflows/services-build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ jobs:
124124
BRANCH: ${{ github.ref_name }}
125125
COMMIT_SHA: ${{ github.sha }}
126126
run: |
127-
TARGETS="console rotor"
127+
TARGETS="console rotor functions-server"
128128
REGISTRY="${{ secrets.DOCKERHUB_USERNAME }}"
129129
SHORT_SHA=$(git rev-parse --short=7 HEAD)
130130

all.Dockerfile

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,49 @@ ENV JITSU_VERSION_STRING=${JITSU_BUILD_VERSION}
200200

201201

202202
ENTRYPOINT ["/app/entrypoint.sh"]
203+
204+
# ============================================================================
205+
# FUNCTIONS-SERVER STAGE - Deno-based UDF execution with Web Worker isolation
206+
# ============================================================================
207+
# Sandboxed functions execution for free-tier workspaces
208+
FROM denoland/deno:debian AS functions-server
209+
210+
ARG JITSU_BUILD_VERSION=dev,
211+
ARG JITSU_BUILD_DOCKER_TAG=dev,
212+
ARG JITSU_BUILD_COMMIT_SHA=unknown,
213+
214+
WORKDIR /app
215+
216+
# Install curl for healthchecks
217+
RUN apt-get update && \
218+
apt-get install -y --no-install-recommends ca-certificates curl && \
219+
rm -rf /var/lib/apt/lists/*
220+
221+
EXPOSE 3401
222+
223+
# Copy Deno-specific build artifacts from builder
224+
COPY --from=builder /app/services/rotor/dist/functions-server.mjs ./functions-server.mjs
225+
COPY --from=builder /app/services/rotor/dist/workspace-worker.mjs ./workspace-worker.mjs
226+
# Copy node_modules with native deps (installed by build.mts)
227+
# Workspace packages and pure JS deps are bundled into functions-server.mjs by esbuild
228+
COPY --from=builder /app/services/rotor/dist/node_modules ./node_modules
229+
COPY --from=builder /app/services/rotor/dist/package.json ./package.json
230+
231+
ENV JITSU_VERSION_COMMIT_SHA=${JITSU_BUILD_COMMIT_SHA}
232+
ENV JITSU_VERSION_DOCKER_TAG=${JITSU_BUILD_DOCKER_TAG}
233+
ENV JITSU_VERSION_STRING=${JITSU_BUILD_VERSION}
234+
ENV NODE_ENV=production
235+
236+
HEALTHCHECK CMD curl --fail http://localhost:3401/health || exit 1
237+
238+
ENTRYPOINT ["deno", "run", \
239+
"--allow-net", \
240+
"--allow-read", \
241+
"--allow-write=/tmp/jitsu-udf,/data", \
242+
"--allow-env", \
243+
"--allow-sys", \
244+
"--allow-ffi", \
245+
"--allow-run=/app/node_modules/@esbuild/linux-arm64/bin/esbuild,/app/node_modules/@esbuild/linux-x64/bin/esbuild,/app/node_modules/esbuild/bin/esbuild", \
246+
"--unstable-worker-options", \
247+
"--no-check", \
248+
"functions-server.mjs"]

build-fs.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
set -e
3+
4+
DATE_TAG=$(date +"%Y%m%d%H%M")
5+
IMAGE="jitsucom/fs:dev-${DATE_TAG}"
6+
7+
echo "Building rotor image..."
8+
docker buildx build \
9+
--target functions-server \
10+
--progress=plain \
11+
--load \
12+
-t "$IMAGE" \
13+
-f all.Dockerfile \
14+
.
15+
16+
echo "Loading image into minikube..."
17+
minikube image load --overwrite=true "$IMAGE"
18+
19+
echo "Done: $IMAGE"

builder.Dockerfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ FROM node:24-bookworm-slim
33
# Install Node.js 24 manually from NodeSource + all runtime dependencies
44
# This includes everything needed for building AND running the final images
55
RUN apt-get update && \
6-
apt-get install -y ca-certificates gnupg git curl telnet python3 g++ make jq nano cron bash netcat-traditional procps && \
6+
apt-get install -y ca-certificates gnupg git curl telnet python3 g++ make jq nano cron bash netcat-traditional procps unzip && \
77
rm -rf /var/lib/apt/lists/* && \
88
npm -g install pnpm@10 && \
9-
npm cache clean --force
9+
npm cache clean --force && \
10+
ARCH=$(uname -m) && \
11+
curl -fsSL "https://github.com/denoland/deno/releases/latest/download/deno-${ARCH}-unknown-linux-gnu.zip" -o /tmp/deno.zip && \
12+
unzip -o /tmp/deno.zip -d /usr/local/bin && \
13+
chmod +x /usr/local/bin/deno && \
14+
rm /tmp/deno.zip
1015

1116
#print current user
1217
RUN whoami && echo "Current user is $(whoami)"

bulker/jitsubase/appbase/abstract_repository.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,15 @@ func (r *AbstractRepository[T]) start() {
192192
}
193193
}
194194
} else {
195+
// No refresh period configured — poll with a minimum interval to avoid busy-looping
196+
ticker := time.NewTicker(1 * time.Second)
195197
for {
196198
select {
199+
case <-ticker.C:
200+
r.refresh(true)
197201
case <-r.closed:
202+
ticker.Stop()
198203
return
199-
default:
200-
r.refresh(true)
201204
}
202205
}
203206
}

bulker/operator/operator.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func (o *Operator) reconcile() {
199199
continue
200200
}
201201

202-
wsWorkspaceData := CalculateWorkspaceData(ws.ID, connections, functions, true)
202+
wsWorkspaceData := CalculateWorkspaceData(ws.ID, connections, functions)
203203

204204
if slices.Contains(functionsClasses, FunctionsClassPremium) || slices.Contains(functionsClasses, FunctionsClassDedicated) {
205205
wData := *wsWorkspaceData // Copy
@@ -1102,7 +1102,8 @@ func (o *Operator) buildDeploymentFromData(data *DeploymentData) *appsv1.Deploym
11021102

11031103
var resources corev1.ResourceRequirements
11041104
resourcesConfig := o.config.PodsResources
1105-
if data.FunctionsClass == FunctionsClassPremium && o.config.PodsResourcesPremium != "" {
1105+
// free deployment use premium resource because it serve multiple workspaces at once
1106+
if (data.FunctionsClass == FunctionsClassPremium || data.FunctionsClass == FunctionsClassFree) && o.config.PodsResourcesPremium != "" {
11061107
resourcesConfig = o.config.PodsResourcesPremium
11071108
}
11081109
if resourcesConfig != "" {

bulker/operator/repository.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ func CalculateWorkspaceData(
215215
workspaceID string,
216216
connections []*EnrichedConnectionConfig,
217217
functions []*FunctionConfig,
218-
hasDedicatedFS bool,
219218
) *WorkspaceData {
220219
var maxUpdatedAt time.Time
221220
var usesWarehouseAPI bool
@@ -249,7 +248,6 @@ func CalculateWorkspaceData(
249248
Connections: filteredConnections,
250249
Functions: functions,
251250
UsesWarehouseAPI: usesWarehouseAPI,
252-
HasDedicatedFS: hasDedicatedFS,
253251
ConfigHash: configHash,
254252
}
255253
}

bulker/operator/types.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ type WorkspaceData struct {
5050
Connections []*EnrichedConnectionConfig
5151
Functions []*FunctionConfig
5252
UsesWarehouseAPI bool
53-
HasDedicatedFS bool
5453
FunctionsClass string // premium, dedicated, free
5554
ConfigHash string // Hash of connections + functions for change detection
5655
FunctionsConfigMapCount int // Number of functions ConfigMaps (for splitting large data)

libs/destination-functions/src/functions/bulker-destination.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export type DataLayoutImpl<T> = (
6363
) => MappedEvent[] | MappedEvent;
6464

6565
export function jitsuLegacy(event: AnalyticsServerEvent, ctx: FullContext<BulkerDestinationConfig>): MappedEvent {
66-
const flat = toJitsuClassic(event, ctx);
66+
const flat = toJitsuClassic(event, ctx, true);
6767
return { event: omit(flat, JitsuInternalProperties), table: event[TableNameParameter] ?? "events" };
6868
}
6969

libs/functions/__tests__/classic-mapping.test.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AnalyticsServerEvent } from "@jitsu/protocols/analytics";
22
import type { Event as JitsuLegacyEvent } from "@jitsu/sdk-js";
3-
import { FullContext, UserAgent } from "@jitsu/protocols/functions";
4-
import { fromJitsuClassic, removeUndefined, TableNameParameter, toJitsuClassic, toSnakeCase } from "../src";
3+
import { FullContext } from "@jitsu/protocols/functions";
4+
import { fromJitsuClassic, toJitsuClassic } from "../src";
55
import { classicEvents } from "./data/classic-events";
66

77
const identify: AnalyticsServerEvent = {
@@ -264,14 +264,22 @@ const legacyPageExpectedWarehouse = {
264264
};
265265

266266
test("legacy event s3", () => {
267-
const identifyLegacyResult = toJitsuClassic(identify, {
268-
props: { keepOriginalNames: true },
269-
destination: { type: "s3" },
270-
} as unknown as FullContext);
271-
const pageLegacyResult = toJitsuClassic(page, {
272-
props: { keepOriginalNames: true },
273-
destination: { type: "s3" },
274-
} as unknown as FullContext);
267+
const identifyLegacyResult = toJitsuClassic(
268+
identify,
269+
{
270+
props: { keepOriginalNames: true },
271+
destination: { type: "s3" },
272+
} as unknown as FullContext,
273+
true
274+
);
275+
const pageLegacyResult = toJitsuClassic(
276+
page,
277+
{
278+
props: { keepOriginalNames: true },
279+
destination: { type: "s3" },
280+
} as unknown as FullContext,
281+
true
282+
);
275283
console.log(JSON.stringify(identifyLegacyResult, null, 2));
276284
expect(identifyLegacyResult).toStrictEqual(legacyIdentifyExpectedS3);
277285

@@ -280,14 +288,22 @@ test("legacy event s3", () => {
280288
});
281289

282290
test("legacy event warehouse", () => {
283-
const identifyLegacyResult = toJitsuClassic(identify, {
284-
props: { keepOriginalNames: true },
285-
destination: { type: "postgres" },
286-
} as unknown as FullContext);
287-
const pageLegacyResult = toJitsuClassic(page, {
288-
props: { keepOriginalNames: true },
289-
destination: { type: "postgres" },
290-
} as unknown as FullContext);
291+
const identifyLegacyResult = toJitsuClassic(
292+
identify,
293+
{
294+
props: { keepOriginalNames: true },
295+
destination: { type: "postgres" },
296+
} as unknown as FullContext,
297+
true
298+
);
299+
const pageLegacyResult = toJitsuClassic(
300+
page,
301+
{
302+
props: { keepOriginalNames: true },
303+
destination: { type: "postgres" },
304+
} as unknown as FullContext,
305+
true
306+
);
291307
console.log(JSON.stringify(identifyLegacyResult, null, 2));
292308
expect(identifyLegacyResult).toStrictEqual(legacyIdentifyExpectedWarehouse);
293309

@@ -307,7 +323,8 @@ test("classic events mapping", () => {
307323
const restored = fromJitsuClassic(event);
308324
const mapped = toJitsuClassic(
309325
restored as AnalyticsServerEvent,
310-
{ props: { keepOriginalNames: true }, destination: { type: "s3" } } as unknown as FullContext
326+
{ props: { keepOriginalNames: true }, destination: { type: "s3" } } as unknown as FullContext,
327+
true
311328
);
312329
delete mapped.anon_ip;
313330
expect(mapped).toStrictEqual(event);

0 commit comments

Comments
 (0)