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
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ import {Uri} from "vscode";
import {BundleFileSet, parseBundleYaml, writeBundleYaml} from "../bundle";
import {BundleTarget} from "../bundle/types";
import {Mutex} from "../locking";
import {BundleConfigs, ConfigReaderWriter, isBundleConfig} from "./types";
import {BundleConfig, ConfigReaderWriter, isBundleConfigKey} from "./types";

/**
* Reads and writes bundle configs. This class does not notify when the configs change.
* We use the BundleWatcher to notify when the configs change.
*/
export class BundleConfigReaderWriter
implements ConfigReaderWriter<keyof BundleConfigs>
implements ConfigReaderWriter<keyof BundleConfig>
{
private readonly writeMutex = new Mutex();

private readonly writerMapping: Record<
keyof BundleConfigs,
keyof BundleConfig,
(t: BundleTarget, v: any) => BundleTarget
> = {
clusterId: this.setClusterId,
Expand All @@ -25,10 +25,10 @@ export class BundleConfigReaderWriter
};

private readonly readerMapping: Record<
keyof BundleConfigs,
keyof BundleConfig,
(
t?: BundleTarget
) => Promise<BundleConfigs[keyof BundleConfigs] | undefined>
) => Promise<BundleConfig[keyof BundleConfig] | undefined>
> = {
clusterId: this.getClusterId,
authParams: this.getAuthParams,
Expand All @@ -42,7 +42,7 @@ export class BundleConfigReaderWriter
public async getHost(target?: BundleTarget) {
return target?.workspace?.host;
}
public setHost(target: BundleTarget, value: BundleConfigs["host"]) {
public setHost(target: BundleTarget, value: BundleConfig["host"]) {
target = {...target}; // create an explicit copy so as to not modify the original object
target.workspace = {...target.workspace, host: value};
return target;
Expand All @@ -51,7 +51,7 @@ export class BundleConfigReaderWriter
public async getMode(target?: BundleTarget) {
return target?.mode;
}
public setMode(target: BundleTarget, value: BundleConfigs["mode"]) {
public setMode(target: BundleTarget, value: BundleConfig["mode"]) {
target = {...target};
target.mode = value;
return target;
Expand All @@ -62,7 +62,7 @@ export class BundleConfigReaderWriter
}
public setClusterId(
target: BundleTarget,
value: BundleConfigs["clusterId"]
value: BundleConfig["clusterId"]
) {
target = {...target};
target.compute_id = value;
Expand All @@ -71,12 +71,12 @@ export class BundleConfigReaderWriter

public async getWorkspaceFsPath(
target?: BundleTarget
): Promise<BundleConfigs["workspaceFsPath"]> {
): Promise<BundleConfig["workspaceFsPath"]> {
return target?.workspace?.file_path;
}
public setWorkspaceFsPath(
target: BundleTarget,
value: BundleConfigs["workspaceFsPath"]
value: BundleConfig["workspaceFsPath"]
) {
target = {...target};
target.workspace = {
Expand All @@ -94,7 +94,7 @@ export class BundleConfigReaderWriter
}
public setAuthParams(
target: BundleTarget,
value: BundleConfigs["authParams"]
value: BundleConfig["authParams"]
): BundleTarget {
throw new Error("Not implemented");
}
Expand Down Expand Up @@ -123,10 +123,7 @@ export class BundleConfigReaderWriter
});
}

async getFileToWrite<T extends keyof BundleConfigs>(
key: T,
target: string
) {
async getFileToWrite<T extends keyof BundleConfig>(key: T, target: string) {
const priorityList: {uri: Uri; priority: number}[] = [];
await this.bundleFileSet.forEach(async (data, file) => {
// try to find a file which has the config
Expand Down Expand Up @@ -163,10 +160,10 @@ export class BundleConfigReaderWriter
* @returns status of the write
*/
@Mutex.synchronise("writeMutex")
async write<T extends keyof BundleConfigs>(
async write<T extends keyof BundleConfig>(
key: T,
target: string,
value?: BundleConfigs[T]
value?: BundleConfig[T]
) {
const file = await this.getFileToWrite(key, target);
if (file === undefined) {
Expand Down Expand Up @@ -194,22 +191,22 @@ export class BundleConfigReaderWriter
* @param target target to read from
* @returns value of the config
*/
async read<T extends keyof BundleConfigs>(key: T, target: string) {
async read<T extends keyof BundleConfig>(key: T, target: string) {
const targetObject = (await this.bundleFileSet.bundleDataCache.value)
.targets?.[target];
return (await this.readerMapping[key](targetObject)) as
| BundleConfigs[T]
| BundleConfig[T]
| undefined;
}

async readAll(target: string) {
const configs = {} as any;
for (const key of Object.keys(this.readerMapping)) {
if (!isBundleConfig(key)) {
if (!isBundleConfigKey(key)) {
continue;
}
configs[key] = await this.read(key, target);
}
return configs as BundleConfigs;
return configs as BundleConfig;
}
}
143 changes: 96 additions & 47 deletions packages/databricks-vscode/src/configuration/ConfigModel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {Disposable, EventEmitter, Uri, Event} from "vscode";
import {
BundleConfigs,
DatabricksConfigs,
isBundleConfig,
isOverrideableConfig,
BundleConfig,
DATABRICKS_CONFIG_KEYS,
DatabricksConfigSource,
DatabricksConfig,
isBundleConfigKey,
isOverrideableConfigKey,
} from "./types";
import {ConfigOverrideReaderWriter} from "./ConfigOverrideReaderWriter";
import {BundleConfigReaderWriter} from "./BundleConfigReaderWriter";
Expand All @@ -13,61 +15,82 @@ import {CachedValue} from "../locking/CachedValue";
import {StateStorage} from "../vscode-objs/StateStorage";

function isDirectToBundleConfig(
key: keyof BundleConfigs,
mode?: BundleConfigs["mode"]
key: keyof BundleConfig,
mode?: BundleConfig["mode"]
) {
const directToBundleConfigs: (keyof BundleConfigs)[] = [];
const directToBundleConfigs: (keyof BundleConfig)[] = [];
if (mode !== undefined) {
// filter by mode
}
return directToBundleConfigs.includes(key);
}

const defaults: DatabricksConfigs = {
const defaults: DatabricksConfig = {
mode: "dev",
};

/**
* In memory view of the databricks configs loaded from overrides and bundle.
*/
export class ConfigModel implements Disposable {
private disposables: Disposable[] = [];

private readonly configsMutex = new Mutex();
private readonly configCache = new CachedValue<DatabricksConfigs>(
async (oldValue) => {
if (this.target === undefined) {
return {};
}
const overrides = await this.overrideReaderWriter.readAll(
this.target
);
const bundleConfigs = await this.bundleConfigReaderWriter.readAll(
this.target
);
const newValue: DatabricksConfigs = {
...bundleConfigs,
...overrides,
};

for (const key of <(keyof DatabricksConfigs)[]>(
Object.keys(newValue)
)) {
if (
oldValue === null ||
JSON.stringify(oldValue[key]) !==
JSON.stringify(newValue[key])
) {
this.changeEmitters.get(key)?.emitter.fire();
this.onDidChangeAnyEmitter.fire();
}
private readonly configCache = new CachedValue<{
config: DatabricksConfig;
source: DatabricksConfigSource;
}>(async (oldValue) => {
if (this.target === undefined) {
return {config: {}, source: {}};
}
const overrides = await this.overrideReaderWriter.readAll(this.target);
const bundleConfigs = await this.bundleConfigReaderWriter.readAll(
this.target
);
const newValue: DatabricksConfig = {
...bundleConfigs,
...overrides,
};

const source: DatabricksConfigSource = {};

/* By default undefined values are considered to have come from bundle.
This is because when override for a key is undefined, it means that the key
is not overridden and we want to get the value from bundle.
*/
DATABRICKS_CONFIG_KEYS.forEach((key) => {
source[key] =
overrides !== undefined && key in overrides
? "override"
: "bundle";
});

let didAnyConfigChange = false;
for (const key of DATABRICKS_CONFIG_KEYS) {
if (
// Old value is null, but new value has the key
(oldValue === null && newValue[key] !== undefined) ||
// Old value is not null, and old and new values for the key are different
(oldValue !== null &&
JSON.stringify(oldValue.config[key]) !==
JSON.stringify(newValue[key]))
) {
this.changeEmitters.get(key)?.emitter.fire();
didAnyConfigChange = true;
}
}

return newValue;
if (didAnyConfigChange) {
this.onDidChangeAnyEmitter.fire();
}
);
return {
config: newValue,
source: source,
};
});

private readonly changeEmitters = new Map<
keyof DatabricksConfigs | "target",
keyof DatabricksConfig | "target",
{
emitter: EventEmitter<void>;
onDidEmit: Event<void>;
Expand Down Expand Up @@ -102,7 +125,7 @@ export class ConfigModel implements Disposable {
await this.readTarget();
}

public onDidChange<T extends keyof DatabricksConfigs | "target">(
public onDidChange<T extends keyof DatabricksConfig | "target">(
key: T,
fn: () => any,
thisArgs?: any
Expand Down Expand Up @@ -164,33 +187,59 @@ export class ConfigModel implements Disposable {
this.onDidChangeAnyEmitter.fire();
}

public async get<T extends keyof DatabricksConfigs>(
public async get<T extends keyof DatabricksConfig>(
key: T
): Promise<DatabricksConfig[T] | undefined> {
return (await this.configCache.value).config[key] ?? defaults[key];
}

/**
* Return config value along with source of the config.
* Refer to {@link DatabricksConfigSource} for possible values.
*/
public async getS<T extends keyof DatabricksConfig>(
key: T
): Promise<DatabricksConfigs[T] | undefined> {
return (await this.configCache.value)[key] ?? defaults[key];
): Promise<
| {
config: DatabricksConfig[T];
source: DatabricksConfigSource[T] | "default";
}
| undefined
> {
const {config: fullConfig, source: fullSource} = await this.configCache
.value;
const config = fullConfig[key] ?? defaults[key];
const source =
fullConfig[key] !== undefined ? fullSource[key] : "default";
return config
? {
config,
source,
}
: undefined;
}

@Mutex.synchronise("configsMutex")
public async set<T extends keyof DatabricksConfigs>(
public async set<T extends keyof DatabricksConfig>(
key: T,
value?: DatabricksConfigs[T],
value?: DatabricksConfig[T],
handleInteractiveWrite?: (file: Uri | undefined) => any
): Promise<void> {
// We work with 1 set of configs throughout the function.
// No changes to the cache can happen when the global mutex is held.
// The assumption is that user doesn't change the target mode in the middle of
// writing a new config.
const {mode} = {...(await this.configCache.value)};
const {mode} = {...(await this.configCache.value).config};

if (this.target === undefined) {
throw new Error(
`Can't set configuration '${key}' without selecting a target`
);
}
if (isOverrideableConfig(key)) {
if (isOverrideableConfigKey(key)) {
return this.overrideReaderWriter.write(key, this.target, value);
}
if (isBundleConfig(key)) {
if (isBundleConfigKey(key)) {
const isInteractive = handleInteractiveWrite !== undefined;

// write to bundle if not interactive and the config can be safely written to bundle
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {EventEmitter} from "vscode";
import {Mutex} from "../locking";
import {StateStorage} from "../vscode-objs/StateStorage";
import {OverrideableConfigs, ConfigReaderWriter} from "./types";
import {OverrideableConfig, ConfigReaderWriter} from "./types";

export class ConfigOverrideReaderWriter
implements ConfigReaderWriter<keyof OverrideableConfigs>
implements ConfigReaderWriter<keyof OverrideableConfig>
{
private writeMutex = new Mutex();
private onDidChangeEmitter = new EventEmitter<void>();
Expand All @@ -20,10 +20,10 @@ export class ConfigOverrideReaderWriter
* @returns status of the write
*/
@Mutex.synchronise("writeMutex")
async write<T extends keyof OverrideableConfigs>(
async write<T extends keyof OverrideableConfig>(
key: T,
target: string,
value?: OverrideableConfigs[T]
value?: OverrideableConfig[T]
) {
const data = this.storage.get("databricks.bundle.overrides");
if (data[target] === undefined) {
Expand All @@ -49,10 +49,10 @@ export class ConfigOverrideReaderWriter
return this.storage.get("databricks.bundle.overrides")[target];
}

async read<T extends keyof OverrideableConfigs>(
async read<T extends keyof OverrideableConfig>(
key: T,
target: string
): Promise<OverrideableConfigs[T] | undefined> {
): Promise<OverrideableConfig[T] | undefined> {
return this.storage.get("databricks.bundle.overrides")[target]?.[key];
}
}
Loading